ssh-mcp gives an LLM agent SSH access it has to earn
MCP server exposing SSH control for Linux servers via Model Context Protocol.
At a glance
- What is it?
- ssh-mcp is a Model Context Protocol server that exposes SSH to LLM agents with command classification, a role by host-group authorization matrix, approval gates for destructive work and audit logging. The documentation is unusually blunt that prompt injection has no general fix, so the server narrows the blast radius rather than removing the risk.
- Who is it for?
- ssh-mcp is worth the setup cost for a team that has to let an agent touch real hosts and can accept a policy file, a non-root account and a human in the approval loop. It is the wrong tool for an unattended agent that needs unattended shell access, because the destructive-work gate is the feature doing the real work.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Prompt injection has no fix, so every command is treated as untrusted
The README opens by naming the risk rather than hiding it. Giving an LLM shell access on a remote host puts private data, untrusted input and network egress in one place, which the documentation links to Simon Willison's "lethal trifecta". The conclusion drawn from that is the design brief for the whole server: prompt injection has no general fix, so ssh-mcp assumes any command may be attacker influenced.
Given that assumption, the server does three things before a command runs. It classifies the command, it authorizes the command against a role by host-group matrix, and it gates destructive work behind approval. Every decision is recorded either way, so a refusal is as auditable as an execution.
The documentation is careful about what this buys you. Narrowing the blast radius is not the same as removing the risk, and two obligations stay with the operator: never point the server at a root account, and never set `auto` approval on a production profile. Those are not style suggestions, they are the two ways the security model collapses.
The tool surface is 14 tools. The read-only ones visible in the table are `list-connections`, which discovers available hosts and connection status, and `list-sessions`, which lists active sessions per host. Session lifecycle is handled by `open-session`, for a named interactive stateful session or a background session, and `close-session`. The remainder of the table is truncated in the documentation available here, so the full catalogue has to be read from `tools/list` on a running server.
Approval modes decide when the agent has to stop and ask
The config file is where the policy lives. It decides which hosts, roles and policy rules the server honours, and the documentation is explicit that the server checks that nobody but you can read it.
A minimal profile looks like this:
[defaults]
defaultProfile = "dev"
approvalMode = "ask-destructive"
[[profiles]]
name = "dev"
host = "192.168.1.100"
port = 22
user = "deploy" # NOT root!
auth = "key"
keyRef = "~/.ssh/id_ed25519"
role = "admin"
approvalPolicy = "auto" # dev is permissiveTwo approval mechanisms appear here and they are not the same thing. `approvalMode` is a global default set to `ask-destructive`, which is the sensible value. `approvalPolicy` sits on the profile and in this example is set to `auto` because the profile is a development one.
The two rules the documentation repeats are worth internalising precisely because the example violates one of them. The `auto` in this snippet is acceptable on a development profile pointed at a `deploy` user on a private address, and it is the exact configuration the README warns against on a production profile. The user line says `NOT root!` in the same way.
`role` is the axis that feeds the authorization matrix, and it is worth noting that the role and the SSH user are separate fields. The role decides what the agent may attempt, the user decides what the session can actually do once a command lands, and the two do not have to match. That separation is the point: a policy can allow a low privilege role to attempt a narrow set of operations without granting the underlying account the capability to do anything more.
On Linux and macOS the server refuses to start on a loose config file
The permission model differs by platform, and the documentation says why. On Linux and macOS the check is enforced: the server refuses to start unless the config file and its directory are owner-only. The fix is one command, and the directory mode is included because `mkdir -p` under the default umask leaves the directory at 0755:
chmod 700 ~/.config/ssh-mcp && chmod 600 ~/.config/ssh-mcp/config.tomlThe config file locations are platform specific. On Linux it is `~/.config/ssh-mcp/config.toml`, or `$XDG_CONFIG_HOME/ssh-mcp/config.toml`. On macOS it is `~/Library/Application Support/ssh-mcp/config.toml`. On Windows it is `%APPDATA%\ssh-mcp\config.toml`.
The reason for enforcement here is simple and the documentation states it plainly: only the owner is an unambiguous requirement on a POSIX machine, and chmod fixes it in one line. A config that another account can read is a config that decides which hosts, roles and approval policy the server honours, so leaking it leaks the policy.
There is also a documented configuration file, `config.default.toml`, in the repository, which is the reference to consult when building a production profile rather than writing one from the quick start example.
Windows reads the ACL and treats read exposure differently from write
Windows has no mode bits, so the server reads the ACL with `icacls` instead. The outcome table is more nuanced than the POSIX rule, and the reason given is that read exposure and write exposure are not treated alike because Windows is much clearer about one of them than the other.
If the ACL lets another account only read the config, the situation is reported and the server starts. If another account can change the config, the server refuses. If there is no ACL at all, the server refuses, since that is full control for everyone. If the ACL cannot be read at all, the server refuses, except when `icacls` is absent or the check timed out.
The reasoning behind the split is stated directly: read exposure is reported rather than refused because Windows is genuinely muddier than POSIX there, and refusing over it blocked a config at the documented location. Write exposure is refused because another account being able to rewrite the file that decides which hosts, roles and approval policy the server honours is an authorization bypass, not a disclosure.
Two flags move the whole behaviour. `--strictConfigAcl` refuses everything the check objects to, read-only grants included. `--allowUncheckedConfigAcl` reports everything and refuses nothing. Neither combination leaves you without an exit, which the documentation connects back to the same blocked config.
One Windows detail catches people out. A config under `%APPDATA%` inherits access for the user, `SYSTEM` and `Administrators` and needs nothing done to it, while a file created elsewhere does not: a file under `C:\` inherits read access for every local account and modify access for every authenticated one. The error message names the two `icacls` commands that fix it.
Exit statuses separate a server defect from a configuration mistake
Three exit statuses are defined. Status `0` is a clean shutdown. Status `1` is a defect in the server, printed with a stack trace, and the documentation asks that it be reported. Status `2` is how it was invoked or configured, printed as a message with no stack.
That split exists for supervisors. A supervisor that treats any non-zero status as a failure needs no change. One that matched on `1` to detect a startup problem should also match on `2`, which is the actionable advice here.
The subtle case is an unconfigured deployment. Starting with nothing configured is not an exit-2 condition. The server starts so that it can be described to a client, and refuses each tool call instead. A client or directory can complete the MCP handshake and read `tools/list` even with no config file present, but every tool call is refused with a message naming the config path.
The consequence for automation is that a non-zero exit is no longer a signal for unconfigured. The documentation points at two alternatives: watch for `starting unconfigured` on stderr, or read `configured` from `GET /health` when running the HTTP transport. If your deployment health check treats a non-zero exit as down, this server will look healthy while refusing to do anything, which is the failure mode to design around.
Install with npm, then register the server in your MCP client
Installation is a global npm install:
npm install -g ssh-mcpThe published package is `ssh-mcp`, the binary is `ssh-mcp`, and the MCP server name registered with clients is `io.github.tufantunc/ssh-mcp`. The package version is 2.19.1, matching the latest release tag, and the project is an ES module with a `build/index.js` entry point produced by `tsc`.
Registering it with Claude Code over the stdio transport takes one command:
claude mcp add --transport stdio ssh-mcp -- ssh-mcpFor Claude Desktop, Cursor and Windsurf the equivalent is a JSON block in the client configuration:
{
"mcpServers": {
"ssh-mcp": {
"command": "ssh-mcp",
"env": {
"SSH_MCP_PASSWORD": "your-password"
}
}
}
}Credentials come from environment variables rather than from the config file. For password authentication:
export SSH_MCP_PASSWORD="your-password" # if using auth=password
# OR use SSH agent (recommended):
export SSH_AUTH_SOCK="$SSH_AUTH_SOCK" # already set if agent runningThe SSH agent route is the recommended one, and it lines up with the `auth = "key"` and `keyRef` settings in the profile, where the key reference points at `~/.ssh/id_ed25519`.
One rule is stated in bold in the README and deserves the emphasis: never pass passwords as CLI arguments, because they are visible through `ps aux`. Use environment variables, config files, the SSH agent or the OS keychain. There is also an inspector command in the package scripts, `npx @modelcontextprotocol/inspector node build/index.js`, for listing the tools from a local build without wiring up a client.
MIT licensed, released weekly, and instrumented with OpenTelemetry
The project is MIT licensed, with a `LICENSE` file and a `SECURITY.md` that the README points to for the full threat model. The licences matter here for the obvious reason: this is a component that holds SSH credentials and a policy file, and an MIT licence means an organisation can audit, patch and vendor it into an internal image without negotiating terms.
The release cadence is fast. Recent tags include v2.18.0 on 2026-10-04, v2.19.0 on 2026-10-12 and v2.19.1 on 2026-10-14, and the last push to the repository was on 2026-10-14. Changes are tracked with changesets, and the version script syncs `server.json` and the lockfile version alongside the changelog. A weekly cadence with patch releases a day or two after a minor is a reasonable signal that the project treats its own releases as the unit of deployment.
Observability is built in rather than bolted on. The dependency list includes the OpenTelemetry API, the OTLP HTTP trace exporter, the SDK for Node and the semantic conventions, so trace export is part of the standard build. For a server whose whole value is a record of what an agent tried to do, shipping tracing in the default dependency set makes sense.
There is also a container path. The repository carries a `Dockerfile` and a `docker-compose.yml`, and the compose file is not a demo: it defines role based test SSH servers, an `ssh-admin` account with sudo access, a `ssh-viewer` account without it and an `ssh-operator` account, all from the same openssh-server image. The compose comments explain why those images are pinned by digest, because a silently changed sshd could make a host-key verification or ProxyJump test pass when it should fail. A project that tests its own authorization model against pinned servers is more convincing than one that asserts the model works.
Editorial conclusion
ssh-mcp is worth the setup cost for a team that has to let an agent touch real hosts and can accept a policy file, a non-root account and a human in the approval loop. It is the wrong tool for an unattended agent that needs unattended shell access, because the destructive-work gate is the feature doing the real work. Before you turn it on, read SECURITY.md end to end, set approvalMode to ask-destructive for any profile that reaches a production host, and run the chmod command on the config file so the server does not refuse to start on your Linux or macOS machine.
Frequently asked questions
How do I install the ssh-mcp server?
Run npm install -g ssh-mcp, then register it with your client, for example claude mcp add --transport stdio ssh-mcp -- ssh-mcp for Claude Code. The package exposes the binary ssh-mcp and the MCP server name io.github.tufantunc/ssh-mcp.
Where does the ssh-mcp config file go?
On Linux it is ~/.config/ssh-mcp/config.toml or $XDG_CONFIG_HOME/ssh-mcp/config.toml, on macOS it is ~/Library/Application Support/ssh-mcp/config.toml, and on Windows it is %APPDATA%\ssh-mcp\config.toml. Without a config the server still starts and refuses every tool call.
Why does ssh-mcp refuse to start on my machine?
On Linux and macOS the config file and its directory must be owner-only, so run chmod 700 ~/.config/ssh-mcp && chmod 600 ~/.config/ssh-mcp/config.toml. On Windows the ACL is checked instead, and a config another account can change is refused.
What is the difference between approvalMode and approvalPolicy in ssh-mcp?
approvalMode is the global default in the config, with ask-destructive as the sensible value, while approvalPolicy is set per profile and controls whether commands on that profile need a human. The documentation warns never to set auto approval on a production profile.
How should I pass an SSH password to ssh-mcp?
Use the SSH_MCP_PASSWORD environment variable, or the SSH agent, which the documentation recommends. Never pass a password as a command line argument, because it is visible through ps aux.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/tufantunc-ssh-mcp)Was this useful?