Repository navigation
Releases: jaredshuai/mcp-ssh4agent
Release list
v4.0.0
[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 ...