Skip to main content
Glama
WilliamSmithEdward

ssh-for-agents

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_hosts

List configured hosts and the active safety policy.

check_command

Dry-run the policy for a command (allow / needs_confirmation / block).

run_command

Run a shell command on a host. Destructive commands need confirm=true.

read_file

Read a remote file over SFTP (size-capped).

list_dir

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_envname 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 — set false to 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.json is git-ignored since it describes your infrastructure. Commit hosts.example.json instead.

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/log

It 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 # equivalent

The 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_mcp

Set 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 ──▶ host

Safety model

The policy runs before any command reaches the server. Pick a mode:

Mode

Behavior

readonly

Only commands on a read-only allowlist run; everything else is blocked.

guarded

(default) Most commands run freely. Destructive/state-changing commands return needs_confirmation until the agent re-calls with confirm=true. A few catastrophic patterns are hard-blocked.

unrestricted

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, recursive chmod/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, and sudo/env wrappers are seen through, so cd /x && sudo rm y is 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 sshd ForceCommand. 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 guide
Install Server
A
license - permissive license
A
quality
C
maintenance

Resources

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