Repository navigation
Releases
Releases · YawLabs/aws-mcp
v2.7.1
Changed
- A
tools/callwhose undeclared arguments total more than 64 KiB is refused with JSON-RPC-32602instead of being processed with those arguments silently dropped. Undeclared arguments are never read by any tool, so no legitimate input is lost: declared arguments are never measured, and large ones (anaws_lambda_invokepayload,aws_callparams,aws_scriptcode, anaws_resource_*desired state) go through as before. A small stray argument is still ignored. The refusal names the undeclared keys (at most 10, each cut to 64 characters) and the tool's declared ones. This clears thesecurity-oversized-inputwarning from@yawlabs/mcp-complianceon both the Node and oam legs.
Fixed
- A client cancel now stops the
awsCLI child already in flight, instead of leaving it to run to its per-call timeout (60s by default).runAwsCalltakes anAbortSignal: already aborted, nothing is spawned; aborted mid-call, the child is killed through the same path the timeout uses and the call settles at once ascancelled. Wired intoaws_sso_list_assignments/aws_sso_generate_profilesdiscovery,aws_multi_regionandaws_multi_account, where an in-flight entry now comes back aserrorKind: "cancelled". - Concurrent writes to
~/.aws/credentialsor the AWS config file no longer fail spuriously on Windows. Under contention, Windows can fail the exclusive create of<file>.lockwithEPERM/EACCESinstead ofEEXIST(likely a lock file whose delete is still pending), and the writer treated that as fatal, so a concurrentaws_assume_roleoraws_sso_generate_profilescould fail withEPERM ... credentials.lock. On Windows an unbroken run of those errors is now retried as contention for up to 1 s, after which the real error is reported, so a genuinely read-only~/.awsstill fails fast with its true cause. Other platforms are unchanged.
v2.7.0
Added
- IAM Identity Center bootstrap for a user with no profile yet (#65).
aws_login_starttakesssoSession+startUrl+ssoRegion: it writes a managed[sso-session]block to the AWS config file and runsaws sso login --sso-session(AWS CLI 2.9.0+, withAWS_PROFILEstripped from the child so a stale one cannot fail it).aws_login_completeon such a login returns the token status instead of running an identity check there is no profile for. Two new tools follow it:aws_sso_list_assignments(read-only; every account and role the login can use, flagging which already have a profile) andaws_sso_generate_profiles(writes[profile ...]sections for all assignments, up to 200, or aselection;dryRunpreviews). Generated sections carry amanaged-by: aws-mcpmarker and are the only sections aws-mcp ever changes -- a hand-written section is never modified, and every other byte of the file is preserved. The SSO bearer token reaches the CLI only through a 0600 temp file and never appears in a tool result.aws_list_profilesgainsssoAccountId,ssoRoleNameandmanaged.
Changed
- The oam floor moves from 0.19.1 to 0.19.5, so the
aws-mcplauncher no longer runs the server on oam 0.19.1-0.19.4. 0.19.5 is verified on the installed aarch64-pc-windows-msvc binary through the launcher withOAM_BINpointing at it andAWS_MCP_RUNTIME=oam: a full MCP handshake listing all 30 tools,aws_list_profilesserved, and the documentedaws_scriptsandbox divergence re-measured on it (eval('1+1')returns 2 andFunction('return 7')()returns 7 under oam;Function('return this')()yields a global with noprocessand norequire; both raise the code-generation error under Node). - The locked atomic writer behind
aws_assume_role's credentials file moved tosrc/ini-file.tsand is shared with the config-file writer. No behaviour change foraws_assume_role. - README: new section "Multi-user gateways: one process per user" (#65) -- run one aws-mcp process per user, each with its own
HOME/USERPROFILE,AWS_CONFIG_FILE,AWS_SHARED_CREDENTIALS_FILEandTMPDIR, routed by the gateway's verified principal.
Fixed
AWS_CONFIG_FILEandAWS_SHARED_CREDENTIALS_FILEare honoured everywhere the server reads those files itself (#67).aws_list_profiles, the profile start-URL lookup that picks the cached SSO token foraws_whoami,aws_login_completeandaws_refresh_if_expiring_soon, andaws_assume_roleread~/.aws/config/~/.aws/credentialsdirectly, so with either variable set they disagreed with theawsCLI they spawn. Paths now resolve as botocore resolves them:$NAME/${NAME}(and%NAME%on Windows) are expanded, then~; an unset reference is left as written; the value is not trimmed; and a set-but-empty value means "no file", whichaws_list_profilesreports as an error andaws_assume_rolerefuses, instead of falling back to the default path.- SSO device login recognizes the newer verification URLs (#66, thanks @SolomonHD).
aws_login_startnow accepts the IAM Identity Center portal (https://<id>.awsapps.com/start/#/device) and AWS sign-in (https://...signin.aws/.../device) forms. Asignin.awsURL must end in/device, so an unrelated sign-in URL cannot suppress the PKCE fallback, and the URL is re-read from the full output when the login settles, so a stdout chunk split inside?user_code=...no longer drops the code.
Security
- Lockfile-only bumps, within existing ranges, clearing all open Dependabot alerts and
npm auditfindings (#70): undici 8.12.4 (3 high), fast-uri 3.1.9, hono 4.13.18,@hono/node-server2.1.7, zod 4.6.7, body-parser 2.4.0, qs 6.17.1, ip-address 10.7.6.
v2.6.1
Changed
- The oam floor moves from 0.18.0 to 0.19.1, so the
aws-mcplauncher no longer runs the server on oam 0.18.x or 0.19.0. 0.19.1 is verified on the installed aarch64-pc-windows-msvc binary through the launcher withOAM_BINpointing at it andAWS_MCP_RUNTIME=oam: a full MCP handshake listing all 28 tools,aws_list_profilesserved, and the documentedaws_scriptsandbox divergence re-measured on it (eval('1+1')returns 2 andFunction('return 7')()returns 7 under oam;Function('return this')()still yields a global with noprocessand norequire; the hostprocess.versions.oamread through the bridge was 0.19.1). AnOAM_BINat oam 0.19.0 is now refused underAWS_MCP_RUNTIME=oamwithRun oam self-update, and falls back to Node under the defaultauto. The self-update advice'sssh-keygennote now names only an oam that predates 0.18.0, the first signed release, since a below-floor oam may now be a signed one. - Source comments that said oam drops the creation mode of
open/writeFileSyncnow say that was true before 0.19.1: oam 0.19.1 honours it like Node (re-measured on Windows: a file created 0o400 comes back read-only). Thefchmod/chmod/ private-mode checks stay, as defense in depth for mounts that ignore modes and for a client that runsoam rundirectly and bypasses the launcher's floor. No behaviour change.
Fixed
scripts/check-oam-floor.mjs(run byrelease.shandnpm run check:oam-floor) now runs from a junctioned or symlinked checkout. It decided whether it was the main module by comparingprocess.argv[1]with its own URL, which Node realpaths andargv[1]does not, so from such a checkout it printed nothing and exited 0 -- a silent pass of the release gate. Both sides are now realpath'd. Release tooling only.
v2.6.0
Added
- The server now sends MCP
instructions: a short, plain-ASCII routing guide that names which tool to reach for (start withaws_whoami, one operation withaws_call, fan-out withaws_multi_region/aws_multi_account, resources by CloudFormation type, logs, metrics, docs). It is held under 2000 bytes, the ceiling yaw-mcp cuts upstream instructions at, by a test that also fails if it names a tool that does not exist or leaves one out.src/server-instructions.tshad promised this block since 2.5.0 without it ever being wired. release.shgrades the server with@yawlabs/mcp-compliancebefore every release, pinned at^0.20.4, the current release line (yaw-mcp's ownyaw-mcp auditstill pins^0.17.1).npm run check:compliancerunsmcp-compliance test --strictagainst the published launcher underAWS_MCP_RUNTIME=nodeand again under oam; a required-test failure stops the release, and a leg that could not run (the package not installed, no oam on PATH) is reported as a warning, never as a pass. Both legs grade A here. Release tooling only.
Changed
- The launcher's "no usable oam" advice names the fix for what it actually found. Under
AWS_MCP_RUNTIME=oaman oam below the floor now getsRun oam self-update, a binary that would not run gets "check that it is an executable oam binary for this platform", a missingOAM_BINgets "point OAM_BIN at an existing oam binary", and only a machine with no oam at all is sent to https://oamjs.org (never on linux-arm64, where oam publishes no build). Theoam self-updateadvice also says that updating from an oam older than 0.18.0 needsssh-keygen8.1 or later, because that update installs the first signed release and verifies it. - The launcher finds an oam installed to
OAM_INSTALL_DIR. That is the oam installer's own target directory, and an oam installed there and not onPATHwas never found; it is now searched first. - On an oam host, the launcher takes oam's inherited
--permission/--allow-*flags out ofNODE_OPTIONSbefore it starts the server in a child (Node after a handoff, or a newer oam). oam 0.18.0 hands those flags to every child throughNODE_OPTIONS, and this server is never meant to run sandboxed. The flags are matched the way Node tokenizesNODE_OPTIONS(double quotes, backslash escapes), so a quoted option is kept whole, and a permission path holding a space, which oam appends unquoted, is dropped with its flag. On Node,NODE_OPTIONSis passed through untouched. - A docs fetch under oam that fails with no error code no longer blames an untrusted certificate. Since oam 0.16.3 a refused certificate carries Node's code and gets the
NODE_EXTRA_CA_CERTSadvice as it does on Node; the uncoded failure that remains (measured on oam 0.18.0 with a server that answers TLS with garbage) now points at a TLS-intercepting proxy or an unreachableHTTPS_PROXY. npm run build:binary:oamverifies the cross-build carrier against the release's signedRELEASE-MANIFEST(ssh-keygen -Y verifyagainst oam's release keys, vendored inscripts/oam-release-keys/, with the manifest's tag checked against the one requested), not against the unsignedSHA256SUMSserved beside the binary. It defaults to the launcher's oam floor instead oflatest. A tag before v0.18.0, which has no manifest, is checked as oam's installers check it: itsSHA256SUMSmust match the pinned digest for that tag (oam'srelease-keys/presigning-sums, vendored beside the keys), and a tag with no pin is refused. Every failure aborts the build. Release tooling only.- Source comments about oam behaviour are brought up to the 0.18.0 floor:
writeFileSync'swxflag,mkdtempnames and the child's pick between case-duplicate environment names all now match Node on oam 0.18.0. No behaviour change.
Fixed
scripts/update-manifests.mjsescapes every value it writes into a Ruby string in the Homebrew formula (CodeQL js/incomplete-sanitization). Thedescescaped"but not\, so a description holding\"came out as\\"-- an escaped backslash and then a closing quote -- and it left#{...}alone, which Ruby interpolates inside double quotes, so such a description would have run as code when brew loaded the formula. A newrubyString()escapes the backslash first, then", a#that starts interpolation (#{,#@,#$; a plain#stays as written sobrew styledoes not flag it), CR and LF, and is applied to desc, homepage, version, license, the release URLs, the sha256s and the command name. For the real package.json the generated formula and Scoop manifest are byte-identical to the old script's (checked on v1.5.3). Release tooling only; the server itself is unchanged.
v2.5.2
Changed
- The oam floor moves from 0.17.0 to 0.18.0, so the
aws-mcplauncher no longer runs the server on an older oam. This server is verified on one oam release at a time, and the floor keeps the launcher off anything older than that release. 0.18.0 is verified on the published aarch64-pc-windows-msvc binary, checksum matched against the release SHA256SUMS, through the launcher withOAM_BINpointing at it: a full MCP handshake listing all 28 tools (the same 28 as on Node),aws_list_profilesserved, and the documentedaws_scriptsandbox divergence re-measured on it (inside the sandboxeval('1+1')returns 2 andFunction('return 7')()returns 7 under oam, while both raiseEvalErrorunder Node;Function('return this')()still yields a global with noprocessand norequire; the hostprocess.versions.oamread through the bridge was 0.18.0). If the launcher finds only oam 0.17.x or older, it now falls back to Node under the defaultAWS_MCP_RUNTIME=auto, and exits with an error underAWS_MCP_RUNTIME=oam-- it says so on stderr, naming the version it found and the floor (measured with the published 0.17.0 asOAM_BIN). On Node the tools and the AWS CLI they run are the same; the one difference is inaws_script, whereevalandFunctionraiseEvalErrorinstead of working (see Runtime in the README). A client that runsoam run /path/to/aws-mcp/dist/index.jsdirectly bypasses the launcher and keeps the oam it names. Runoam self-update, or setAWS_MCP_RUNTIME=nodeto make the choice explicit.
Security
- The bundled MCP SDK moves from 1.30.0 to 1.32.1 (GHSA-6qxp-vccf-f47h, high: the SDK's OAuth client could send credentials to an authorization server chosen by the MCP server). The SDK is bundled into
dist, so this ships in the package; the dependency floor is now^1.32.1. Transitive dev dependencies are patched alongside it:proxy-addr2.0.8 (GHSA-jqcg-44mw-7w3h),fast-uri3.1.8 (GHSA-hrr3-gc8f-f4qj) andip-address10.7.3 (GHSA-j6r3-76f7-8jcv).npm auditreports 0 vulnerabilities.
Fixed
release.shruns every mcp-publisher call to the MCP Registry, each login and each publish attempt, under coreutilstimeout(MCP_PUBLISH_TIMEOUT_S, default 90 s) where one (or Homebrew'sgtimeout) is on PATH -- without one the call runs unbounded, and a warning says so -- because mcp-publisher waits for the registry's answer with no limit of its own and a registry that never answered would have hung the release. A publish attempt that gets no answer is retried on the same 30, 60 and 90 s clock as the registry's own 429-504: one the limit stopped, which used to hang the step, and one whose connection failed or dropped (the client'serror sending requestorerror reading response), which used to fail it at once. A login the limit stopped says the registry did not answer; one that failed any other way says how to read mcp-publisher's output (a 401 is the registry refusing the token exchange; a 429, a 5xx or a connection error is the registry or the network) instead of naming token scopes, which never fail a login: the registry reads the org role at login and refuses only at publish. A publish refused with a 403 says what the io.github.YawLabs namespace takes: a YawLabs org Owner whose token can read org roles. Release tooling only; the server itself is unchanged.
v2.5.1
Changed
- The oam floor moves from 0.16.3 to 0.17.0, so the
aws-mcplauncher no longer runs the server on an older oam. This server is verified on one oam release at a time, and the floor keeps the launcher off anything older than that release. 0.17.0 is verified on the published aarch64-pc-windows-msvc binary, checksum matched against the release SHA256SUMS: a full MCP handshake listing all 28 tools,aws_list_profilesserved, and the documentedaws_scriptsandbox divergence re-measured on it (inside the sandboxeval('1+1')returns 2 andFunction('return 7')()returns 7 under oam, while both raiseEvalErrorunder Node;Function('return this')()still yields a global whoseprocessandrequireareundefined). If the launcher finds only oam 0.16.3 or 0.16.4, it now falls back to Node under the defaultAWS_MCP_RUNTIME=auto, and exits with an error underAWS_MCP_RUNTIME=oam-- it says so on stderr, naming the version it found and the floor. On Node the tools and the AWS CLI they run are the same; the one difference is inaws_script, whereevalandFunctionraiseEvalErrorinstead of working (see Runtime in the README). A client that runsoam run /path/to/aws-mcp/dist/index.jsdirectly, as the README's Runtime section shows, bypasses the launcher and keeps the oam it names. Runoam self-update, or setAWS_MCP_RUNTIME=nodeto make the choice explicit.
Fixed
release.shwaits up to 600 s, not 300, for npm to serve a new version before the MCP Registry step, gives the post-publishnpxsmoke 60 attempts 10 s apart, not 30, and polls npm up to 120 times 5 s apart in its final check instead of reading once after 3 s. On 2026-09-29 the @yawlabs/fetch-mcp 0.8.2 release spent 295 s of its 300 s gate waiting for npm to serve the new version, andnpxneeded 313 s, 24 of its 30 attempts, to install @yawlabs/lemonsqueezy-mcp 1.0.1. Release tooling only; the server itself is unchanged.
v2.5.0
Added
commandArgv: the exact argv beside everycommandstring. Every envelope that carriescommandnow also carriescommandArgv-- the same call as an array of unquoted tokens, entry 0 the binary as displayed and one entry per argument, redacted identically. This is the fix for something no amount of quoting could solve. 2.4.0 stoppedcommandfrom EXECUTING when pasted into PowerShell, but it could not make one string correct everywhere: incmd.exesingle quotes are not quoting characters at all, so&,|,^and a raw newline stay live whatever is done to the string (measured: 0 of 24 character-class probes correct, unchanged by the 2.4.0 fix ----query 'x & echo PWNED_CMD'printed PWNED_CMD, and a value carrying a newline ran its tail as a fresh command line). It also could not serve Git Bash on Windows, where the PowerShell-correct doubled-quote form reads as concatenation and silently drops the quotes, turningBuckets[?Name=='prod'].Nameinto a different, invalid expression. And fish treats\\and\'as escapes inside single quotes, so a backslash-bearing value arrived corrupted there -- the one POSIX shell the string form is wrong for. An array has none of these problems: there is nothing for a shell to re-interpret. The string is now RENDERED from the array rather than built beside it, so the two cannot drift and a redaction cannot apply to one and not the other.commandremains, unchanged in shape, as the form for a human to read; parsecommandArgvinstead of it.
Changed
- The oam floor moves from 0.15.2 to 0.16.3, so an oam older than that now falls back to Node. This server is verified on one oam release at a time, and the floor is what enforces that. 0.16.3 is verified: full MCP handshake, all 28 tools, and the documented
aws_scriptsandbox divergence re-measured on it (inside the sandboxeval('1+1')returns 2 andFunction('return 7')()returns 7 under oam, while both raiseEvalErrorunder Node). If you are on oam 0.15.2 through 0.16.2, the launcher will now hand off to Node instead of serving on your oam -- it says so on stderr, naming the version it found and the floor. Nothing about the server's behaviour changes when that happens; oam and Node run the same code and the same resolved AWS CLI. Update oam, or setAWS_MCP_RUNTIME=nodeto make the choice explicit. AWS_MCP_AWS_CLIpointing at a Windowsaws.exeis now refused when the server is not on Windows. This is the WSL case, and it was a trap rather than a configuration:/mnt/c/.../aws.exepasses every other check (the path is absolute, the.exerule is Windows-only, and DrvFs reports 0777 so the executable test is satisfied), and WSL interop really does run it -- so it worked until it didn't. The server is a POSIX process: it writes/tmp/aws-mcp-input-*/params.jsonand passesfile:///tmp/..., andaws_lambda_invokehands the CLI a POSIX outfile path. A Windows CLI resolves both against the current drive, so params over the inline cap cannot be read and an invoke response is written where nothing looks for it. The error names the binary, the platform, and which transports break. Install the Linux build and point this at that. (The override is the only way a Windows executable can be reached on POSIX -- thePATHwalk looks for a file namedaws, neveraws.exe.)
Fixed
- A mis-spelled
AWS_MCP_AWS_CLIno longer passes in silence on macOS and Linux. Environment variables are case-sensitive there, soaws_mcp_aws_cli=/usr/local/bin/awswas not "the override set wrong", it was "no override at all": thePATHwalk answered instead and returned ok, leaving the operator with a working server running a different binary than the one they configured. Measured end to end -- an override naming a real executable plus aPATHholding anotherawsresolved to thePATHone, with no diagnostic. That defeats the whole design of this variable, which is to fail loudly rather than fall back, because it picks which binary handles your credentials. A near-miss now prints one line naming the spelling found and the canonical one, once per spelling rather than once per call. Windows is unaffected: the lookup case-folds there, so the variable is honoured as written. - The
awschild processes passwindowsHide, matching the launcher.bin/aws-mcp.mjsset it on its version probe and both launch paths; the three spawns that run the actual AWS CLI did not, so libuv never addedCREATE_NO_WINDOWfor them. Bounded honestly: a console application inherits its parent's console, so a window could only appear where the server itself has none -- a GUI-subsystem MCP host launching it detached. Asserted rather than measured, because discriminating it needs a console-less parent plus a window probe. Inert on macOS and Linux.
v2.4.0
Security
- The display
commandno longer runs anything when pasted into PowerShell. Every result carries acommandstring built for a reader to paste, and a value containing a single quote made it executable.shellQuoteArgemitted the POSIX close-escape-reopen idiom; PowerShell reads'...'as literal text the way POSIX does but has no backslash escape inside it, so that form tokenises into separate words -- a;in the value ends the statement, whatever follows it RUNS, and a#comments out the dangling quote that would otherwise be a parse error. Measured on win32/arm64 with PowerShell 5.1, driving the real call against a loopback stub: a--queryvalue ofx'; echo PWNED #printed PWNED. The other 24 character-class probes were delivered byte-identically in PowerShell and in Git Bash, so a single quote in the value is the whole trigger -- and a--querysuch asBuckets[?Name=='prod'].Nameis an ordinary way to get one. Quoting is now chosen by the platform the reader is on: Windows emits PowerShell's own escape, a doubled quote, which is also inert in bash ('a''b'is concatenation), so a Git Bash paste yields a wrong value rather than a running command.commandis still a display string, not a universal one: it is correct in a POSIX shell and in PowerShell, and NOT incmd.exe, where&,|and a newline are live whatever the quoting (measured 0 of 24 probes correct, and unchanged by this fix). A%VAR%is now quoted rather than emitted bare, so a batch file no longer expands it away. Re-quote the string yourself before pasting it into a shell other than the server's own. - The 0600 on the two files that can hold credentials is verified now rather than assumed, and a filesystem that cannot honour it fails the call instead of leaking. The
--cli-input-jsonparams file (which can carry a SecureString) and~/.aws/credentials(which carries an access key, a secret key and a session token in plaintext) were each opened at 0600 and thenfchmod-ed at 0600 -- open's mode because Node honours it, thefchmodbecause oam drops it. Neither is sufficient: a filesystem may accept a chmod, report success, and change nothing. Measured on WSL Ubuntu (linux/arm64, Node 22.23.2) against a Windows drive under/mnt(v9fs/DrvFs):mkdtempreturns 0777, the exclusive create returns 0777, andfchmodSync(fd, 0o600)succeeds while changing nothing -- so both files landed world-readable and world-writable, with nothing able to detect it. World-writable is the worse half: the CLI opens the params path after this server has closed it, so another local user can substitute the payload in between. Neither configuration is exotic --TMPDIRinto a Windows drive is how you share one scratch directory between a WSL distro and its Windows host, andAWS_SHARED_CREDENTIALS_FILE(botocore's own variable, named inaws_assume_role's description) pointed at the Windows-side~/.awsis how you share one credentials file and SSO cache. The mode is now checked with onefstat, and a file that cannot be made private is not written at all: the call fails naming the variable to change (TMPDIR/TMP/TEMP, orAWS_SHARED_CREDENTIALS_FILE) before any secret byte reaches disk. This is a failure you can newly hit: with either path on such a mount, calls that used to appear to succeed now fail with that message. That is the intended trade, because the previous behaviour was a silent exposure. CIFS/SMB, FAT-family and NTFS mounts underfmask/dmask, and Docker Desktop bind mounts, are expected to behave the same way -- reasoned, not measured. No POSIX measurement of oam exists anywhere, since oam publishes no linux-arm64 build and this work had no Mac, so every oam filesystem row in this release was measured on Windows. - The AWS CLI is resolved to an absolute path before every call, and never from the working directory, on any platform. 2.3.3 closed the Windows case by setting
NoDefaultCurrentDirectoryInExePathon the server process, which left the protection resting on the server's own environment and did nothing for aPATHthat names the working directory itself: on macOS and Linux an emptyPATHentry or.means the current directory toexecvp. The server now walks the absolute directories on the child environment'sPATHitself --aws.exeonly on Windows, a regular executable file elsewhere -- and spawns the path it found. Every entry that would resolve against a directory the server does not control is skipped rather than searched: empty,., a relativebin, a drive-relativeC:foo, a root-relative\tools, an unexpanded%X%\bin. Each Windowsawschild also getsNoDefaultCurrentDirectoryInExePath=1explicitly, so the CLI's own helpers do not resolve there either --aws ssm start-sessionhands off tosession-manager-pluginby bare name, and acredential_processcan name a bare command too. There is no bare-name fallback: when no CLI is found nothing runs, because falling back is exactly the planted-checkout case on a machine without the CLI. Resolving the path also settles a runtime difference -- oam searches its own install directory before the parentPATHwhile Node searches the working directory -- so Node and oam now run the same binary. The walk is not cached and does not need to be: 0.47-1.16 ms over this host's 58-entryPATH(20 runs, median 0.56) against a CLI start of hundreds of milliseconds, and a cache would go stale after an install or anaws update.
Fixed
aws_logs_tailreturns structured events against a real AWS CLI -- for the first time since it shipped. It wrappedaws logs tail --format jsonon the premise that the output is NDJSON. It never was: that flag selects the CLI's pretty-print formatter, which prints<iso-timestamp> <stream> <message>lines and re-indents any message that is entirely JSON across several lines (awscli/customizations/logs/tail.py, byte-identical from 2.34.3 to the v2 head). The first line never parsed, so every non-empty call took the raw-string fallback:eventswas one text blob,eventCountandtotalEventswerenull,truncatedwasfalse, and themaxEventsbound added in 2.2.0 never applied -- on aws-cli 2.34.3 against a loopback endpoint a six-event window came back as a 637-character string, and a 2,000-event window withmaxEvents: 5as 329,998 characters. Only an empty window was right. Nor can that text be parsed back into events: a message beginning with a timestamp reads as a new event, a stream name with a space cannot be told from the message after it, a traceback's continuation lines carry no prefix, and a JSON message has already been re-serialized. The tool now calls FilterLogEvents throughaws logs filter-log-events-- the same APItailcalls -- whose JSON output is a single document, and returns events as{timestamp, logStreamName, message}:timestampin ISO 8601 UTC with milliseconds,messageverbatim rather than re-indented and right-stripped. The parser is strict now, which is the pin that keeps this from recurring: stdout that is not the document the fixed--queryasked for isok: falsenaming what arrived, where the old parser turned text intook: true. That removes an arm the Stability section documented --eventsas the raw blob witheventCountandtotalEventsbothnull-- soeventsis always an array now. It is not held for a major because every non-empty call against a real CLI landed there: the shape being removed is the bug itself, not an output any caller could have gotten a real event out of. The tail scenarios in the fake are replaced by payloads captured from the real CLI, and a new suite drives the handler through the INSTALLEDawsbinary against an in-process endpoint on everynpm test-- ten cases, four of which also assert the fake returns the identical envelope -- so a fake that drifts from the real format fails instead of hiding the next one.- A log-group ARN now addresses the group it names. The ARN used to be cut down to its bare name, so a source-account ARN silently read the same-named group in the caller's own account, and an ARN for another region read the call's region instead. It is now sent as FilterLogEvents'
logGroupIdentifier(a trailing:*removed), so a cross-account read works from a CloudWatch cross-account monitoring account; an ARN whose region differs from the call's region is refused before anything spawns, with both regions named, because FilterLogEvents is regional; and an AWS CLI older than 2.9.15, which cannot address a group by ARN, gets an error saying so -- naming the ARN's own account and the bare-name workaround -- instead of a silent retarget. - Every
awscall pins the CLI settings that change what this server reads. AWS CLI 2.34.0 addedcli_error_format; set tojson,yaml,textortable, it removes theAn error occurred (Code) when calling ...line the error classifier reads, so an expired or rejected credential came back asnonzero_exitwith no remedy instead ofexpired_creds/invalid_creds(measured on 2.34.3 against a loopback stub answering 403).cli_auto_prompt = onfailed every call with exit 255 before a connection was opened --aws sso loginincluded, soaws_login_startcould not work at all. Both are now pinned through the environment (AWS_CLI_ERROR_FORMAT=enhanced, the CLI's own default, andAWS_CLI_AUTO_PROMPT=off), not with flags: AWS CLI 2.22.0 answers--cli-error-formatwithUnknown optionsand exit 252, while an environment variable it does not know is ignored. The pins go on top of whatever environment the caller built rather than instead of it, soaws_multi_account's per-account credentials and theAWS_MAX_ATTEMPTS=1that keepsaws_lambda_invoketo one attempt both survive them. - Non-ASCII output no longer breaks calls on Windows. Writing to a pipe, the Windows CLI uses the ANSI code page...
v2.3.4
Fixed
aws_iam_simulateanswered per action when it promises per resource, and reported resources as denied that AWS allows. On 2026-07-30 IAM changed SimulatePrincipalPolicy server-side, so every CLI version is affected: it now returns ONEEvaluationResultper action, whose top-levelEvalDecisionis the most restrictive decision across every resource and whoseEvalResourceNameis an ARN TEMPLATE -- literallyarn:${Partition}:s3:::${BucketName}/${KeyName}. The per-resource answers live only inResourceSpecificResults, which this tool dropped. So "can this role read these three buckets" came back as one row that named none of them and said deny for all three because one was denied: safe and unreadable, and the opposite of the tool's job -- it is meant to stop a model attempting work that will 403, and instead it stopped the model attempting work that would have succeeded, sending the user hunting for permissions they already have. Rows now come fromResourceSpecificResults, one per resource, each with that resource's own decision, matched statement ids and missing context keys, sosummarycounts (action, resource) pairs again -- which is what the description and the README have promised all along. No field is added or removed. AWS's reference says the older one-result-per-resource shape carried the same aggregate decision on every result, so readingResourceSpecificResultsis right whichever shape AWS serves: one code path, no version sniffing. An action AWS does not break down per resource falls back to its own top-level result, so no action can drop out ofresultsbecause AWS sent a shape we had not seen. Three corollaries, each a wrong answer on its own: a call that names noresourcesreports resource*again rather than the ARN template (that is the commonest call shape, "can I do this at all?", and AWS applies['*']server-side);missingContextValuessurvives that call, because the API reports those keys on the TOP-LEVEL result for a*simulation and puts them per resource only when the call named resources -- only a*entry inherits them, since the top-level list is a union across resources and copying it onto a specific ARN would blame that ARN for another's keys; and a decision outside allowed / explicitDeny / implicitDeny now readsunknown, as the README always said, where before only a MISSING decision landed there and every other string, including""and a miscased"Allowed", passed through verbatim and was counted indenied.organizationsDecisionandpermissionsBoundaryDecisionare reported by AWS per ACTION, so a resource row that was allowed now reads"allowed"instead of the action's aggregate"denied"-- an SCP or boundary deny cannot coexist with an allow -- while a row that is not allowed still carries the action's value, which the description now says rather than leaving the caller to guess. Everyiam_simulatefixture the suite had was pre-change shape, which is why nothing caught this; the four new ones were transcribed from captures the real aws-cli 2.34.3 rendered from the documented XML through a loopback stub, and the 2-actions-by-3-resources call now yields 4 rows with no${in anyresource.aws_iam_simulaterejects an STS session ARN up front, with the lookup that fixes it.aws_whoamireportsarn:aws:sts::<account>:assumed-role/<role>/<session>for every SSO session, which makes it the ARN a model reaches for first, but the simulator takes a user, group or role ARN only -- so the first call from an SSO session went to IAM and came back with nothing to act on. The handler now refuses it before anything spawns and names theiam get-rolelookup for the role it parsed out. It deliberately does not rebuild the role ARN, because the session ARN drops the role's path and SSO roles live underaws-reserved/sso.amazonaws.com/. The description and the README's rows also state what the simulator cannot see: SCP statements never appear inmatchedStatementIds, keys only an SCP references are never reported missing (so a region-locking SCP needsaws:RequestedRegionpassed incontextEntriesyourself), and "allowed" is necessary but not sufficient -- RCPs, the target resource's own policy, session policies and VPC endpoint policies are not evaluated. The pagination wording now matches what the CLI does:aws iam simulate-principal-policyfollows IAM'sIsTruncated/Markeritself and prints the merged pages, sohasMoreis false andmarkernull on every first call, and only a call that resumed frommarkercan carry either. The fake was emitting a truncated FIRST page the real CLI never produces -- the same fake-vs-real gap that hid theaws_logs_tailbug -- and now models the CLI both ways. The response shape is unchanged.aws_docs_readcould not open theindex.htmlpagesaws_docs_searchhands out. docs.aws.amazon.com answers every<path>/index.htmlwith a 301 to<path>/, and the post-redirect check added in 2.0.0 re-used the input pattern, which demands a.htmlsuffix. So the canonical form of a URL the site itself returns came back as "redirected to '/', which is not an 'https://docs.aws.amazon.com/...html' page" -- an error worded like a blocked off-site redirect. In a live census of eight queries, 126 of 800 search hits wereindex.htmlURLs, among them the AWS CLI's own per-service command indexes (cli/latest/reference/s3api/index.html), which are the most relevant family of pages there is for a server that wraps the CLI. The landing URL is now judged three ways instead of one, and the 2.0.0 hardening is kept whole: it must still be https, on docs.aws.amazon.com, on the default port, with no credentials, and an off-site redirect is refused exactly as before.- A guessed page name now says it does not exist, instead of coming back as another page's content. The site does not 404 a missing page inside a guide that exists: it 302s to the guide's landing page, and a missing CLI command to that service's command index. So the redirect fix on its own would have answered a guessed command name with the whole
s3apiindex as the content of a command nobody can run -- which is what accepting every same-host redirect, the behavior an origin-only check gives you, amounts to. A PAGE request that lands on some other directory is nowok: falsewith "does not exist" and the landing URL, so a model that guessed learns that it guessed. Every shape here was re-verified against the live site on 2026-09-19, and the fixtures are trimmed captures of those responses rather than invented HTML. - A docs fetch blocked by a corporate gateway blamed AWS's backend for it. Node's fetch rejects with a bare
TypeError: fetch failedand puts the real verdict inerr.cause, which no production file here read -- so behind a TLS-inspecting gateway or a private CA (the ordinary shape of a corporate network, and these are the only two tools in this server that reach the internet without theawsCLI) both docs tools reported "fetch failed" and nothing else, andaws_docs_searchwent on to say the undocumented backend at proxy.search.docs.aws.com "may have changed or be unreachable" about a request that never left the machine. The unit test meant to cover this threwnew Error("ECONNREFUSED"), a shape fetch never produces, so the suite stayed green over it. All four fetch branches now append the cause -- read defensively, a non-empty string code only so a numeric errno cannot print as a bare "0", with anyuserinfo@redacted out of both halves -- to the LAST@of the authority and with no:required, because WHATWG userinfo runs to the final@(new URL("http://svc:Pa@ss@h:3128")gives passwordPa%40sson 22.22.2, so an unencoded@in a password is a legitimate value) and becausehttp://<token>@proxyis auth too;?and#end the authority like/does, so a docs URL whose query or fragment holds an@is untouched -- and name the remedy where the verdict has one: an untrusted certificate chain asks forNODE_EXTRA_CA_CERTSorNODE_USE_SYSTEM_CA=1(Node 22.19+) and says both are read at process start, so they belong in the MCP config'senvblock rather than your shell; an expired certificate or a hostname mismatch deliberately gets no CA advice, because a CA fixes neither;HTTPS_PROXYset withoutNODE_USE_ENV_PROXY=1(Node 22.21+) gets the fact that Node's fetch ignores it, which is why theawsCLI -- which readsHTTPS_PROXYitself -- can keep working while these two tools cannot, and that hint also reaches the 30s timeout branch, because on a proxy-only network the likelier symptom is nothing answering at all; and an uncoded transport failure under oam, which reports no code for one, gets the certificate remedy plus a note that an unreachable proxy looks identical from there. Every shape was reproduced locally on 2026-09-19 rather than read out of documentation: a self-signed HTTPS server on a loopback port (Node 22.22.2 givesDEPTH_ZERO_SELF_SIGNED_CERT, oam 0.16.2 gives no code at all, andNODE_EXTRA_CA_CERTSfixes both), a dead port and an unresolvable host, andHTTPS_PROXYpointed at a dead port with the opt-in present, absent, set to0, and viaNODE_OPTIONS. aws_callno longer blames you for the params you did supply.aws_callwithservice: 's3api', operation: 'get-object', params: {Bucket, Key}exited 252 withthe following arguments are required: --bucket, --key-- naming the two values that were inparams-- and no suggestion at all, so a model reads it as its own mistake and retries with the same params. Same forbedrock-runtime invoke-model,bedrock-agentcore invoke-agent-runtime,lambda invoke,logs tail,s3 cp/ls/syncandconfigure get. The cause is not the parameters: the AWS CLI never registers--cli-input-jsonon an operation whose argument table carries anoutfile(awscli/customizations/cliinput.py...
v2.3.3
Security
- A Cloud Control identifier, token or pagination cursor, or an
aws_logs_tailfilter pattern, that starts withfile://orfileb://no longer makes the AWS CLI read a local file and send it to AWS. The CLI's paramfile loader (awscli/paramfile.py) replaces any command-line parameter value that begins with exactlyfile://orfileb://with the contents of that local file -- after expanding~,$VARand%VAR%-- before the request is signed, and v2 stores itsno_paramfilesetting without ever reading it, so there is no per-parameter opt-out.aws_resource_*'sidentifier,requestToken,clientTokenandnextToken,aws_paginate'sstartingTokenandaws_logs_tail'sfilterPatternreached the command line unguarded, on tools annotated read-only that hosts commonly auto-approve: a prompt-injectedidentifieroffile://~/.aws/credentialssent that file to AWS as the identifier, and thefileb://form echoed the file's bytes back to the model in the error. The Cloud Control and pagination fields now reject the prefix before any process starts, in the same class as the existing leading--reject and witherrorKindabsent the same way. As a backstop,runAwsCallrefuses any such value among the CLI-level arguments a tool hands it (errorKind: "bad_input", naming the flag it belongs to) -- that is what closesfilterPattern, which has no field-level reject of its own, and it also covers the values that come back from AWS, such as the request tokenawaitCompletionpolls with. The match is exactly the loader's own case-sensitive, whitespace-freestartswith, so nothing that reached AWS as itself before is refused now:FILE://, a leading space andfile:/with one slash all still pass through, verified against aws-cli 2.34.3 and 2.22.0 through a loopback stub.aws_lambda_invoke's own--payload fileb://<temp file>is exempted by its exact value, so--qualifierand the rest of that call stay guarded. - On Windows, an
aws.exein the server's working directory no longer stands in for the AWS CLI. Node resolves the bare commandawsby looking in the current directory beforePATHunlessNoDefaultCurrentDirectoryInExePathis set. Claude Code sets it; hosts on the MCP SDK's default stdio transport pass only a short list of variables that does not include it, and start the server in their own working directory -- often the open project. So a repository with anaws.exeat its root ran that binary, with your AWS environment, on the first tool call: reproduced over MCP stdio on Node 22.22.2 against a build of this release with the fix removed, where a copy ofnode.exeplanted asaws.exeansweredaws_call sts get-caller-identityand the stub endpoint saw no requests at all. oam 0.16.2 never searched the working directory either way. The server now sets the variable for itself at startup when the host has not, as the first statement of the entry point, before any tool can spawn anything; an existing value is left alone whatever case the host spelled the key in. Windows evaluates the setting in the parent at spawn time, so theawschildren are covered too, and the CLI inherits it, which also stops the CLI's own helpers resolving there: a plantedsession-manager-plugin.exeotherwise ran underaws ssm start-sessionand was handed the session's token. Acredential_processconfigured as a bare program name that exists only in the working directory stops resolving; name it by absolute path.
Fixed
aws_lambda_invokecould run a function three times and still return nothing. A function slower than 60 s hit the AWS CLI's default socket read timeout, and the CLI's standard retry mode then sentInvokeagain -- three attempts by default, five for a profile withmax_attempts = 5. The tool's own description told callers to raisetimeoutMsfor a long function, which is exactly what set it off: through the v2.3.2 handler, aws-cli 2.34.3 against a local stub, a 70 s function withtimeoutMs: 200000received threeInvokePOSTs 60 s apart and the call came back after 183 s asnonzero_exitRead timeout on endpoint URL, with no payload and no log tail. The invoke now runs withAWS_MAX_ATTEMPTS=1, which overrides anymax_attemptsin the caller's profile -- every spelling of the variable is dropped from the child environment first, because Node hands a child the upper-case key while oam hands it the last one -- and with a--cli-read-timeoutderived fromtimeoutMs, so the request is sent once and the CLI waits as long as the caller asked.timeoutMsnow means how long to wait for the function: the CLI gets 10 s more, enough for a cold start's init phase, so a function that hits its own timeout still comes back as afunctionErrorwith its log tail, and the tool's own kill timer fires 5 s after the CLI's as a backstop. Values above 900000, Lambda's synchronous maximum, are treated as 900000, which also stops a hugetimeoutMstiming the call out at once -- Node fires a timer longer than 2^31-1 ms after 1 ms. No answer in time iserrorKind: "timeout"whichever clock fired -- the CLI's read timeout used to surface asnonzero_exit-- and the message says whether the invoke was sent. That distinction is not cosmetic:--cli-read-timeoutalso bounds the CLI's credential calls, and a hung STS endpoint for arole_arnprofile produces the identicalRead timeouttext with nothing sent to Lambda, so the handler reads the URL in the message rather than assuming the function ran.- Error codes and remedies survive the CLI giving up on its retries. Once botocore stops retrying it marks the error
MaxAttemptsReachedand writes... when calling the X operation (reached max retries: N): ..., and the parser requiredoperation:straight after the operation name, so every retry-exhausted error lost its code, its operation and its suggestion -- throttling and 5xx on every tool, which is exactly the case the backoff suggestion was written for. It matters more now thataws_lambda_invokeruns withAWS_MAX_ATTEMPTS=1: botocore marks the FIRST failure the same way whenmax_attemptsis 1, so the CLI writes(reached max retries: 0)on every Lambda service error, and a missing function would have lost "Verify the resource identifier and region." On Windows the same pattern also swallowed the CLI'sAdditional error details:block, because the terminator looked for\n\nand the CLI writes\r\n\r\n. Both are fixed by the one regex, and the throttling suggestion now says how many times the CLI had already retried -- nothing is appended when it had not. parseAwsErrorhas a remedy for three transport failures that had none:Read timeout on endpoint URLandConnection was closed before we received a valid response(the request was sent, so a call that changes state may have taken effect -- check before retrying), andConnect timeout on endpoint URL(check network access, proxy and region). The remedies are deliberately generic about retry safety -- every tool butaws_lambda_invokereaches them with the CLI's retries still on -- and the request they say was sent is the one to the endpoint the message names, which for arole_arnprofile whose STS endpoint hangs is the credential call rather than the caller's operation.aws_lambda_invokeaccepts the qualifier$LATEST.PUBLISHEDand function ARNs up to 256 characters. Both limits predate Lambda Managed Instances, and both rejected valid input here before anything spawned:FunctionNamenow allows the 256 characters the Invoke API documents (it was 170, which a 64-character name with a 59-character alias already exceeds), and the qualifier charset gains.for$LATEST.PUBLISHED-- sent as?Qualifier=%24LATEST.PUBLISHED, verified through the installed CLI.:and/stay out, so a qualifier still cannot spellfile://. The qualifier description no longer says an unqualified invoke always runs$LATEST-- on Managed Instances it runs$LATEST.PUBLISHED, and durable functions need an explicit qualifier -- the tool description says Managed Instances functions do not support the log tail, andinvocationTypeno longer offersaws_iam_simulateas a DryRun substitute: the simulator evaluates the caller's identity policies and never fetches the function's resource-based policy.- Pagination cursors are accepted up to 8,192 characters, up from 2,048. Cloud Control documents ListResources
NextTokenat up to 4,096 characters, and aws-cli returned and accepted one that long against a local stub, butaws_resource_listrejected it locally before any process spawned.aws_paginateneeds more headroom still: the CLI wraps a raw token as base64 JSON for--starting-token, so a 4,096-character cursor arrives as 5,484. It is the 128-character bug 2.0.0 fixed, one limit higher. The identifier cap (2,048) and therequestToken/clientTokencap (128) are unchanged.
Changed
aws_lambda_invokeno longer lets the AWS CLI retry a throttle, a 5xx or a dropped connection. This is a behavior change, not merely a fix. The CLI re-sent all three, and every re-send of anInvokethat reached the function is another run -- a dropped connection, a 429TooManyRequestsExceptionand a 500ServiceExceptionwere each sent three times on the CLI's defaults. They now reach the caller on the first failure. For aTooManyRequestsExceptionthis gives something up: Lambda refused before running anything, so the old silent retry was safe, but no environment variable or flag limits retries to some error codes and not others. The tool description now says a throttle is safe to retry, and that after a dropped connection or a 5xx the function may have run, so check its logs first.aws_lambda_invoke'stimeoutMsnow bounds the wait for the function, not the whole call. This is a behavior change, not merely a fix: the CLI is allowed 10 s beyond the value in force ...