mcp-ssh-manager@4.0.3
BLOCKEDPublisher claims: "MCP server for SSH: let AI agents (Claude Code, Codex, any MCP client) run commands, transfer files and manage servers over SSH, with human approval, an encrypted vault and a live audit trail." Observed in sandbox: reads credential files.
Coverage
Server started and answered tools/list.- Tested
- MCP initialize and tools/list · every tool description, for hidden instructions · every file read, file written and host contacted by what ran
- Not tested
- what the tools do when called with real credentials
Revoked: guards refuse to launch this version
confirmed incident: read ~/.ssh-manager/profile Every machine running vet-skills guard stops starting it at the next launch; the state clears when the honeypot re-run or a reviewer clears it.
Before you give it your token
- It will ask for
SSH_AUTH_SOCKConfiguration, not credentials: CODEX_HOME, GITHUB_OUTPUT, MAX_SOCKET_PATH, MCP_SSH_COMPACT_JSON, MCP_SSH_CONNECTION_TIMEOUT, MCP_SSH_DEBUG, MCP_SSH_DEFAULT_TIMEOUT, MCP_SSH_KEEPALIVE_INTERVAL, MCP_SSH_MAX_OUTPUT_LENGTH, MCP_SSH_MAX_RSYNC_OUTPUT, MCP_SSH_MAX_TAIL_LINES, MCP_SSH_MAX_TIMEOUT, npm_execpath, PREFER_TOML_CONFIG, RELEASE_REQUIRE_TAG, RELEASE_TAG, SSH_CONFIG_PATH, SSH_ENV_PATH, SSH_GROUPS_FILE, SSH_HISTORY_FILE, SSH_LOG_FILE, SSH_LOG_LEVEL, SSH_MANAGER_ANNOUNCE_AGENT, SSH_MANAGER_APPROVAL, SSH_MANAGER_APPROVAL_SOCKET, SSH_MANAGER_APPROVAL_TIMEOUT_MS, SSH_MANAGER_DEMO_DROP, SSH_MANAGER_ENV, SSH_MANAGER_HOME, SSH_MANAGER_KEY_SOURCE, SSH_MANAGER_KNOWN_HOSTS, SSH_MANAGER_LOG_OUTPUT, SSH_MANAGER_PROFILE, SSH_MANAGER_VAULT, SSH_VERBOSE- Where it talks to
- no vendor host named in the codeAlso no-color.org, openxmlformats.org — not the publisher's domain; check what for before connecting.
- What the honeypot saw
- read ~/.ssh-manager/profile · no network
- What we could not test
- what the tools do when called with real credentials
Do not connect it. The honeypot or a critical finding blocked this version; the evidence is below.
Never paste a credential into a chat, a ticket or a report, including ours. How the guard holds it.
Is [email protected] safe to install?
KillerSkills ran it in the KillerSkills honeypot sandbox (2 scenarios) on 2026-11-14 and scored it 37/100 (Blocked). Do not install it: the sandbox observed behaviour consistent with credential theft, exfiltration or persistence. Notable findings: Access to sensitive file paths; Access to sensitive file paths; Reads credential paths from the home directory. Summary: reads credential files.
Behavior trace
Honeypot run · 29 s · 2 scenariosStatic findings
keyPath: '~/.ssh/fixture-key', passphrase: 'fixture-passphrase', port: 2222, defaultDir: '~/fixture folder',Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.if [ ! -f "$HOME/mcp/mcp-ssh-manager/.env" ] && [ ! -f "$HOME/.ssh-manager/.env" ]; thenFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.label: 'OpenSSH config (~/.ssh/config)',Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* Installs three times and probes .env, TOML and process-environment setups.Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.const env = { ...(previous?.env || {}) };Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.2. `.env` file in project rootFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.'rm -rf /srv/app/releases/2026-08-21',Fix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.{ command: 'rm -rf /srv/app/releases/2026-08-21' },Fix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.// from a .env, a TOML file or the environment. An environment variable wouldFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose."description": "Path to a .env file declaring servers as SSH_SERVER_<NAME>_HOST, _USER, _KEYPATH and friends. Defaults to a .env next to the package.",Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* @property {string} [sudoPassword] Password piped to `sudo -S`.Fix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.4. **Deployment Strategy**: The deploy helper detects permission issues and automatically creates scripts for sudo execution when neededFix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.print_info "No .env file found. You'll need to configure servers after installation."Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.- `ssh_execute_sudo`: Execute commands with sudo privilegesFix: Remove the text that addresses the agent rather than the reader; describe behavior, do not instruct.<summary><b>Spending less context — tool groups</b></summary> <br> All 37 tools cost roughly **43.5k tokens** of context. Most people don't need all of them. ``Fix: Remove the text that addresses the agent rather than the reader; describe behavior, do not instruct.| Windows x64 / arm64 | `…-setup.exe` | **Not code-signed in 4.0.x.** Windows shows *"Windows protected your PC"*: *More info → Run anyway*. Check the file against the SHA-256 in `release-manifest-winFix: Pin versions and sources; vendor anything fetched at install time.* User-global only: ~/.ssh-manager/tools-config.jsonFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.// as the start of another job, so `* * * * * curl … | sh #` installed a jobFix: Build commands from fixed arguments, never from interpolated strings; prefer an API client over shelling out.(state / '.env').write_text(DEMO_ENV)Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.SSH_SERVER_[NAME]_SUDO_PASSWORD=pass # Optional for automated sudoFix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.1. **Environment Variables (.env)** - Traditional format for Claude CodeFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.if git ls-files | grep -q "^\.env$"; thenFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.# Check for .env in gitFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.const envFile = path.join(dir, '.env');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.- `ssh_execute_sudo`: Execute commands with sudo privilegesFix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.return { command: engine.command, args: engine.args, env: { ...env, ...engine.env } };Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.echo " • All configurations are saved in .env file"Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.- Manages `.env` file with server configurationsFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.sudo_password = "pass" # Optional for automated sudoFix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.echo " ✅ No .env file in git"Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.ssh-manager codex convert to-toml # Convert .env to TOMLFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.ssh-manager codex convert to-env # Convert TOML to .envFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.untouched(scenarios); ok('previous release: .env, TOML and environment each preserve all 20 effective fields and expose 37 tools');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.echo " ❌ WARNING: .env file is tracked in git!"Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.// sudo password reaches `sudo -S` without ever appearing in the remoteFix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.// write a production password to ~/.ssh-manager.log and to stderr, whereFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.…ctions. Deliberately omits secrets, so no passwords, key paths, passphrases, or sudo passwords are returned. Takes no parameters. Useful as a first call to discover …Fix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.* SSH_MANAGER_HOME (~/.ssh-manager by default), readable by their owner only.Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* clear-text .env it replaces, and it keeps the vault usable on Windows, inFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* The same .env must be used by the MCP engine, CLI and optional control plane.Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.path.join(process.cwd(), '.env'),Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.// write it in clear text in a .env or TOML file. That file sits in a projectFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* manager home, beside the .env. Up to 3.8.5 the default was inside theFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* User-global only: ~/.ssh-manager/tools-config.jsonFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.+ 'If the servers are still in a .env: delete the vault and run ssh-manager vault import\n'Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose./** @type {string|null} The .env actually read, once one has been. */Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.// Step 2: Determine if we need sudoFix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.// sees .env servers, never TOML ones — hence the provider.Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.// Keep an explicit empty value so the lower-priority .env cannot fillFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.// A step that needs the sudo password carries it as `stdin`, never insideFix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.chmod +x "$TEMP_BUILD/ssh-manager"Fix: Request the narrowest permission that works; replace wildcards with the exact paths or hosts.keyPath: '~/.ssh/id_ed25519', group: 'staging' },Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* Nobody starts here. They have a `~/.ssh/config` with forty hosts, or aFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.// Load .env file (higher priority, overwrites TOML)Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.// command line: interpolating it into `echo "..." | sudo -S` left itFix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.* such as `^rm ` missed `echo x; rm -rf /data` for the same reason. SplittingFix: Build commands from fixed arguments, never from interpolated strings; prefer an API client over shelling out.// step.stdin carries the sudo password for steps that need it, so itFix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.…xecute_sudo', { description: 'Runs a command with elevated privileges via sudo on the named server and returns the exit code and output. Prepends sudo when abs…Fix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.keyPath: '~/.ssh/id_ed25519', defaultDir: '/var/www', group: 'production',Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.// strings "true"/"1"/"yes"/"on" (case-insensitive) from .env, where every valueFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.label: 'OpenSSH config (~/.ssh/config)',Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* Which .env the engine reads, and why.Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* install directory (issue #87). They now live beside the .env, inFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.// being told they can clean up their .env.Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* 2. .env fileFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.// Execute command with sudo supportFix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.* ~/.ssh/known_hosts with a line number from an error message.Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* What is still living in a .env rather than in the vault.Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.- `ssh_process_manager`: List, monitor, or kill processesFix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.* (~/.ssh-manager/) rather than in the project directory, so it is not caughtFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.chmod +x "$TEMP_BUILD/migrate.sh"Fix: Request the narrowest permission that works; replace wildcards with the exact paths or hosts.// Method 3: Parse /etc/passwd directlyFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.// `command`. Interpolating it (`echo "<pass>" | sudo -S …`) published theFix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.chmod +x "$TEMP_BUILD/install.sh"Fix: Request the narrowest permission that works; replace wildcards with the exact paths or hosts.path.join(managerHome(), '.env'),Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.// afterwards tells sudo no more is coming; only done when a callerFix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.'Change the group of the servers themselves in your .env/TOML config, or use a different group name.'Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* or ~/.ssh-manager. Read at call time so a test or a host that sets theFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* `inherit` hands it the terminal (sudo asks for its password there)Fix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.// The .env is not touched, here or anywhere: it stays the fallback, andFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.// /etc/passwd parsing failed, try last resortFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.…ackup can be disabled per call. Options can set owner and permissions, supply a sudo password, and name a single service to restart afterward. Detects sensible owner…Fix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.const result = await this.execCommand('grep "^$USER:" /etc/passwd | cut -d: -f6', {Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* by every handler. **Field names are camelCase, always** — the `.env` keysFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.const sudoPrefix = sudoPassword ? 'sudo -S -k -p \'\' ' : 'sudo ';Fix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.keyPath: '~/.ssh/id_ed25519', defaultDir: '/var/www', group: 'production',Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.keyPath: '~/.ssh/id_ed25519', group: 'staging' },Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.subprocess.run(['ffmpeg', '-y', '-loglevel', 'error', '-i', str(src),Fix: Build commands from fixed arguments, never from interpolated strings; prefer an API client over shelling out.command: `${sudoPrefix}chown ${shellQuote(owner)} ${target}`Fix: Request the narrowest permission that works; replace wildcards with the exact paths or hosts.const kill = signal => {Fix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.fs.writeFileSync(path.join(home, 'Documents', 'upload-me.txt'), 'Temporary candidate preview upload.\n');Fix: Remove the call, or declare the host in frontmatter and explain in the README what is sent and why.const kill = setTimeout(() => child.kill(), 1000);Fix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.if kill -0 $PID 2>/dev/null; thenFix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.return `kill -${signal} ${pid}`;Fix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.… In follow mode, output is streamed to stderr.\nTo stop tailing, you'll need to kill the session.` } ] }; } else { // Non…Fix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.else npx --no-install asar list "$ASAR" 2>/dev/nullFix: Pin versions and sources; vendor anything fetched at install time.Inlined: the browser requests a favicon without the token, which the control plane refuses. A data URI costs no request at all.Fix: Keep SKILL.md to visible, descriptive text. Remove comments and encoded text that address the agent.// Try multiple ways to kill the streamFix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.result = subprocess.run(['script', '-q', '/dev/null', 'bash', str(ROOT / 'cli' / 'ssh-manager')],Fix: Build commands from fixed arguments, never from interpolated strings; prefer an API client over shelling out.subprocess.run(['ffmpeg', '-y', '-loglevel', 'error', '-framerate', str(fps),Fix: Build commands from fixed arguments, never from interpolated strings; prefer an API client over shelling out.# shell and not at the repository root — `npx --no-install asar` from hereFix: Pin versions and sources; vendor anything fetched at install time.…op processes (read-only), info returns details for one process (read-only), and kill sends a signal to terminate a process and mutates remote state. pid is required …Fix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.* Build command to kill a processFix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.browser a tab pointed at the local control plane — what `npx mcp-ssh-managerFix: Pin versions and sources; vendor anything fetched at install time.* that used to kill the whole MCP server process, while the awaited promiseFix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.if (/\b(ls|ll)\b/.test(command)) return 'README.txt reports uploads\n';Fix: Remove the call, or declare the host in frontmatter and explain in the README what is sent and why.// as the start of another job, so `* * * * * curl … | sh #` installed a jobFix: Build commands from fixed arguments, never from interpolated strings; prefer an API client over shelling out.chmod +x "$TEMP_BUILD/demo.sh" 2>/dev/null || trueFix: Request the narrowest permission that works; replace wildcards with the exact paths or hosts.kill $PID 2>/dev/nullFix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.command: `${sudoPrefix}chmod ${shellQuote(permissions)} ${target}`Fix: Request the narrowest permission that works; replace wildcards with the exact paths or hosts.await ssh.execCommand(`printf '%s' ${shellQuote(scriptContent)} > ${quotedPath} && chmod +x ${quotedPath}`);Fix: Request the narrowest permission that works; replace wildcards with the exact paths or hosts.// Execute command with timeout - using child_process timeout for real killFix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.subprocess.run(['ffmpeg', '-y', '-loglevel', 'error', '-framerate', str(fps),Fix: Build commands from fixed arguments, never from interpolated strings; prefer an API client over shelling out.Cost to the agent
Tokens estimated at four characters each. The tool list is re-read by the agent on every turn, so it is the number to compare between servers that do the same job: ranked across the index.
Known vulnerabilities
No advisories in OSV for mcp-ssh-manager@4.0.3 as of 2 h ago. This is the package itself, not its dependencies.
What would make this Verified
43 points to 80 · weights: behavior 45%, static 25%, prompt 15%, publisher 15%- Reads credential files≈ +27 on the totalThe sandbox saw reads under ~/.ssh, ~/.aws, ~/.npmrc, ~/.config or similar. Read only files inside the project, and only the ones the task needs.
- Reads credential paths from the home directory≈ +9 on the totalStop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.
- README tells the reader to click past SmartScreen≈ +9 on the totalPin versions and sources; vendor anything fetched at install time.
- Pipe-to-shell pattern≈ +9 on the totalBuild commands from fixed arguments, never from interpolated strings; prefer an API client over shelling out.
- Unclaimed publisher≈ +8 on the totalClaim bvisible: a verified publisher gets +50 on publisher trust. It is free and usually instant with GitHub sign-in.
- Hidden instructions≈ +6 on the totalRemove HTML comments, zero-width characters and white-on-white text that carry instructions. Everything the agent is meant to read should be visible to the person reading the file.