ssh-for-agents
Enables local Ollama models to execute SSH commands and manage remote files through a function-calling bridge.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@ssh-for-agentsrun 'uptime' on myserver"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
ssh-for-agents
An MCP (Model Context Protocol) server that lets AI agents talk to SSH servers — run commands, read files, and list directories on remote hosts — behind a configurable command-safety policy.
Because it speaks MCP, any MCP-capable agent (Claude Desktop, Claude Code, or your
own client) can use it with no glue code. Connections are async (built on
asyncssh) and pooled per host.
AI agent ──MCP(stdio)──▶ ssh-for-agents ──asyncssh──▶ remote host(s)
│
guardrails policy
(readonly / guarded / unrestricted)Quickstart
git clone https://github.com/WilliamSmithEdward/pySSHForAgents.git
cd pySSHForAgents
python -m venv .venv
.venv\Scripts\Activate.ps1 # macOS/Linux: source .venv/bin/activate
pip install -e .Store your SSH key's passphrase in an environment variable (it's never written to a
config file) — Windows setx NAME "value" then open a new terminal, macOS/Linux
export NAME=value — then register a host and test it end to end:
ssh-for-agents-config add myserver --hostname 203.0.113.10 --user deploy \
--passphrase-env MYSERVER_SSH_PASSPHRASE
ssh-for-agents-run myserver "uptime"That's it. Now point your agent at it — see Connect to Claude below, or Codex / a local Ollama model. The full walkthrough (keys, host-key verification, every agent) is in SETUP.md.
Related MCP server: ssh-mcp
Tools exposed to the agent
Tool | What it does |
| List configured hosts and the active safety policy. |
| Dry-run the policy for a command (allow / needs_confirmation / block). |
| Run a shell command on a host. Destructive commands need |
| Read a remote file over SFTP (size-capped). |
| List a remote directory over SFTP. |
Requirements
Python 3.10+
An SSH key (password auth is not wired up by design — use keys).
Install
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # macOS/Linux
pip install -e .asyncssh needs bcrypt to decrypt passphrase-protected OpenSSH keys; it's a
declared dependency, so the install above pulls it in.
Configure
Copy the example and edit it:
cp hosts.example.json hosts.json{
"hosts": {
"myserver": {
"hostname": "203.0.113.10",
"username": "deploy",
"port": 22,
"private_key": "~/.ssh/id_ed25519",
"passphrase_env": "MYSERVER_SSH_PASSPHRASE",
"known_hosts": "~/.ssh/known_hosts",
"verify_host_key": true
}
},
"policy": {
"mode": "guarded",
"extra_denylist": [],
"extra_readonly_allow": [],
"max_output_bytes": 100000,
"default_timeout": 60
}
}Host fields:
private_key— path to the private key (~is expanded).passphrase_env— name of an environment variable holding the key's passphrase. The passphrase itself is never stored in the config. Omit if the key has no passphrase.known_hosts— path to a known_hosts file used to verify the server's host key. Omit to use the default (~/.ssh/known_hosts+ system files).verify_host_key— setfalseto skip host-key verification (insecure; only for throwaway hosts).
The server looks for hosts.json in the working directory, or wherever
SSH_AGENT_CONFIG points.
hosts.jsonis git-ignored since it describes your infrastructure. Commithosts.example.jsoninstead.
Adding & managing hosts
The config holds any number of hosts — add as many as you like under hosts.
Manage them with the bundled CLI instead of hand-editing JSON:
# add or update a host
ssh-for-agents-config add prod \
--hostname 203.0.113.10 --user deploy \
--key ~/.ssh/id_ed25519 --passphrase-env PROD_SSH_PASSPHRASE
ssh-for-agents-config list # show configured hosts + policy
ssh-for-agents-config remove prod # drop a host
ssh-for-agents-config import-ssh-config # pull hosts from ~/.ssh/config--config <path> (or $SSH_AGENT_CONFIG) targets a specific hosts.json.
Hot-reload: a running server watches hosts.json and reloads it when it
changes, so a newly added host is usable on the next tool call — no restart needed.
Live connections to unchanged hosts are kept; a half-written or invalid edit is
ignored (the last good config stays active).
Shell CLI — for agents that can't call MCP tools
Some agents run shell commands but can't reliably call MCP tools — notably a local
(Ollama) model in Codex, which emits tool-call names the host rejects. For those,
the same hosts and safety policy are exposed as a plain command, ssh-for-agents-run:
ssh-for-agents-run hosts # list aliases + policy
ssh-for-agents-run linode "df -h" # run a command (default verb)
ssh-for-agents-run linode "rm /tmp/x" --confirm
ssh-for-agents-run check linode "rm -rf /" # dry-run the policy
ssh-for-agents-run read linode /etc/os-release
ssh-for-agents-run ls linode /var/logIt reuses hosts.json and the guardrails, and on Windows resolves the key passphrase
from the persisted user environment even when the launching shell filters env vars.
See SETUP.md for wiring it into Codex (AGENTS.md + giving the sandbox read
access to ~/.ssh).
Run
ssh-for-agents # console script (serves over stdio)
python -m ssh_agent_mcp # equivalentThe server communicates over stdio — it's normally launched by an MCP client rather than run by hand.
Connect to Claude Code
claude mcp add ssh-for-agents -- /abs/path/to/.venv/Scripts/python.exe -m ssh_agent_mcpSet the working directory (or SSH_AGENT_CONFIG) so it finds hosts.json.
Connect to Claude Desktop
Add to claude_desktop_config.json:
{
"mcpServers": {
"ssh-for-agents": {
"command": "C:\\path\\to\\.venv\\Scripts\\python.exe",
"args": ["-m", "ssh_agent_mcp"],
"env": {
"SSH_AGENT_CONFIG": "C:\\path\\to\\hosts.json",
"MYSERVER_SSH_PASSPHRASE": "..."
}
}
}
}Use it from a local Ollama model
Ollama runs models and does function calling, but it is not an MCP client — so a
small host/bridge sits between them: it launches this server, hands the model the
tools, runs the tool-calling loop, and routes tool calls back. A working one is
included at examples/ollama_bridge.py:
pip install -e ".[examples]" # adds the `ollama` client library
ollama pull qwen3-coder:30b # any tool-calling model works
python examples/ollama_bridge.py "what is the uptime and disk usage on linode?"The model discovers hosts via list_hosts and runs commands via run_command. When
the policy returns needs_confirmation, the bridge prompts you on the terminal
rather than letting the model authorize its own destructive command. Pick a model
with OLLAMA_MODEL=...; it must support tool calling (qwen3-coder, llama3.1/3.2,
mistral-nemo, …).
Ollama model ◀──function calling──▶ bridge ◀──MCP(stdio)──▶ ssh-for-agents ──▶ hostSafety model
The policy runs before any command reaches the server. Pick a mode:
Mode | Behavior |
| Only commands on a read-only allowlist run; everything else is blocked. |
| (default) Most commands run freely. Destructive/state-changing commands return |
| Anything runs. Only sensible for disposable/sandboxed hosts. |
In guarded mode:
Needs confirmation:
rm,dd,mkfs,shutdown/reboot,kill,mount, firewall edits,crontab, package installs/removals (apt install, …), service changes (systemctl stop, …),git push/reset,curl … | sh, recursivechmod/chown, redirects into system paths, and more.Hard-blocked (cannot be confirmed away): fork bombs, recursive deletes of root paths (
rm -rf /,/home, …), and writing/formatting raw disk devices (dd of=/dev/sda,mkfs … /dev/nvme0n1).Commands are parsed into segments across
;,&&,||, and pipes, andsudo/envwrappers are seen through, socd /x && sudo rm yis still flagged.
Extend it without editing code via extra_denylist (more binaries that need
confirmation) and extra_readonly_allow (more binaries allowed in readonly mode).
Important — this is a safety net, not a sandbox. String-based inspection of shell commands can always be evaded by a determined caller (
base64 … | sh, exotic quoting, writing then running a script, etc.). It exists to prevent accidents and obvious mistakes. For a real trust boundary, connect as a dedicated unprivileged user and constrain it with OS permissions and/or sshdForceCommand. Treat the mode as defense-in-depth, not as containment of an adversarial agent.
Develop & test
pip install -e ".[dev]"
pytest # guardrail policy tests (no SSH needed)Project layout
ssh_agent_mcp/
guardrails.py # command-safety policy engine (pure, well-tested)
config.py # hosts.json loading + validation
ssh_client.py # asyncssh connection pool (run / read_file / list_dir)
server.py # FastMCP server + tools (with hosts.json hot-reload)
manage.py # `ssh-for-agents-config` CLI: add / list / remove / import hosts
run_cli.py # `ssh-for-agents-run` CLI: run SSH from the shell (Codex/local models)
tests/
test_guardrails.py
test_manage.py
test_run_cli.py
examples/
ollama_bridge.py # drive a local Ollama model with these tools
hosts.example.json
SETUP.md # step-by-step reproduction guideResources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/WilliamSmithEdward/pySSHForAgents'
If you have feedback or need assistance with the MCP directory API, please join our Discord server