
A multi-server SSH orchestration layer that pipes remote command execution, file transfers, and DevOps operations through MCP tools. You get 37 tools across six groups: core SSH operations (execute, deploy, tail logs), session management, health monitoring (disk, memory, process tracking), automated backups for MySQL/PostgreSQL/MongoDB with retention policies, and sudo execution. Supports SSH agent auth, passphrase-protected keys, ProxyJump/bastion chains, and runs on both Unix and Windows OpenSSH hosts. Reach for this when you want Claude to manage production servers directly instead of copying commands back and forth, or when you're automating deployments and database snapshots across a fleet. The tool activation system lets you expose only what you need to keep context usage down.
A Model Context Protocol (MCP) server that enables Claude Code and OpenAI Codex to manage multiple SSH connections. Execute commands, transfer files, manage databases, create backups, monitor health, and automate DevOps tasks across your servers — directly from your AI assistant.
🔒 Security release — three command-injection advisories fixed, one of which defeated readonly mode (Released: August 28, 2026)
Upgrade if you use ssh_backup_*, ssh_db_dump, ssh_service_status or ssh_tail — and especially if you rely on the readonly / restricted security modes.
readonly / restricted (GHSA-m793-whw6-f537) — ssh_service_status and ssh_tail are read-only, so they stay enabled on servers you locked down, and neither quoted its arguments nor consulted the policy layer. A service name like nginx; id > /tmp/pwned executed. This defeated the exact control those modes exist to provide.ssh_db_dump (GHSA-796j-h5q5-jx6p) — the stat command run after the dump interpolated the output path raw. The v3.6.7 patch had stopped one line short.ssh_backup_* tool (GHSA-qwwm-vrm9-4mw8) — backup-manager.js had zero shell escaping across its 9 builders, while database-manager.js had 95. The v3.6.7 fix was never extended to it.The quoting helper now lives in one module (src/shell-quote.js) so "did this builder quote its inputs?" has a single answer, and a new test drives 340 builder × argument × payload combinations through a real shell to prove none of them execute.
An MCP SSH server is the most dangerous tool you can hand an AI agent: a shell on machines that matter. This one is built so you decide how far the agent can go — per server, not globally.
| Mode | What the agent can do |
|---|---|
unrestricted (default) | Everything. Same behaviour as any other SSH MCP server. |
readonly | Mutating tools are refused outright — no deploy, no upload, no sudo, no database import. Read commands still work. |
restricted | Every command must match an allow pattern and no deny pattern. Anything else is refused before it reaches the host. |
SSH_SERVER_PROD_MODE=readonly
SSH_SERVER_STAGING_MODE=restricted
SSH_SERVER_STAGING_ALLOW_PATTERNS=^systemctl (status|restart) myapp$;^tail -n \d+ /var/log/
Alongside that:
ps, in /proc/<pid>/cmdline, or
in an auditd trail — unlike the echo "$pass" | sudo -S pattern common in
this category (#34).ssh_db_query refuses anything
that is not a SELECT.npm ci, and a test enforces that every dependency resolves to
registry.npmjs.org with an integrity hash and no unreviewed install scripts.~/.ssh-manager.log and stderr (which your MCP host captures), so one call site handing it a server config would have persisted a production password. Redaction now happens inside the logger. Also: CodeQL on every push, every action pinned by SHA, and a broken example that never parsed. Full changelog →echo "<password>" | sudo -S, readable in ps and /proc/<pid>/cmdline by every account on the host. It now goes over the SSH channel's stdin. Also: listed in the official MCP Registry as io.github.bvisible/mcp-ssh-manager, releases published from CI with SLSA provenance and a CycloneDX SBOM, and the per-server security modes documented at last. Full changelog →npm run test:lockfile guarding it against drift and tampering. CI installs with npm ci; ESLint and the JSDoc typecheck became blocking gates after being purely decorative. Full changelog →ssh_sync on Windows, tunnel crash fix (August 14, 2026)group field per server (#56 — contributed by @ice616, requested in #55) — tag a server with group = "production" and it is in that group: ssh_execute_group resolves members straight from your .env/TOML, union'd with any .server-groups.json you already keep. Also: ssh_sync fixed from a Windows host (#59 — contributed by @2836603852), a tunnel on a busy port no longer takes the whole MCP server down, the @modelcontextprotocol/sdk floor raised to ^1.30.0 over three advisories, and JSDoc type-checking added to CI. Full changelog →FORWARD_AGENT / forward_agent option (#53 — requested by @raphaelbahat in #52) — the equivalent of OpenSSH's ForwardAgent yes, per server: processes on the remote host authenticate to other SSH hosts with the keys in your local ssh-agent, without copying any private key. Requires a running agent and defaults to false. Full changelog →ssh_db_* argument is now shell-quoted (#51 — responsibly disclosed by Ugur Ozer, Aeon AI Risk Management (http://airiskmanagement.ca), see #48) — caller-controlled values (ssh_db_list most notably, which stayed allowed in readonly/restricted modes) were interpolated into shell-evaluated strings, allowing arbitrary command execution on the SSH target. A centralized shellQuote() now wraps every value across all 15 builders, guarded by a 648-combination injection test. Full changelog →SUDO_PASSWORD / DEFAULT_DIR / ssh_sync key auth work again (July 11, 2026)ssh_execute_sudo ignored SUDO_PASSWORD, DEFAULT_DIR was ignored by ssh_execute/ssh_group_execute/ssh_list_servers, and ssh_sync never passed the configured SSH key to rsync. All aligned with the loader's camelCase fields, with a regression test locking the loader output shape. Full changelog →ssh_db_query shell-injection security fix + real row_count (June 30, 2026)$(…) inside queries (which corrupted backtick identifiers and let the "SELECT-only" tool run arbitrary shell commands), and row_count now reflects each engine's real output instead of counting wrapper lines. Full changelog →knip.json plus a blocking knip CI step keep unused code from creeping back. Full changelog →ssh_sync reports the real transfer count (June 18, 2026)--stats parsing: --stats is always passed now, and rsync 2.x/3.x wording, openrsync's B suffix, and locale separators are all handled. Full changelog →description strings changed. Full changelog →tunnel-manager.js and session-manager.js registered module-level setIntervals that were never unref()'d, so importing either module kept Node's event loop alive. Both are now unref()'d. Full changelog →.env/TOML and the running MCP server picks it up on the next call, no restart. A ServerConfigManager reloads lazily on file-signature change (path + mtime + size); a failed reload keeps the last known-good config; real process.env vars keep top priority. No watcher, no polling.SIGINT handler every session leaked a ~83 MB node process. Shutdown is now idempotent across SIGINT/SIGTERM/SIGHUP/stdin-close, timers are unref()'d, and the process exits ~10 ms after teardown instead of never. Full changelog →Dead (#39 — thanks @username77) — the liveness probe ran echo "ping" and cmd.exe echoed the quotes literally, failing a strict === 'ping' check and needlessly rebuilding live connections. Now uses echo ping parsed by a null-safe isPingAlive(stdout) helper (CRLF/quote/case-normalized), covered by tests/test-ssh-ping.js. Full changelog →readonly / restricted + audit log (May 18, 2026)A second authorization layer that filters tool invocations inside the MCP server, complementing the existing client-side autoApprove. Useful when sharing the MCP with a third-party agent, a CI bot, or any client where ssh_execute shouldn't be unconditionally trusted.
MODE field = identical to v3.4.x):
unrestricted (default) — strict no-op. evaluatePolicy() early-returns on the first line, zero overhead.readonly — blocks mutating tools (ssh_upload, ssh_deploy, ssh_sync, ssh_execute_sudo, ssh_backup_*, ssh_db_import/dump, plus action-gated ssh_key_manage accept|remove, ssh_alert_setup set, ssh_process_manager kill) AND applies a built-in denylist on ssh_execute (rm, mv, dd, mkfs, chmod, chown, sudo, systemctl restart/stop, docker rm/stop, pipe-to-sh, redirect outside /tmp, curl|sh, etc.).restricted — every command must match at least one ALLOW_PATTERNS regex AND no DENY_PATTERNS regex. DENY wins. With no ALLOW_PATTERNS everything is refused (fail-closed).SSH_SERVER_<N>_AUDIT_LOG=/path/to/audit.jsonl). Records ts, server, tool, args, allowed, reason on denial, exitCode/success on execution. Sensitive arg fields (password, passphrase, sudoPassword, token, secret, apikey) are replaced with ***.DENY pattern can't be bypassed via an alias..env or TOML loads identically. No MODE field → zero behavior change. The interactive wizard (ssh-manager server add) defaults all three new prompts to skip. All 13 pre-existing tests pass unmodified. New tests/test-policy.js adds 26 tests covering modes, DENY > ALLOW precedence, invalid-regex handling, redaction, and the backward-compat fast path. Full reference →curve25519-sha256 (+@libssh.org), diffie-hellman-group15-sha512, diffie-hellman-group16-sha512rsa-sha2-512, rsa-sha2-256 (RFC 8332)aes128-gcm@openssh.com, aes256-gcm@openssh.comhmac-sha2-256-etm@openssh.com, hmac-sha2-512-etm@openssh.com, hmac-sha1-etm@openssh.comSet-Location replacing cd && (#31, thanks @WenKingSu)ECHO: 0 PTY, real $? exit codes, no more "Timeout waiting for shell prompt" on custom/slow/AIX shells (#30, thanks @MakksSh)ssh_execute timeout silently capped at 30 s — fixed (#28, #29)/bin/bash shim error — fixed (#22, #23)server add blocked by missing rsync — rsync now optional (#26).env path resolution now uses a fallback chain instead of hardcoded __dirname — works correctly with npm install -g (#16, #19)
~/.ssh-manager/.env → cwd/.env → ~/.env → project .env~/.ssh-manager/.env on first ssh-manager server addssh-manager CLI registered as binary: npm install -g now creates both mcp-ssh-manager and ssh-manager commands (#18)PROXYJUMP config field (#15)
npx mcp-ssh-manager now works correctly (#14)ssh-agent when SSH_AUTH_SOCK is available — passphrase-protected keys work transparentlypassphrase field for both .env and TOML formatsThanks to @snjax for the original contribution (#12).
platform config field (SSH_SERVER_FOO_PLATFORM=windows or platform = "windows" in TOML)platform=windows, the Linux timeout/sh -c command wrapper is skipped and the SSH library's native timeout is used insteadssh_execute, ssh_tail, ssh_monitor, ssh_deploy, ssh_execute_sudo, ssh_group_execute) are platform-awareprocess.env.HOME is undefined (#8)os.homedir() for cross-platform compatibility (Linux, macOS, Windows)ssh-manager tools list/configure/enable/disableThis release adds 12 new MCP tools transforming SSH Manager into a comprehensive DevOps automation platform:
📊 Total: 37 MCP Tools | 🔧 ~4,100 Lines of Code Added | ✅ Production Ready
.env file setup with guided configuration toolNEW in v3.1: Reduce Claude Code context usage by 92% with tool activation management!
MCP SSH Manager includes 37 tools organized into 6 groups. By default, all tools are enabled, but you can optimize for your specific workflow:
# Interactive configuration wizard
ssh-manager tools configure
# View current configuration
ssh-manager tools list
# Enable/disable specific groups
ssh-manager tools enable monitoring
ssh-manager tools disable backup
| Mode | Tools | Context Usage | Best For |
|---|---|---|---|
| All (default) | 37 tools | ~43.5k tokens | Full feature set, most users |
| Minimal | 5 tools | ~3.5k tokens | Basic SSH operations only |
| Custom | 5-37 tools | Varies | Tailored to your workflow |
📖 Complete Tool Management Guide →
brew install hudochenkov/sshpass/sshpassapt-get install sshpassOption A: Install from npm (recommended)
# Install globally from npm
npm install -g mcp-ssh-manager
# Or install locally
npx mcp-ssh-manager
Option B: Install from source
# Clone and install
git clone https://github.com/bvisible/mcp-ssh-manager.git
cd mcp-ssh-manager
npm install
# Install the Bash CLI
cd cli && ./install.sh
# Configure your first server
ssh-manager server add
# For personal use (current user only)
claude mcp add ssh-manager node /path/to/mcp-ssh-manager/src/index.js
# For team sharing (creates .mcp.json in project)
claude mcp add ssh-manager --scope project node /path/to/mcp-ssh-manager/src/index.js
# For all your projects
claude mcp add ssh-manager --scope user node /path/to/mcp-ssh-manager/src/index.js
To avoid being prompted for approval on every SSH command, add auto-approve configuration:
Edit ~/.config/claude-code/claude_code_config.json:
{
"mcpServers": {
"ssh-manager": {
"command": "node",
"args": ["/path/to/mcp-ssh-manager/src/index.js"],
"autoApprove": [
"mcp__ssh-manager__ssh_execute",
"mcp__ssh-manager__ssh_list_servers",
"mcp__ssh-manager__ssh_upload",
"mcp__ssh-manager__ssh_download",
"mcp__ssh-manager__ssh_sync",
"mcp__ssh-manager__ssh_alias"
]
}
}
}
Important: Restart Claude Code after making this change.
For full auto-approval of all SSH tools, see the complete list in examples/claude-code-config.example.json.
autoApprove is all-or-nothing per tool: once ssh_execute is approved, anything goes. If you want a second layer that filters what the MCP server actually accepts to run — useful when sharing the MCP with a third-party agent, a CI bot, or a client's server — declare a per-server security mode.
# In your .env — three optional fields. Omit them all to keep v3.4.x behavior exactly.
SSH_SERVER_CLIENT_PROD_HOST=client-prod.example.com
SSH_SERVER_CLIENT_PROD_USER=consultant
SSH_SERVER_CLIENT_PROD_KEYPATH=~/.ssh/consultant_ed25519
SSH_SERVER_CLIENT_PROD_MODE=readonly # unrestricted | readonly | restricted
SSH_SERVER_CLIENT_PROD_AUDIT_LOG=~/.ssh-manager/audit.jsonl # opt-in JSONL audit trail
# For mode=restricted, provide an allowlist of regex (DENY wins over ALLOW):
# SSH_SERVER_CI_ALLOW_PATTERNS="^docker (ps|logs);^kubectl get "
unrestricted (default, no field needed) — identical to pre-v3.5.0 behavior. Zero overhead.readonly — blocks ssh_upload, ssh_deploy, ssh_sync, ssh_execute_sudo, backup/db write tools, and built-in destructive commands (rm, mv, sudo, systemctl restart, redirects outside /tmp, curl | sh, …).restricted — every ssh_execute command must match at least one ALLOW_PATTERNS regex AND no DENY_PATTERNS regex.Existing configs are unaffected — no field is mandatory, no behavior changes unless you opt in. See docs/SECURITY_MODES.md for the full reference, recipes, and limitations.
In Claude Code, you can now:
"List all my SSH servers"
"Execute 'ls -la' on production server" # Uses default directory if set
"Run 'docker ps' on staging"
"Upload config.json to production:/etc/app/config.json"
"Download logs from staging:/var/log/app.log"
With Default Directories:
If you set /var/www/html as default for production, these commands are equivalent:
"Run 'ls' on production" → executes in /var/www/html"Run 'ls' on production in /tmp" → executes in /tmp (overrides default)Same installation as Claude Code (see above), then configure for Codex:
# Set up Codex integration
ssh-manager codex setup
# Migrate existing servers to TOML format (if you have .env servers)
ssh-manager codex migrate
# Test the integration
ssh-manager codex test
If you prefer manual setup, add to ~/.codex/config.toml:
[mcp_servers.ssh-manager]
command = "node"
args = ["/absolute/path/to/mcp-ssh-manager/src/index.js"]
env = { SSH_CONFIG_PATH = "/Users/you/.codex/ssh-config.toml" }
startup_timeout_ms = 20000
Create or edit ~/.codex/ssh-config.toml:
[ssh_servers.production]
host = "prod.example.com"
user = "admin"
password = "secure_password" # or use key_path
key_path = "~/.ssh/id_rsa" # for SSH key auth (recommended)
passphrase = "key_passphrase" # optional, for passphrase-protected keys
port = 22
default_dir = "/var/www"
group = "production" # optional, free-form label for grouping/import-export
description = "Production server"
[ssh_servers.staging]
host = "staging.example.com"
user = "deploy"
key_path = "~/.ssh/staging_key"
port = 2222
default_dir = "/home/deploy/app"
[ssh_servers.winhost]
host = "192.168.1.90"
user = "svc-ssh"
key_path = "~/.ssh/winhost_key"
port = 2222
platform = "windows"
description = "Windows host via OpenSSH"
[ssh_servers.bastion]
host = "bastion.example.com"
user = "jumpuser"
key_path = "~/.ssh/bastion_key"
[ssh_servers.internal]
host = "10.0.0.5"
user = "admin"
key_path = "~/.ssh/internal_key"
proxy_jump = "bastion"
description = "Private server behind bastion"
💡 See examples/codex-ssh-config.example.toml for more complete examples!
In OpenAI Codex, you can now:
"List my SSH servers"
"Execute 'docker ps' on production"
"Upload file.txt to staging:/tmp/"
"Monitor CPU usage on all servers"
"Download production:/var/log/app.log to ./logs/"
Switch easily between Claude Code (.env) and Codex (TOML):
# Convert .env to TOML (for Codex)
ssh-manager codex convert to-toml
# Convert TOML back to .env (for Claude Code)
ssh-manager codex convert to-env
Both formats can coexist! The system supports both simultaneously.
ssh_list_serversLists all configured SSH servers with their details.
ssh_executeExecute commands on remote servers.
server (name), command, cwd (optional working directory)cwd is provided, uses the server's default directory if configuredssh_uploadUpload files to remote servers.
server, local_path, remote_pathssh_downloadDownload files from remote servers.
server, remote_path, local_pathssh_backup_createCreate backup of database or files on remote server.
server, type, name, database, paths, retentionssh_backup_listList all available backups on remote server.
server, type (optional filter)ssh_backup_restoreRestore from a previous backup.
server, backupId, database, targetPathssh_backup_scheduleSchedule automatic backups using cron.
server, schedule (cron format), type, namessh_health_checkPerform comprehensive health check on remote server.
ssh_service_statusCheck status of services (nginx, mysql, docker, etc.).
server, services (array)ssh_process_managerList, monitor, or kill processes on remote server.
ssh_alert_setupConfigure health monitoring alerts and thresholds.
ssh_db_dumpCreate database dump/backup on remote server.
server, type, database, outputFile, dbUser, dbPassword, dbHost, dbPortcompress (gzip), tables (specific tables only)ssh_db_importImport SQL dump or restore database on remote server.
server, type, database, inputFile, dbUser, dbPassword, dbHost, dbPortdrop (drop database before restore for MongoDB)ssh_db_listList databases or tables on remote server.
server, type, database (optional), dbUser, dbPassword, dbHost, dbPortssh_db_queryExecute read-only SQL queries on remote database.
server, type, database, query, dbUser, dbPassword, dbHost, dbPortcollection parameter for find queriesssh_deploy 🚀Deploy files with automatic permission and backup handling.
server, files (array), options (owner, permissions, backup, restart)ssh_execute_sudo 🔐Execute commands with sudo privileges.
server, command, password (optional), cwd (optional)ssh_alias 🏷️Manage server aliases for easier access.
action (add/remove/list), alias, serverssh_command_alias 📝Manage command aliases for frequently used commands.
action (add/remove/list/suggest), alias, commandssh_hooks 🎣Manage automation hooks for SSH operations.
action (list/enable/disable/status), hookssh_profile 📚Manage configuration profiles for different project types.
action (list/switch/current), profileSSH Manager uses profiles to configure aliases and hooks for different project types:
Set active profile:
export SSH_MANAGER_PROFILE=frappe.ssh-manager-profile with profile namedefault profile if not specifiedAvailable profiles:
default - Basic SSH operationsfrappe - Frappe/ERPNext specificdocker - Docker container managementnodejs - Node.js applicationsprofiles/ directoryServers are configured in the .env file with this pattern:
# Server configuration pattern
SSH_SERVER_[NAME]_HOST=hostname_or_ip
SSH_SERVER_[NAME]_USER=username
SSH_SERVER_[NAME]_PASSWORD=password # For password auth
SSH_SERVER_[NAME]_KEYPATH=~/.ssh/key # For SSH key auth
SSH_SERVER_[NAME]_PASSPHRASE=key_passphrase # Optional, for passphrase-protected keys
SSH_SERVER_[NAME]_PORT=22 # Optional, defaults to 22
SSH_SERVER_[NAME]_DEFAULT_DIR=/path/to/dir # Optional, default working directory
SSH_SERVER_[NAME]_DESCRIPTION=Description # Optional
SSH_SERVER_[NAME]_GROUP=production # Optional, free-form label for grouping/import-export
SSH_SERVER_[NAME]_PLATFORM=windows # Optional: "linux" (default) or "windows"
SSH_SERVER_[NAME]_PROXYJUMP=bastion # Optional: name of another server to use as jump host
SSH_SERVER_[NAME]_PROXYCOMMAND=command # Optional: custom proxy command (ncat, ssh -W, etc.)
SSH_SERVER_[NAME]_FORWARD_AGENT=true # Optional: forward local ssh-agent to remote (needs SSH_AUTH_SOCK; security risk — see SSH Agent section)
# Example: Linux server
SSH_SERVER_PRODUCTION_HOST=prod.example.com
SSH_SERVER_PRODUCTION_USER=admin
SSH_SERVER_PRODUCTION_PASSWORD=secure_password
SSH_SERVER_PRODUCTION_PORT=22
SSH_SERVER_PRODUCTION_DEFAULT_DIR=/var/www/html
SSH_SERVER_PRODUCTION_DESCRIPTION=Production Server
SSH_SERVER_PRODUCTION_SUDO_PASSWORD=secure_sudo_pass # Optional, for automated deployments
# Example: Windows server (OpenSSH for Windows)
SSH_SERVER_WINHOST_HOST=192.168.1.90
SSH_SERVER_WINHOST_USER=svc-ssh
SSH_SERVER_WINHOST_KEYPATH=~/.ssh/winhost_key
SSH_SERVER_WINHOST_PORT=2222
SSH_SERVER_WINHOST_PLATFORM=windows
SSH_SERVER_WINHOST_DESCRIPTION=Windows host via OpenSSH
# Example: Server behind a bastion/jump host
SSH_SERVER_BASTION_HOST=bastion.example.com
SSH_SERVER_BASTION_USER=jumpuser
SSH_SERVER_BASTION_KEYPATH=~/.ssh/bastion_key
SSH_SERVER_INTERNAL_HOST=10.0.0.5
SSH_SERVER_INTERNAL_USER=admin
SSH_SERVER_INTERNAL_KEYPATH=~/.ssh/internal_key
SSH_SERVER_INTERNAL_PROXYJUMP=bastion
SSH_SERVER_INTERNAL_DESCRIPTION=Private server behind bastion
The Python management tool (tools/server_manager.py) provides:
mcp-ssh-manager/
├── src/
│ ├── index.js # Main MCP server (37 tools)
│ ├── ssh-manager.js # SSH connection handling
│ ├── config-loader.js # .env & TOML config loading
│ ├── session-manager.js # Persistent SSH sessions
│ ├── backup-manager.js # Backup & restore
│ ├── health-monitor.js # Health checks & alerts
│ ├── database-manager.js # Database operations
│ ├── tunnel-manager.js # SSH tunnel management
│ ├── server-groups.js # Group operations
│ └── ...
├── cli/
│ ├── ssh-manager # Bash CLI entrypoint
│ ├── commands/ # CLI command modules
│ └── lib/ # CLI libraries
├── profiles/ # Configuration profiles (frappe, docker, nodejs...)
├── examples/ # Example configs
├── docs/ # Documentation
└── package.json
python tools/test-connection.py production
claude mcp list
/mcp
.env files - Always use .env.example as templateMCP SSH Manager supports passphrase-protected SSH keys in two ways:
Option 1: SSH Agent (recommended)
If your SSH key is loaded into ssh-agent, MCP SSH Manager will use it automatically — no configuration changes needed:
# Add your key to the agent (enter passphrase once)
ssh-add ~/.ssh/your_key
# Verify the key is loaded
ssh-add -l
The server detects the SSH_AUTH_SOCK environment variable and connects to the running agent. This is the same mechanism that regular ssh uses for GUI passphrase prompts.
Option 2: Passphrase in configuration
You can store the passphrase directly in the server config:
.env format:
SSH_SERVER_MYSERVER_KEYPATH=~/.ssh/id_rsa
SSH_SERVER_MYSERVER_PASSPHRASE="your_passphrase"
TOML format:
[ssh_servers.myserver]
key_path = "~/.ssh/id_rsa"
passphrase = "your_passphrase"
Note: SSH Agent is preferred over storing passphrases in config files for better security.
Enable per-server agent forwarding (the equivalent of OpenSSH's ForwardAgent yes) so processes on the remote host can authenticate to other SSH hosts using the keys in your local ssh-agent — e.g. git clone over SSH on a remote server using your local GitHub key, without copying any private key to the server.
It is opt-in per server and defaults to false. It requires a running local agent (SSH_AUTH_SOCK present); when the agent is unavailable the flag is simply ignored.
.env format:
SSH_SERVER_MYSERVER_FORWARD_AGENT=true
TOML format:
[ssh_servers.myserver]
forward_agent = true
⚠️ Security warning: agent forwarding lets any process that can read the forwarded agent socket on the remote host — including anyone with root there — use your loaded keys to impersonate you against other hosts for the life of the connection. Only enable it for servers you trust, mirroring the same caution
ssh_config(5)advises forForwardAgent.
Connect to servers behind a bastion or jump host. The connection is tunneled through the jump server transparently — all tools (execute, upload, download, sync) work as usual.
# Define the bastion server
SSH_SERVER_BASTION_HOST=bastion.example.com
SSH_SERVER_BASTION_USER=jumpuser
SSH_SERVER_BASTION_KEYPATH=~/.ssh/bastion_key
# Point the target server to the bastion
SSH_SERVER_PRIVATE_HOST=10.0.0.5
SSH_SERVER_PRIVATE_USER=admin
SSH_SERVER_PRIVATE_PROXYJUMP=bastion
Or in TOML:
[ssh_servers.bastion]
host = "bastion.example.com"
user = "jumpuser"
key_path = "~/.ssh/bastion_key"
[ssh_servers.private]
host = "10.0.0.5"
user = "admin"
proxy_jump = "bastion"
Chained jumps are supported: if bastion itself has a proxy_jump, the chain is followed recursively. Circular references are detected and rejected.
Connect through SOCKS5 proxies or custom proxy commands. The proxy command executes locally and forwards traffic to the remote host.
# SOCKS5 proxy via ncat
SSH_SERVER_SOCKS_HOST=target.example.com
SSH_SERVER_SOCKS_USER=admin
SSH_SERVER_SOCKS_PROXYCOMMAND="ncat --proxy 127.0.0.1:1080 --proxy-type socks5 %h %p"
# Windows SSH proxy command
SSH_SERVER_WINPROXY_HOST=internal.example.com
SSH_SERVER_WINPROXY_USER=admin
SSH_SERVER_WINPROXY_PROXYCOMMAND="C:\Windows\System32\OpenSSH\ssh.exe -W %h:%p user@jump-host"
Or in TOML:
[ssh_servers.socks]
host = "target.example.com"
user = "admin"
proxy_command = "ncat --proxy 127.0.0.1:1080 --proxy-type socks5 %h %p"
[ssh_servers.winproxy]
host = "internal.example.com"
user = "admin"
proxy_command = "C:\\Windows\\System32\\OpenSSH\\ssh.exe -W %h:%p user@jump-host"
The proxy command must be a valid command that reads from stdin and writes to stdout, accepting %h and %p placeholders for host and port.
Tag a server with group and it becomes part of that group — no extra file to maintain. The label is free-form and travels with the server definition, so it survives an export to (or import from) another tool.
SSH_SERVER_WEB1_HOST=10.0.0.1
SSH_SERVER_WEB1_USER=deploy
SSH_SERVER_WEB1_GROUP=production
SSH_SERVER_WEB2_HOST=10.0.0.2
SSH_SERVER_WEB2_USER=deploy
SSH_SERVER_WEB2_GROUP=production
Or in TOML:
[ssh_servers.web1]
host = "10.0.0.1"
user = "deploy"
group = "production"
Both servers are then reachable as a group:
Run "uptime" on the production group → ssh_execute_group
List my server groups → ssh_group_manage (action: list)
ssh_list_servers also reports the group of each server, so you can see membership without opening the config.
How it combines with ssh_group_manage: groups you create with ssh_group_manage live in .server-groups.json and carry execution settings (strategy, delay, stop-on-error). Groups implied by the group field carry membership only. When a name exists on both sides, membership is the union — the stored list plus every server tagged with that name — and the stored execution settings apply. Group names are case-insensitive.
A group that exists only through the group field is read-only for ssh_group_manage: to change who belongs to it, edit the servers' group in your .env/TOML. Creating a group of the same name with ssh_group_manage is still allowed and simply adds stored members and settings on top.
Symptoms:
Solution: v3.1.1 includes automatic fixes:
Performance Tuning (add to .env):
# Reduce output size (default: 10000 characters)
MCP_SSH_MAX_OUTPUT_LENGTH=5000
# Increase timeout for slow commands (default: 120000ms)
MCP_SSH_DEFAULT_TIMEOUT=180000
# Use compact JSON to save tokens (default: false)
MCP_SSH_COMPACT_JSON=true
For large outputs:
# Instead of: cat huge-log.txt
# Use: tail -n 100 huge-log.txt
# Or: grep ERROR huge-log.txt | tail -n 50
See docs/TROUBLESHOOTING.md for complete guide.
claude mcp listssh-manager server test [server_name]chmod 600 ~/.ssh/your_key"Backup production MySQL database before deployment"
"List all backups on production server"
"Restore backup from yesterday"
"Schedule daily database backup at 2 AM"
"Backup website files excluding cache and logs"
For detailed backup examples, see examples/backup-workflow.md and docs/BACKUP_GUIDE.md.
# Basic server management
ssh-manager server list
ssh-manager server add
ssh-manager ssh prod1
# File synchronization
ssh-manager sync push prod1 ./app /var/www/
ssh-manager sync pull prod1 /var/log/app.log ./
# SSH tunnels
ssh-manager tunnel create prod1 local 3307:localhost:3306
ssh-manager tunnel list
# Execute commands
ssh-manager exec prod1 "docker ps"
Once installed, simply ask your AI assistant:
Claude Code examples:
OpenAI Codex examples:
Both AI assistants support the same MCP tools! 🚀
We welcome contributions! Please see CONTRIBUTING.md for details.
./scripts/setup-hooks.sh
This project uses automated quality checks:
Run validation manually: ./scripts/validate.sh
This project is licensed under the MIT License - see the LICENSE file for details.
timeout wrapper is used for reliable command terminationPLATFORM=windows in your server config to skip the Linux timeout/sh -c wrapper (which is incompatible with Windows OpenSSH)sshpass to be installedlocal:C:\project or local:.\project; ssh_sync converts drive-letter and UNC paths to MSYS2 format before launching rsync. Prefer native paths, since Node checks them on disk using Windows path semantics — a path already written as /c/... or //server/share is passed through to rsync untouched rather than converted twice.ssh_connection_status tool with reconnect actionFor issues, questions, or suggestions: