Repository navigation
v2.1.0
This is a v2 minor release. It fixes the agent-experience issues from the
2026-09 usage audit (tracking issue #89) and changes some default
behavior and some error codes. Only some of the changes have a
compatibility switch; the lists below say which. The official exact-tag release workflow creates the stable v2.1.0
Release with make_latest=false; v2.0.2 remains GitHub latest until
published-asset canaries pass and a separate explicit latest-promotion
decision is made. Ordinary ssm update stays within v2, and once v2.1.0 is
promoted, same-major ssm update (including automatic update) adopts these
defaults on existing installs. If a fleet needs the old behavior, pin
sync_mode: strict and SSM_RUN_OUTPUT=buffered first.
Default behavior changes (read before upgrading)
- Local-first inventory (#72). Read commands (
run,map,get,put,
check,doctor,list,host list|show,host-key,keys,status)
read the local vault and no longer contact the sync server first; a sync
server outage no longer fails them. Automatic sync runs in a detached
background process, rate-limited (sync_interval, default 10m) with
exponential backoff (30s doubling to 1h).statusreports freshness
(remote_state,last_successful_sync,last_sync_error,
next_sync_attempt,cache_age_seconds,inventory_stale,
inventory_unsynced); inventory older thanstale_after(default 7d) is
reported stale.runJSON addsinventory_stale,inventory_unsynced,
inventory_sync_error. Restore the v2.0.2 behavior with
"sync_mode": "strict"insettings.jsonorSSM_SYNC_MODE=strict.
--offlineis deprecated for reads (it now only suppresses background
sync);SSM_OFFLINE=1is equivalent.loginnow fetches the inventory
immediately. Local vault mutations and every pull (explicitsync/pull,
strict-mode refresh, background) take a short vault write lock; a
concurrent writer can produce a bounded (5s) "vault is busy ... retry"
failure. - Streaming human-mode
runoutput (#71). Output is streamed as it
arrives instead of being held until the command exits (no more 8 MiB
per-stream limit); stdout is byte-exact, stderr is redacted line by line.
--jsonstill returns one complete value.SSM_RUN_OUTPUT=buffered
restores the previous buffering. Local SIGINT/SIGTERM/SIGHUP are forwarded
to the remote command (interrupted, exit 128+signal). - Connect timeout covers the SSH handshake (#73).
--connect-timeout
(default 15s) bounds TCP connect plus the SSH handshake and
authentication; legacy--timeouton run/exec/plan/map/--streamis a
deprecated alias of it (last flag wins; flags beatSSM_CONNECT_TIMEOUT/
SSM_TIMEOUT).put/get/cpkeep--timeoutas the transfer timeout.
See the lists below for the resulting error-code changes. - SSH keepalive on by default (#73).
keepalive@openssh.comevery 15s;
the connection is closed after 3 unanswered probes (connection_lost).
SSM_KEEPALIVE=0disables it;SSM_KEEPALIVE=<duration>changes it and an
invalid value falls back to 15s.
Error-code changes (affects automation that matches error)
These replace codes that were too generic (internal) or split out a specific
cause. None has a compatibility switch. Exit 255 is the transport-failure exit
(dial_*, connection_lost, handshake_failed, session_limit).
- A dropped connection after the command was sent:
connection_lost
(stageremote_execution, exit 255, additiveoutcome:"unknown") instead
ofinternal(exit 1); aput/getwhose connection breaks midway is
connection_lost(exit 255) instead ofremote_write_failed/
remote_read_failed(exit 1) (#79). - A handshake failure or stall after TCP connected:
handshake_failed(stage
handshake, exit 255) instead ofinternalordial_timeout(#73, #79). - Windows refused connection:
dial_refused(stagedial, exit 255) instead
ofinternal(exit 1) (#85). - A session-limit wait that gives up:
session_limit(stagesession, exit
255) instead ofsession_failed(#76). - New codes:
exec_timeout(exit 124),remote_shell_unsupported(exit 1),
sftp_unavailable(exit 1) andproxy_jump_invalid(exit 2);error
values that v2.0.2 never produced.
Changes without a compatibility switch
Besides the error codes above:
--connect-timeout(and requesttimeouton run requests,--timeouton
run/exec/plan/map) now also covers the SSH handshake and authentication,
not only TCP connect. Mitigation: pass a larger--connect-timeout.- CLI duration flags reject integers with trailing text (
30abc) and other
malformed values.
Changes that do have a switch: sync_mode: strict / SSM_SYNC_MODE=strict
(local-first reads), SSM_RUN_OUTPUT=buffered (streaming run output),
SSM_KEEPALIVE=0 (keepalive), --no-stdin / SSM_FORWARD_STDIN=0
(stdin forwarding stays as before by default).
New features
--exec-timeoutfor run/exec/map/--streamand request-v1
exec_timeout: SIGTERM at the deadline, 5s grace,exec_timeout, exit 124,
timed_out:true, output so far kept (#73).- Explicit stdin forwarding:
--stdin,--no-stdin,--stdin-file,
request-v1stdin_file, JSONstdin_forwarded; defaults unchanged (#77). --interpreterfor non-shell-fscripts and shebang-aware script errors (#81).- Command and flag suggestions instead of
alias_not_foundfor typos (#82). --retry-dial N[:backoff](N at most 10, exponential backoff with jitter
frombackoff, default 250ms, capped at 30s) andsshctl wait <alias>
(--timeout,--interval,--until ssh|tcp): retry only transport
failures before the host key arrived; never retry authentication or
host-key failures; one SSH connection perwaitattempt (#85).- SFTP transfers for targets without a POSIX shell (#87):
put/get --sftp,
host setting--transfer auto|shell|sftp(host.transferin request v1,
defaultautothrough the shell path) and a per-operation request
transferfield. SFTP handles single regular files only. New error codes:
remote_shell_unsupported(stagediscovery) andsftp_unavailable
(stagecapability). - ProxyJump aliases (
host ... --proxy-jump <alias>,host.proxy_jumpin
request v1; chains up to 5 hops, no cycles, otherwiseproxy_jump_invalid,
exit 2) with per-hop host-key verification and per-hop local credentials,
an optionalviafield on failures, andsshctl cp <a>:<path> <b>:<path>
(single regular file) relayed through this machine with three-way SHA-256
verification.cp --directis not implemented.
Upgrade every client before usingproxy_jump: v2.0.2 and older
ignore the field and drop it if they re-save the synced vault.
Fixes
- Security: Go 1.26.8,
golang.org/x/cryptov0.56.0, grpc v1.83.1 (#69). -h/--helpand--jsoninside remote argv are no longer captured by
sshctl;ssm pull --helpno longer performs a pull (#70, #75).- host-key: a different recorded key type is no longer reported as a MITM
mismatch;acceptno longer deletes other key types (#74). - Sync failures keep and classify their cause (
cause) without exposing the
server address (#78). - Directory transfer error handling,
--sha256digest-tool detection,
--dir-mode, get/put option parity (#80). - The connection pool keeps the shared connection when the server's session
limit is reached and waits for a free slot (#76). - Help, README and agent skill aligned with the actual flags and environment
variables, enforced by a contract test (#83).
Maintenance
- The test suite no longer requires the pinned toolchain, CI triggers were
cleaned up, and a weeklygovulncheckruns (#84, #88).
Request schema (request v1, version stays 1)
Compared with v2.0.2, every change is additive, and no request that v2.0.2
accepted is now rejected (v2.0.2 already rejected unknown fields):
- New
run/planfields:interpreter,exec_timeout,stdin_file
(stdin_fileis not valid withscript_file;interpreteronly with
script_file;preflight:trueis invalid with a non-shell interpreter). - New
put/getfields:transfer(auto|shell|sftp);dir_modeon
put.getnow also acceptssha256andtimeout(v2.0.2 rejected them). - New
hostfields:transferandproxy_jump(empty string clears it). - Run-only fields (
interpreter,stdin_file,exec_timeout) stay invalid
onput/get;transferis invalid on run, check, doctor and host ops. op:getitself is not new; it was added in v2.0.0.
There is no request-schema or JSON-field breaking change: new result fields
(inventory_*, stdin_forwarded, via, outcome, timed_out,
dial_attempts) are additive. The behavior changes are the default changes
and the error-code changes listed above; v2 compatibility behavior remains
available only through the switches named there.