Repository navigation
[4.0.0] - 2026-09-04
Changed
-
BREAKING — full rebrand from
ssh-manager/mcp-ssh-managertossh4agent/mcp-ssh4agent. Bin commands are nowssh4agentandmcp-ssh4agent; the MCP server registers asssh4agent(re-add it in your agent — auto-approval patterns becomemcp__ssh4agent__*); the config directory is~/.ssh4agent(existing~/.ssh-managerkeeps loading until the new directory exists);SSH_MANAGER_*env vars are nowSSH4AGENT_*; the log file is.ssh4agent.log. Remote-side paths changed with no migration (the package had no published release): alert configs live at/etc/ssh4agent-alerts.json, backups under/var/backups/ssh4agent, scheduled scripts at/usr/local/bin/ssh4agent-backup-*— re-runssh_alert_setupandssh_backup_scheduleon hosts configured with earlier code. -
BREAKING (contributors) — the codebase is now TypeScript, run natively by Node's type stripping. All
src/,cli/,scripts/,debug/modules are.tswith explicit.tsimport extensions (erasable syntax only — no enums/namespaces/parameter properties); there is no tsx and no build step:node src/index.tsis the whole dev runtime, andnode --check/npm run typecheckgate it in CI. The dev tree needs Node ≥ 23.6 (type stripping). Exception for publishing only: the npm artifact is compiled to JS underdist/byprepack(Node refuses type stripping undernode_modules), so consumers need Node ≥ 20. -
BREAKING (contributors) — Lint/format tooling is Biome, replacing ESLint + Prettier.
biome checkgates pre-commit and CI;npm run lint:fixis the one-command fixer. -
The tool registration funnel owns Policy, Audit, and the response envelope. Every tool handler now declares its policy at registration (
gate: 'server'by default, withcommandArg/serverFrom/whenvariants, and explicitgate: 'exempt' | 'manual'for the two deliberate exemptions) instead of hand-weavingapplyServerPolicy + auditOkin 15 places: a newly registered mutating tool can no longer skip the per-server policy, and audit entries are written on both the success and failure paths — failure is derived from theisErrorflag or a thrown error, fixing 29 catch blocks that omittedisErrorand were audited as successes. Tool modules receive their infrastructure via aToolContext(ADR-0002) and never import the entry point. -
Tunnels own their dedicated connection; shutdown closes tunnels first. A tunnel no longer borrows a pooled connection it cannot account for — it holds its own, disposes it on close, and
ssh_tunnel_createnow refusesproxyJump/proxyCommandexplicitly (it silently ignored them before) (ADR-0003). -
One tool-enablement owner for CLI and server.
src/tool-config-manager.ts(logger-free, pure fs + registry data) is the single source for tool groups, modes, and per-tool overrides; the CLI's drifted third copy of the list and its four observable behaviour gaps (mode transitions silently enabling all 37 tools, reset resurrecting legacy configs, dead per-tool overrides underall, hardcoded export-claude list) are gone (ADR-0004). -
One config-reading path for CLI and MCP entry point.
src/env-path.tsis the single.envfallback chain (SSH_ENV_PATH→SSH4AGENT_ENV(deprecated alias) →~/.ssh4agent/.env→ legacy →$PWD/.env→~/.env→ package root → default); server reads go through the sharedparseEnvServersTextparser; the CLI's five copies of server→ssh-argument resolution collapsed intoresolveServerToSshArgs(). The two processes can no longer disagree about which file holds the servers. -
Centralized shell quoting and dump/import command builders. Every password/path interpolated into a remote command goes through
src/shell-quote.ts; the three drifted dump implementations (one of them unquoted — password injection) merged intosrc/dump-command-builder.ts.
Fixed
-
Failed dump producers can no longer masquerade as successful backups —
mysqldump … | gzip > outreported the pipe's last exit code, so a wrong password, missing database, or full disk left an empty/partial archive marked as success. Dumps are now two-step (dump to a temp file gated by&&, then compress, with a cleanup arm removing both half-products) — pure POSIX, valid under any remote login shell including BusyBox ash. Verified against real MySQL 8 / PostgreSQL 16 (Alpine) containers. -
Corrupt archives can no longer masquerade as successful imports — the mirror bug:
gunzip -c X | mysqlexited 0 after consuming a truncated stream. Same two-step fix; the plain-inputcat X | mysqlfallback (which reported success for a missing file) became a direct< Xredirection that fails in the shell. Verified against real MySQL / PostgreSQL containers: a truncated.gzfails before the client ever runs, leaving zero partial data. -
MongoDB backup archive paths no longer drift across consumers — the dump produced
<id>.tar.gzwhile the size check, reported location, and restore all looked for<id>.gz. The path now has one source (mongoArchivePath/getBackupArchivePath), the restore verifies the archive exists before touching the target, and pre-existing.tar.gzbackups that were never referenced became restorable.ssh_db_dumpreports the actual archive path for mongodb. -
All user state now lives under
~/.ssh4agent/— the active-profile pointer (.ssh4agent-profile, plus the pre-rebrand.ssh-manager-profile) was the last state file written to the install directory, which is read-only under a global npm install; every state file now routes throughsrc/state-files.tswith one-time best-effort migration from the old location and 0600/0700 permission tightening on every read/write. -
Aliases can no longer bypass per-server policy — server resolution went through two paths and only one applied the security modes; there is now a single
resolveServer()path for everyone. -
Audit entries no longer leak database credentials and failure reporting no longer marks errored calls as successes.
-
Connection pool hardening — in-flight connects are shared instead of racing, failed setups dispose the SSH client before rethrowing, ProxyJump dialing is alias-aware, connect timeouts are wired, and shutdown no longer races live remote forwards.
-
ssh4agent codex setupemits a working entry path for installed users (source vs installed package environments), and the documented-but-missingcodex,monitor, andsessionCLI commands are implemented.
Added
-
Domain documentation for agents:
CONTEXT.mdglossary +docs/adr/decision records (ToolContext injection, tunnel connection ownership, tool-config single owner, module-split decision foradvanced.ts). -
Interface tests without a network: connection pool, remote tunnels, policy funnel, config unification, tool-config unification, state files, and manager interfaces (session/backup/health) — all driven through fakes at the calling-convention level, where the real bugs used to hide.
[3.8.0] - 2026-08-14
Security
@modelcontextprotocol/sdkfloor raised to^1.30.0— the declared range (^1.17.2) still allowed versions carrying three published advisories, one of them high: cross-client data leak through shared server/transport instance reuse, DNS rebinding protection off by default, and a ReDoS. It also dragged in vulnerable transitive express packages (body-parser,path-to-regexp,qs,ajv).npm audit --omit=devnow reports 0 vulnerabilities. Behaviour verified against the real MCP stdio protocol: identical to 1.17.5 (37 tools registered, same negotiation, clean shutdown).
Added
-
Optional
groupfield on server config (#56 — contributed by @ice616, requested in #55)-
A free-form label (
SSH_SERVER_<NAME>_GROUPin.env,group = "..."in TOML) carried on the server entry itself, so a server's group membership can be read straight off its config when exporting to — or importing from — another tool, with no second file to ship. It round-trips throughexportToToml/exportToEnvand is returned byssh_list_servers. -
The label is not just descriptive: it makes the group usable.
ssh_execute_groupandssh_group_manage listnow resolve members from thegroupfield as well, so tagging servers is enough to run a command across them —.server-groups.jsonis no longer required to have a working group. -
Membership is the union of both sources: the explicit list stored by
ssh_group_manage(which keeps carrying strategy, delay and stop-on-error) plus every server tagged with that name. Group names are case-insensitive. Config-derived members are resolved at read time and never written to.server-groups.json, so they cannot go stale. -
A group that exists only through the config is read-only for
ssh_group_manage; attempting to edit it now returns an error pointing at the server config instead of a bare "not found". -
The
ssh-manageradd-server wizard asks for an optional group.
-
-
Tests: new
tests/test-server-groups.js(14 checks) covers config-derived groups, the union with stored groups, case-insensitivity, read-only enforcement, persistence isolation, the dynamic-group reload regression and group execution;tests/test-config-field-names.jsgainsgroupcoverage across the.envloader, the TOML loader, the forbidden-field guard and the export round-trip. -
Static type-checking over the existing JavaScript (
npm run typecheck) — atsconfig.jsonincheckJs/noEmitmode runs TypeScript as a linter over JSDoc annotations. Nothing is compiled and nothing changes for users: the package still ships plain JS straight fromsrc/, with no build step and nodist/. Added as a hard gate in the Code Quality workflow.- A
ServerConfigtypedef insrc/config-loader.jsdocuments the resolved server shape and makes its camelCase contract enforceable: readingserverConfig.default_dir— the exact issue bvisible#49 regression — is now a type error ("Property 'default_dir' does not exist on type 'ServerConfig'. Did you mean 'defaultDir'?") instead of a silentundefined.tests/test-config-field-names.jskeeps guarding the same contract at runtime.
- A
Fixed
-
A tunnel on an already-used local port crashed the entire MCP server —
net.Server#listennever passes an error to its callback (a failed bind is emitted as an'error'event), yetssh_tunnel_createusedlisten(port, host, (err) => ...)and registered no'error'listener. OnEADDRINUSE— the everyday tunnel failure — Node rethrew the unhandled event and killed the process, taking every pooled SSH connection and session with it, while the awaited promise never settled. Binding now rejects properly and the tool returns an error. Guarded bytests/test-tunnel-listen.js. -
ssh_syncalways passed an explicit-p 22to rsync — the guard readserverConfig.port !== '22', comparing a number against a string, so it never matched. Harmless in effect, but the intent (omit the flag on the default port) never applied. Found by the new typecheck, along with a matchingparseInt(serverConfig.port || '22')inssh_known_hoststhat only worked becauseparseIntstringifies its argument first. -
ssh_synctreated native Windows local paths as remote rsync specifications — a path such asC:\projectwas passed unchanged to MSYS2 rsync, which parsedC:as a remote host and attempted an SSH connection toc. Windows drive-letter, rooted, UNC, and extended-length paths are now resolved and converted only for the spawned rsync argument while Node keeps the native path for local filesystem checks and logging; relative paths keep their existing semantics with normalized separators. Trailing separators retain rsync's directory-content semantics. A path already written in MSYS2 form (/c/project,//server/share) — the shape Windows users adopted to work around this very bug — is passed through untouched instead of being mounted twice into/c/c/project. Guarded bytests/test-rsync-path.js(24 checks: conversion table, pass-through, invariants over every input, platform isolation, failure modes, and a static guard on thessh_syncwiring) plus a real MSYS2 rsync dry run. -
The MCP server announced itself as version
1.2.0— theserverInfoversion sent to Claude Code / Codex was a hardcoded literal the release process never bumped, so it had drifted far behind the real package version (3.7.0). It is now derived frompackage.json, the same treatment the CLI banner got in 3.7.0. This matters more under the MCP2026-07-28spec, where servers identify themselves in the_metaof every result. -
exportToEnvno longer truncates agroupcontaining#— the value is now quoted likeDESCRIPTION, so a label such asprod #eu westsurvives an export/re-import instead of coming back asprod. -
The dynamic
allgroup now sees TOML-defined servers — it enumeratedprocess.envonly, sossh_execute_group allsilently skipped every server declared in a TOML config. The group layer now reads the configuration actually loaded byConfigLoader. -
The
allgroup no longer disappears after the first group edit — dynamic groups are deliberately never persisted, so every.server-groups.jsonwritten byssh_group_managelackedall, and the next start loaded that file as the complete set:ssh_execute_group allthen failed withGroup 'all' not found. Dynamic groups are now re-injected when the file is read.
Changed
- Dropped the
uuiddependency in favour of Node's built-incrypto.randomUUID()— available since Node 14.17, and this package already requires Node >= 18. Removes one runtime dependency and the last remaining advisory. Session and tunnel ID formats are unchanged.
[3.7.0] - 2026-07-13
Added
-
Per-server SSH agent forwarding (
ForwardAgent) (#53 — requested by @raphaelbahat in #52)-
New opt-in option,
SSH_SERVER_<NAME>_FORWARD_AGENT=true(.env) orforward_agent = true(TOML), enabling the equivalent of OpenSSH'sForwardAgent yeson a per-server basis. Processes on the remote host can then authenticate to other SSH hosts using the keys held in the operator's localssh-agent(e.g.gitover SSH), without copying any private key to the server. -
Requires a running local agent (
SSH_AUTH_SOCK); the flag is ignored when no agent is present.ForwardAgentis set only alongside a valid agent, sincessh2throws otherwise. Becausessh2's connection-levelagentForwardforwards on every exec/shell channel, no per-command change is needed. -
A
parseBool()helper coerces the.envstring value soFORWARD_AGENT=falseis not treated as truthy; onlytrue/1/yes/onenable it. -
Defaults to
false. Documented in the README (SSH Agent Forwarding) and CLAUDE.md with the standard security warning (a root user on the remote host can use the forwarded agent for the life of the connection).
-
-
Tests:
tests/test-agent-forward.jsverifies theconnect()wiring across the on/off/absent/no-agent states;tests/test-config-field-names.jsgainsforwardAgentcoverage including.envboolean coercion and a TOML export round-trip.
[3.6.7] - 2026-07-11
Security
-
Command injection in the database command builders (#51 — responsibly disclosed by Ugur Ozer, Aeon AI Risk Management (http://airiskmanagement.ca), see #48)
-
Every
ssh_db_*tool argument —database,table,collection, output/input file paths, and theuser/host/port/passwordconnection fields — arrives from tool-call parameters and was interpolated straight into a shell-evaluated command string insrc/database-manager.js. A crafted value ($(…), backticks,; cmd,| cmd,&& cmd, bare> file) executed arbitrary commands on the configured SSH target, under the account the MCP server uses. -
The strongest path is
ssh_db_list: it is classified read-only, so it stayed allowed in thereadonlyandrestrictedper-server security modes while still reaching an injectable builder — bypassing the boundary those modes promise. -
The v3.6.5 heredoc fix (bvisible#44) only hardened
ssh_db_query's query text; thelist/dump/import/restorebuilders and the query builders' connection flags were untouched. -
Fix: a centralized
shellQuote()(single-quote wrap +'\''escaping) now wraps every interpolated caller value across all 15 builders. The MySQL list-tablesdatabasebecame a shell-quoted positional argument instead of an interpolatedUSE <db>SQL clause, closing the SQL path too. Shell-quoting is transparent for legitimate values.
-
Added
-
Security regression test
tests/test-database-injection.js(npm run test:dbinjection, part ofnpm test): drives every builder × every caller-controlled parameter × an injection-payload battery (648 combinations) through a real/bin/shwith fake DB client binaries and asserts no payload ever executes; unit-testsshellQuote(); verifies benign values stay correctly quoted. -
SECURITY.mddocumenting the private vulnerability disclosure process (GitHub private vulnerability reporting + security email; resolves #48).
[3.6.6] - 2026-07-11
Fixed
-
Handlers read stale snake_case config fields that
ConfigLoaderno longer produces (#49 — thanks @egoan82 for the excellent root-cause report; fixed in #50)-
Since the v3.0.0 ConfigLoader refactor, resolved server configs expose camelCase fields (
sudoPassword,defaultDir,keyPath), but several handlers still read the pre-3.0 snake_case/lowercase names — alwaysundefined, so their fallback branches silently never ran:-
ssh_execute_sudoignored the configuredSUDO_PASSWORD→sudo: a terminal is required to authenticateon hosts without a TTY on the exec channel -
ssh_execute/ssh_group_execute/ssh_execute_sudoignoredDEFAULT_DIR— commands silently ran in$HOME -
ssh_list_serversalways reported an emptydefaultDir -
ssh_syncnever passed the configured SSH key to rsync (-i/BatchMode=yes), falling back to whatever default key the agent offered
-
-
No configuration change required:
.env/TOML source keys (SUDO_PASSWORD,default_dir,key_path, …) are unchanged — existing configs simply start working again.
-
Added
- Regression test
tests/test-config-field-names.js(npm run test:fieldnames, part ofnpm test): locks the ConfigLoader's output field names from both.envand TOML sources, and statically forbids stale.sudo_password/.default_dir/.keypathaccess insrc/.
[3.6.5] - 2026-06-30
Security
-
ssh_db_querycould execute arbitrary shell commands on the remote host (#44 — thanks @technophile77)-
The query builders interpolated the raw query into a double-quoted shell string (
mysql -e "${query}",psql -c "${query}",mongo --eval "${query}"), so the remote shell evaluated backticks`…`and$(…)/$VARbefore the database engine ever saw the query. The "SELECT-only" tool could therefore run shell commands on the target server (e.g.SELECT `id`/SELECT '$(id)'). -
The query is now delivered to
mysql/psql/mongoon stdin via a single-quoted heredoc (<<'__MCP_SQL_EOF__'), so the remote shell never parses it — only the database engine interprets the SQL/JS. A sharedbuildHeredoc()helper keeps the existingawkJSON-wrapper pipe on the heredoc opening line (MySQL JSON output is byte-for-byte unchanged) and defensively rejects a delimiter collision.isSafeQuery()is intentionally left as-is, since the heredoc — not keyword filtering — closes the injection path.
-
Fixed
-
ssh_db_query(MySQL) silently corrupted backtick-quoted identifiers (#44 — thanks @technophile77)- The same shell-evaluation bug meant any query using backtick identifiers — hyphenated database names, cross-database joins, reserved-word columns — was rewritten by the shell and returned empty/incorrect results with no error. The stdin heredoc fix above restores these queries verbatim.
-
ssh_db_queryreportedrow_countoff by one (#45 — thanks @technophile77)- The handler reported
output.split('\n').length, which counted cosmetic lines (the leading[of the MySQL JSON wrapper, psql header/separator lines) rather than result rows: 1 row →2, 13 rows →14, 0 rows →2. A newcountQueryRows()derives the count from each engine's actual output structure — MySQL JSON{"row":N,…}entries, MySQL--batchlines, psql's authoritative(N rows)footer (with a header/separator fallback), and MongoDBprintjsondocuments — so empty output is0and the count tracks real rows.
- The handler reported
Tests
-
New
tests/test-database-query-quoting.js(test:dbquoting) — verifies the SQL/JS query text is carried verbatim on stdin for all three builders (never via-e "/-c "/--eval "), that theawkpipe stays on the heredoc opening line with the terminator alone, thatbuildHeredocrejects a delimiter collision, and a stochastic round-trip of 500 random shell-metacharacter queries. -
New
tests/test-database-row-count.js(test:dbrowcount) — covers the exact issue cases (MySQL 0/1/13 rows), a look-alike{"row":value inside a cell, MySQL text format, psql(N rows)/(1 row)/(0 rows)footers and the no-footer fallback, MongoDB single/multi-document counting, and empty/whitespace →0.
[3.6.4] - 2026-06-18
Changed
- Internal dead-code cleanup — no behavioral change. Removed 27 unused exports and 2 duplicate exports across the codebase (−343 lines): fully-unused helper functions deleted, internally-used symbols un-exported, and redundant
export defaults removed. The public surface (the MCP server + CLI) is unchanged — command builders and parsers are byte-identical before/after (verified by a differential test ofmainvs the change), and all 37 tools were exercised end-to-end against a live SSH server (Ubuntu + MariaDB) with no regression.
Tooling
- Dead-code quality gate. Added a calibrated
knip.jsonand a blockingknipstep to thequalityCI workflow, so unused exports/files/dependencies now fail the build and can't creep back in.eslint src/is clean (0 errors, 0 warnings).
[3.6.3] - 2026-06-18
Fixed
-
ssh_syncfalsely reported "No files needed to be transferred" even when files were synced (#42 — thanks @MakksSh)-
Two bugs in the rsync
--statsscraper keptfilesTransferredat0:--statswas only passed whenverbosewas enabled (so rsync produced no statistics lines at all), and the count regex only matched rsync 2.x wording (Number of files transferred) while rsync 3.x — standard on modern distros — emitsNumber of regular files transferred. Agents would mistrust the result, run redundant checks, or retry the sync. -
--statsis now always passed, and the count regex matches both rsync 2.x and 3.x.
-
-
totalSizestayed0on macOS, and locale-formatted numbers were mis-parsed (follow-up hardening)-
openrsync (the default
/usr/bin/rsyncon recent macOS) suffixes byte counts withB, notbytes, so the size regex never matched. Both suffixes are now accepted — openrsync keeps raw byte counts even for large files (verified at 5 MB), so noK/M/Ghandling is needed. -
Numbers grouped with locale-specific separators (
,or.for thousands, the opposite char for the decimal) are now parsed correctly for the file count, transferred size, and speed, instead of only stripping commas. -
The
--statsparsing was extracted intosrc/rsync-stats.jsso it is unit-testable without booting the MCP server.
-
Tests
- New
tests/test-sync-stats.js(wired intonpm testastest:sync) — 46 assertions covering openrsync output captured from a live macOS run, GNU rsync 3.x/2.x, US/EU locale number formats, the zero-files case, and a missing--statsblock.
[3.6.2] - 2026-06-09
Changed
-
All 37 tool descriptions rewritten for clarity and agent reliability. Every MCP tool now documents its real behavior — side effects, destructive vs read-only nature, idempotency, sudo/auth requirements, security-mode gating, and parameter semantics (mutually-exclusive params, defaults, edge cases) — instead of a 4-to-10-word summary. Average description length went from ~6 words to ~70, with no weak outliers (shortest is now 55 words).
-
Improves real tool-calling: agents now know the consequences before invoking a tool — e.g. that
ssh_db_importandssh_backup_restoreare destructive (DROP/overwrite), thatssh_db_queryis strictly SELECT-only, thatssh_upload/ssh_sync/ssh_deployare blocked onreadonlyservers, and which parameters are mutually exclusive. -
Also raises the Glama "Tool Definition Quality" score (previously C — average 3.1/5, lowest 2.1/5).
-
No behavioral change: only
descriptionstrings were edited; all logic, parameters, and tool names are unchanged. All test suites pass unmodified.
-
[3.6.1] - 2026-06-09
Fixed
-
Module-level monitor/cleanup intervals pinned the Node event loop (follow-up to #41)
-
tunnel-manager.js(monitorTunnels, every 30 s) andsession-manager.js(inactive-session cleanup, every 5 min) registered module-levelsetIntervaltimers that were neverunref()'d, so importing either module kept the event loop alive on its own. -
Both are now
unref()'d, matching the keepalive and cleanup timers fixed in bvisible#41. The forced-exit added in bvisible#41 already made these harmless for the orphan-process bug; this restores proper teardown hygiene so process lifetime tracks the stdio transport, not a background timer. -
Verified: before, importing either module never exits on its own; after, both exit naturally. Full
npm test(includingtest:lifecycle) stays green.
-
[3.6.0] - 2026-06-09
Added
-
Live configuration hot reload (#40 — thanks @EnjoySR)
-
The MCP server now reloads its server configuration when the
.envor TOML file changes on disk, so tools likessh_list_serverssee newly added or edited servers without restarting the MCP process. -
A new
ServerConfigManagertracks a lightweight signature (path +mtime+ size) of both config files and reloads lazily — only when that signature changes, on the next config access. No file watcher, no polling thread, no background timer. -
Fail-safe: if a reload throws (e.g. a malformed file caught mid-edit), the last known-good configuration is kept in memory and the error is logged — the server never ends up with an empty or broken config.
-
.envparsing no longer permanently mutatesprocess.env(dotenvis now read withprocessEnv: {}), and real process environment variables keep the highest priority over file values, matching the documented loading order.
-
Fixed
-
stdio server orphaned on session teardown — one leaked node process per session (#41 — thanks @LegendaryGatz)
-
A stdio MCP server is torn down by its host closing stdin (EOF) and/or sending SIGTERM — not
SIGINT, which only arrives on an interactive Ctrl-C. The only shutdown handler wasSIGINT, so on normal teardown the process was never signalled, got reparented to init, and leaked (~83 MB plus live keepalive timers each, accumulating one process per session). -
Shutdown is now an idempotent
shutdown(reason)registered onSIGINT,SIGTERM,SIGHUPand stdin'end'/'close'. The per-connection keepalive interval and the 10-minute cleanup interval areunref()'d so process lifetime tracks the transport, not a timer.ssh.dispose()is wrapped in try/catch so one bad connection can't block the rest, and a shortunref'd timer forces exit so teardown can never hang if the host has already stopped reading. -
Measured: exits cleanly ~10 ms after stdin EOF, versus staying alive indefinitely before.
-
Tests
-
New
tests/test-lifecycle.js(wired intonpm testastest:lifecycle) — black-box teardown coverage asserting a clean exit on stdin EOF, SIGTERM, SIGINT and overlapping signals. Regression guard for the orphan-process bug. -
tests/test-server-config-manager.jsextended to 9 checks — laziness (no reload while files are unchanged, exactly one reload after a change), reload-failure-keeps-last-valid-config plus recovery, and deleted-file robustness, in addition to the original TOML/.envhot-reload paths.
Backward compatibility
-
Zero impact on existing setups. Hot reload is transparent — if your config files don't change, behavior is identical to v3.5.x. No new config field, no new prompt, no new file is ever created.
-
The lifecycle fix is pure teardown hygiene: no change to any tool's behavior or output.
-
All pre-existing test suites pass unmodified.
[3.5.1] - 2026-05-26
Fixed
-
Healthy Windows/OpenSSH sessions wrongly reported as
Dead(#39 — thanks @username77)-
ssh_connection_statusranecho "ping"and compared the output with a strict=== 'ping'. Oncmd.exethe surrounding quotes are echoed literally (output"ping"), so the check failed and a live pooled connection was torn down and rebuilt for nothing. -
The probe now runs
echo ping(no quotes), which emits a barepingconsistently across bash,cmd.exeand PowerShell — fixing the root cause rather than only the symptom. -
Output parsing moved to a new exported helper
isPingAlive(stdout)that normalizes CRLF, stray quotes/backslashes and case before matching, and is null/undefined-safe.includes('ping')(rather than strict equality) is intentional: a liveness probe should err toward "alive" — a false positive just lets the next real command reconnect, whereas a false negative needlessly drops a healthy connection. -
New test suite
tests/test-ssh-ping.js(wired intonpm testastest:ping) covering the quoted/escaped/CRLF/case variants and the null/undefined paths. Validated against a real Windows/OpenSSH host.
-
[3.5.0] - 2026-05-18
Added
-
Per-server security modes — opt-in second authorization layer that filters tool invocations and commands inside the MCP server, complementing the existing client-side
autoApprovemechanism. Useful when sharing the MCP with a third-party agent, a CI bot, or any context where you can't fully trust the client. See docs/SECURITY_MODES.md.-
New per-server fields (env:
SSH_SERVER_<N>_MODE,SSH_SERVER_<N>_ALLOW_PATTERNS,SSH_SERVER_<N>_DENY_PATTERNS,SSH_SERVER_<N>_AUDIT_LOG— TOML:mode,allow_patterns,deny_patterns,audit_log). -
Three modes:
unrestricted(default — identical to v3.4.x),readonly(blocks mutating tools + built-in destructive command denylist),restricted(require regex allowlist + optional denylist, DENY wins over ALLOW). -
Gated tools:
ssh_execute,ssh_execute_sudo,ssh_execute_group,ssh_session_send(command-aware),ssh_upload,ssh_sync,ssh_deploy,ssh_backup_create,ssh_backup_restore,ssh_backup_schedule,ssh_db_import,ssh_db_dump(tool-level), plus action-gatedssh_key_manage(accept/remove),ssh_alert_setup(set),ssh_process_manager(kill). -
Command aliases (
ssh_command_alias) are expanded before policy evaluation, so the denylist can't be bypassed via an alias.
-
-
Audit log — opt-in JSONL trail per server (set
SSH_SERVER_<N>_AUDIT_LOGto a file path). Recordsts,server,tool, sanitizedargs,allowed,reason(on denial),exitCode/success/error(on execution). Credentials (password,passphrase,sudoPassword,token,secret,apikey) are redacted to***even if a tool passes them through args. No log rotation built-in — uselogrotate. -
Interactive wizard prompts (
ssh-manager server add) for the three new fields, each with default = skip. Pressing Enter on all three produces an.envblock identical to v3.4.x output. -
New test suite
tests/test-policy.js— 26 tests covering the three modes, DENY > ALLOW precedence, invalid regex handling, secret redaction, fail-closed for unknown modes, backward-compat fast path.
Backward compatibility
-
Zero impact on existing configs. A v3.4.x
.envor TOML file loads identically — noMODEfield meansunrestricted, andevaluatePolicy()early-returns{ allowed: true }without compiling a single regex. -
No new field is mandatory.
-
The audit log file is never created unless
AUDIT_LOGis explicitly set. -
autoApproveon the Claude Code side keeps working unchanged — the policy intercepts after the client approves, before the SSH command runs, so the client never sees a new prompt. -
All pre-existing tests (
test-profiles,test-command-aliases,test-hooks,test-tool-registry— 13 tests) pass without modification.
[3.4.1] - 2026-05-16
Fixed
-
SSH handshake failing against OpenSSH 9.x servers (Debian 12 / Ubuntu 24.04) (#32 — thanks @YoungHong1992)
-
The hardcoded algorithm list in
src/ssh-manager.jswas missing modern algorithms required by OpenSSH 9.x. Thessh2lib would fail the key-exchange phase against stock Debian 12 / Ubuntu 24.04 servers because no common KEX algorithm could be negotiated. -
KEX — added
curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group15-sha512,diffie-hellman-group16-sha512.curve25519-sha256is now OpenSSH's preferred default since 6.5. -
Server host key — added
rsa-sha2-512andrsa-sha2-256(RFC 8332). OpenSSH 8.2+ deprecates the SHA-1-basedssh-rsasignature algorithm in the default offer; without SHA-2 variants, RSA host keys could no longer be verified. -
Cipher — added
aes128-gcm@openssh.comandaes256-gcm@openssh.comat the head of the list (preferred GCM variants on modern OpenSSH; the plainaes*-gcmnames were already present but are not what OpenSSH advertises). -
HMAC — added
hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com. Encrypt-then-MAC is both faster and cryptographically stronger than encrypt-and-MAC; OpenSSH prefers ETM variants when both peers support them. -
Fully backward-compatible — every legacy algorithm previously in the list (
diffie-hellman-group14-sha1,ssh-rsa, CBC ciphers, plainhmac-sha*) is preserved at lower preference. Connections to older servers (CentOS 7, Debian 10, AIX, network gear with legacy SSH stacks) continue to work unchanged.
-
[3.4.0] - 2026-05-07
Added
-
Full Windows OpenSSH command execution (#31 — thanks @WenKingSu)
-
When a server is configured with
platform = "windows", commands are now wrapped aspowershell -NoProfile -OutputFormat Text -EncodedCommand <utf16le-base64>. This is the same approach used by Ansible, Chef, and Puppet for Windows remote execution. It sidesteps everycmd.exequoting and OEM code page issue (CP950, CP932, CP1252…) that was causing mojibake on non-ASCII output. -
Prepends
$ProgressPreference='SilentlyContinue'(suppresses CLIXML progress sentinels in stderr) and[Console]::OutputEncoding=UTF8(forces clean UTF-8 stdout). -
Working-directory prefix uses
Set-Location '${escapedDir}'; ${cmd}(PowerShell-native, with'→''escaping) instead ofcd ${dir} && ${cmd}which is invalid incmd.exe. Applied consistently acrossssh_execute,ssh_execute_group, andssh_execute_sudo. -
Strictly gated behind
platform === 'windows'— Linux/macOS targets are completely unaffected.
-
Fixed
-
ssh_session_starttiming out on real-world shells (#20, #30 — thanks @MakksSh)-
The previous shell-prompt detection used a fragile regex (
/[$#>]\s*$/) that broke on custom prompts, ANSI color codes, multiline prompts, slow shells,.bashrc/profile script noise, and any non-standard prompt symbol. Sessions would frequently fail to initialize withTimeout waiting for shell prompt. -
Replaced with a marker-based protocol: the PTY is requested with
ECHO: 0, a unique UUID v4 readiness marker is sent on session start, and every executed command is wrapped withset +e; <cmd>; __mcp_status=$?; printf '\n<marker>:%s\n' "$__mcp_status". The completion marker carries the real exit code captured from$?instead of the previous!output.includes('command not found')heuristic. -
A shell prompt is presentation, not a protocol boundary. Marker-based sync is shell-agnostic, deterministic, and requires zero per-server configuration.
-
Bonus: sessions now report accurate
success/exitCodefor downstream consumers.
-
[3.3.0] - 2026-05-02
Added
-
ProxyCommand support for SOCKS5 and custom proxy connections (#24)
-
New
SSH_SERVER_<NAME>_PROXYCOMMANDenv var (andproxy_commandin TOML) to specify a custom command (e.g.ncat --proxy 127.0.0.1:1080 --proxy-type socks5 %h %p,ssh -W %h:%p bastion). -
Useful for reaching servers behind SOCKS5 proxies, custom jump hosts, or any scenario the existing
ProxyJumpdoesn't cover.
-
Fixed
-
ssh_executetimeout silently capped at 30 s when the requested timeout was below 300 000 ms (#28, #29 — thanks @LukasOrcik for the precise root-cause analysis and @MakksSh for the patch)-
In
execCommandWithTimeout, the wrappedtimeout NNN sh -c …path forwardedotherOptionstossh.execCommandwithout atimeoutkey, so the underlyingSSHManager.execCommandfell back to its hardcoded 30 000 ms default. The local stream was aborted at 30 s while the remotetimeoutwrapper was still running. -
Now passes
timeout: timeoutMs + 5000so the inner timer always exceeds the requested timeout. The +5000 grace lets the remotetimeoutbinary return exit code 124/143 first, surfacing the nicerCommand timeout after Nmsmessage instead of a stream abort.
-
-
Windows global install fails with
/bin/bashshim error (#22, #23 — confirmed by @Eleef on Win11 / PS7)-
npm's PowerShell shim refused to launch the legacy bash entry point because Windows has no
/bin/bash.exe. -
New cross-platform
cli/ssh-manager.jsNode wrapper is now thebin.ssh-managerentry point. On Windows it probes Git Bash → WSL →bashon PATH (with properC:\…→/c/…and/mnt/c/…path conversion); on Unix it justspawnSyncsbash.
-
-
server addblocked at startup by missingrsync(#22 follow-up, #26)-
rsyncwas in the required dependency list, but it's only used byssh-manager sync. Git for Windows doesn't shiprsync, so the CLI was unusable on a stock Windows install for users who never callsync. -
rsyncis now optional.cmd_syncchecks lazily and emits an actionable error with install hints for macOS / Debian / Windows (MSYS2 + WSL).
-
-
server addaccepted hyphens in server names, producing entries invisible to MCP clients (#25, #27 — thanks @alexeibugrov)-
The Bash CLI used a loose regex when reading
.envand acceptedweb-server, but POSIX env-var names disallow hyphens. The MCP Node loader uses a strict/^SSH_SERVER_([A-Z0-9_]+)_HOST$/and silently dropped the entry, so Claude Code reported zero servers while the CLI listed them. -
validate_server_namenow rejects-(and any other non-[A-Za-z0-9_]) at the prompt, with a copy-paste suggestion (web-server→Try 'web_server' instead). -
server listdetects pre-existing invalid entries, marks each affected row with⚠ invalid, and prints a warning block telling the user how to migrate. -
Prompt examples updated from
web-servertoweb_serverin bothserver addand the interactive wizard.
-
[3.1.2] - 2026-02-09
Fixed
-
Windows compatibility: Replace
process.env.HOMEwithos.homedir()for cross-platform support (#8)-
process.env.HOMEis undefined on Windows, causing crash at startup -
Fixed in
src/ssh-key-manager.js,src/index.js,src/ssh-manager.js
-
[3.1.1] - 2025-11-15
🔧 Stability & Performance Release
This release fixes critical issues causing Claude Code to crash or freeze during MCP tool execution, particularly with large command outputs.
Fixed
-
Claude Code crashes: Automatic output truncation prevents context overflow
-
All stdout/stderr outputs now limited to 10,000 characters (configurable)
-
Clear truncation indicator showing how many characters were cut
-
Prevents "Interrupted: What should Claude do instead?" errors
-
-
Timeout issues: Default timeout increased from 30s to 2 minutes
-
Maximum timeout raised to 5 minutes for long-running operations
-
Prevents premature command termination
-
-
Standardized error responses: Consistent JSON error format
-
Better error handling and logging
-
Improved debugging information
-
Added
-
Performance configuration via environment variables:
-
MCP_SSH_MAX_OUTPUT_LENGTH: Control output truncation (default: 10000) -
MCP_SSH_DEFAULT_TIMEOUT: Set default command timeout (default: 120000ms) -
MCP_SSH_MAX_TIMEOUT: Set maximum timeout limit (default: 300000ms) -
MCP_SSH_COMPACT_JSON: Enable compact JSON responses (default: false) -
MCP_SSH_DEBUG: Enable debug information (default: false) -
MCP_SSH_CONNECTION_TIMEOUT: Connection idle timeout (default: 1800000ms) -
MCP_SSH_KEEPALIVE_INTERVAL: Keepalive packet interval (default: 60000ms)
-
-
New configuration module (
src/config.js):-
Centralized configuration management
-
Helper functions:
truncateOutput(),formatJSONResponse() -
Environment variable parsing with defaults
-
-
Troubleshooting documentation:
-
Complete guide in
docs/TROUBLESHOOTING.md -
Best practices for handling large outputs
-
Performance optimization tips
-
Debugging steps for common issues
-
Changed
-
Updated
.env.examplewith new performance configuration options -
Enhanced README with troubleshooting section for Claude Code crashes
-
Improved error logging with detailed context
Documentation
-
Added comprehensive troubleshooting guide
-
Updated README with performance tuning section
-
Added examples for handling large command outputs
[3.0.0] - 2025-10-01
🎉 Major Release - Enterprise DevOps Features
This major release transforms MCP SSH Manager into a comprehensive DevOps automation platform with 12 new MCP tools across three major feature areas.
Added
Phase 1: Backup & Restore System (v2.1)
-
ssh_backup_create: Create database or file backups with compression
-
Supports MySQL, PostgreSQL, MongoDB, and file system backups
-
Automatic gzip compression and metadata tracking
-
Configurable retention policies
-
Auto-creates backup directory if missing
-
-
ssh_backup_list: List all available backups with detailed metadata
-
ssh_backup_restore: Restore from previous backups with cross-database support
-
ssh_backup_schedule: Schedule automatic backups using cron
Phase 2: Health Checks & Monitoring (v2.2)
-
ssh_health_check: Comprehensive server health monitoring
-
CPU, Memory (RAM/Swap), Disk usage for all filesystems
-
Network statistics, system uptime, load average
-
Overall health status: healthy/warning/critical
-
-
ssh_service_status: Monitor services (nginx, mysql, docker, etc.)
-
Supports systemd and sysv init systems
-
Returns running/stopped status with PID
-
-
ssh_process_manager: Process management
-
List top processes sorted by CPU or memory
-
Kill processes with configurable signals
-
-
ssh_alert_setup: Configure health monitoring alerts with custom thresholds
Phase 3: Database Management (v2.3)
-
ssh_db_dump: Create database dumps (MySQL, PostgreSQL, MongoDB)
- Gzip compression and selective table backups
-
ssh_db_import: Import and restore databases
- Auto-detection of compressed files
-
ssh_db_list: List databases or tables/collections
- Filters system databases automatically
-
ssh_db_query: Execute read-only SQL queries
-
Security: Only SELECT queries allowed
-
Blocks DROP, DELETE, UPDATE, ALTER operations
-
Fixed
-
ssh_service_status: Fixed parsing bug where active services were incorrectly detected as "stopped"
- Redirected systemctl output to /dev/null for clean status detection
Improved
-
ssh_backup_create: Auto-creates backup directory with error handling
- Previously required manual creation of
/var/backups/ssh-manager
- Previously required manual creation of
Documentation
-
Added
docs/BACKUP_GUIDE.mdwith comprehensive backup strategies -
Added
examples/backup-workflow.jswith 13 real-world examples -
Updated README.md and CLAUDE.md with all new tools
Technical Details
-
New Modules: backup-manager.js (469 lines), health-monitor.js (428 lines), database-manager.js (555 lines)
-
Total Lines Added: ~4,100 lines of production code
-
Total Tools: 37 MCP tools (25 existing + 12 new)
-
Supported Databases: MySQL, PostgreSQL, MongoDB
-
Security: SQL injection prevention, read-only query enforcement
Breaking Changes
None. All existing tools remain fully compatible.
[1.3.0] - 2025-09-04
Added
-
OpenAI Codex compatibility with TOML configuration support
-
Enhanced documentation visibility for both Claude Code and Codex
-
Dual configuration format support (.env and TOML)
-
Badge system in README for platform compatibility
[1.2.0] - 2025-08-12
Added
-
ssh_deploy tool for automated file deployment with permission handling
-
ssh_execute_sudo tool for secure sudo command execution
-
ssh_alias tool for managing server aliases
-
Server alias support - use short names like "prod" instead of full server names
-
Automatic permission detection for system directories
-
Backup creation before file deployment
-
Service restart capability after deployment
-
Deployment helper functions for complex workflows
-
Comprehensive deployment guide documentation
-
Example deployment workflows
Enhanced
-
Connection resolution now supports aliases and partial matches
-
Better error messages with available servers and aliases
-
Secure sudo password handling (masked in logs)
-
Support for batch file deployments
Security
-
Sudo passwords are never logged in plain text
-
Automatic masking of sensitive information in command output
-
Secure temporary file handling during deployments
[1.1.0] - 2025-08-11
Added
-
Default directory configuration per server
-
DEFAULT_DIR field in .env configuration
-
Automatic working directory for commands
Fixed
- Syntax error in index.js (extra parenthesis)
[1.0.0] - 2025-08-10
Initial Release
-
Core SSH connection management
-
ssh_execute tool for remote command execution
-
ssh_upload tool for file uploads
-
ssh_download tool for file downloads
-
ssh_list_servers tool to list configured servers
-
Password and SSH key authentication support
-
Interactive server configuration tool
-
Connection testing utility
-
Pre-commit hooks for code quality
-
GitHub Actions workflow for CI/CD