A lightweight Go daemon that acts as a local intermediary for SSH connections. It exposes an MCP (Model Context Protocol) server over a Unix socket so Claude Code can run remote commands, read and write files, and use other SSH capabilities through a persistent SSH ControlMaster session that you manage.
The key idea: Claude never touches your SSH credentials or manages connection lifecycle. You bring up the ControlMaster session, the daemon proxies through it.
Step 1: Build and install the binary
go build ./cmd/claude-ssh-daemon
sudo cp claude-ssh-daemon /usr/local/bin/Step 2: Start your SSH ControlMaster session
The daemon never manages SSH connections. You start and maintain the session yourself. The -S socket path must match the ssh_socket value in your config.
ssh -M -S /tmp/ssh-ctrl-user@host.sock -fN user@hostStep 3: Write your config
Create ~/.config/claude-ssh-daemon/config.json. See the Config section below for the full example.
Step 4: Install the background service
The daemon should run persistently so it is available whenever Claude Code needs it.
-
macOS (launchd): Copy the plist, substitute your username (launchd does not expand
~in paths), then load the service:cp contrib/com.claude-ssh-daemon.plist ~/Library/LaunchAgents/ sed -i '' "s/YOUR_USERNAME/$(id -un)/g" ~/Library/LaunchAgents/com.claude-ssh-daemon.plist launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.claude-ssh-daemon.plist
View logs with:
tail -f ~/Library/Logs/claude-ssh-daemon.logTo unload:
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.claude-ssh-daemon.plist
-
Linux (systemd): Copy
contrib/claude-ssh-daemon.serviceto~/.config/systemd/user/and run:systemctl --user daemon-reload systemctl --user enable --now claude-ssh-daemonThe service file has install instructions in its comments. View logs with:
journalctl --user -u claude-ssh-daemon -f
Step 5: Register the MCP server with Claude Code
Claude Code spawns MCP servers as subprocesses using a restricted PATH that typically does not include Homebrew (/opt/homebrew/bin) on macOS. You must use the full path to socat.
Find the path first:
which socat
# macOS Apple Silicon (Homebrew): /opt/homebrew/bin/socat
# macOS Intel (Homebrew): /usr/local/bin/socat
# Linux (apt/system): /usr/bin/socatThen register using that path (replace the path and socket path as needed):
claude mcp add claude-ssh-daemon /opt/homebrew/bin/socat -- - UNIX-CONNECT:/tmp/claude-ssh-daemon.sockThis registers the server globally (all projects). Verify it connected:
# In any Claude Code session:
/mcpYou should see claude-ssh-daemon listed as connected with its tools.
SSH tools (v2.1 -- all 9 shipped)
| Tool | Capability | Type | Description |
|---|---|---|---|
ssh_connection_status |
always on | read-only | Check whether the SSH ControlMaster socket is alive and get a re-establishment hint if it is not |
ssh_exec |
exec |
destructive | Execute a remote shell command via the SSH ControlMaster session |
ssh_read_file |
file_read |
read-only | Read the contents of a remote file |
ssh_list_dir |
file_read |
read-only | List the contents of a remote directory |
ssh_write_file |
file_write |
destructive | Write or overwrite a remote file |
ssh_upload_file |
file_write |
destructive | Upload a local file to the remote host |
ssh_download_file |
file_write |
destructive | Download a remote file to the local machine |
ssh_forward_port |
port_forward |
destructive | Create a local SSH port forward via the ControlMaster session (auto-allocates a free local port) |
ssh_list_forwards |
port_forward |
read-only | List all active port forwards managed by this daemon instance, with running/dead status |
Core daemon
- Starts up and creates a Unix socket at the path you configure
- Loads config from
~/.config/claude-ssh-daemon/config.json - Exposes an MCP server over the socket using the official
go-sdk(spec 2025-11-25) - Accepts one client connection at a time (sequential, intentional)
- Handles
SIGTERMandSIGINTfor clean shutdown with a 5-second drain timeout - Removes the socket file on exit so restarts do not hit
EADDRINUSE
Security
- Socket created with mode 0600 (owner read/write only)
- Umask-before-listen pattern to close the race window between
listen()andchmod()(mitigates CVE-2023-45145 class) - Capability toggles in config (
exec,file_read,file_write,port_forward) all default to off; disabled tools are never registered - Safeguards layer: prompt-injection scanning on all tool output (on by default), overwrite protection for
ssh_write_file(opt-in), destructive command blocking forssh_exec(opt-in) - Per-host
base_dir: lexically confines all file and exec-cwd operations to a directory subtree (opt-in per host) - Per-host
exec_allowlist: restrictsssh_execto a set of command prefixes (opt-in per host)
- Go 1.23+
- macOS or Linux
- An SSH ControlMaster session already running (you manage this); OpenSSH 6.0+ recommended (
-O checkrequires OpenSSH 5.6+, 6.0+ is a safe documented floor) socatfor the Claude Code stdio bridge:brew install socat(macOS) orapt install socat(Debian/Ubuntu). Use the full absolute path tosocatin your MCP config — Claude Code spawns servers with a restricted PATH and may not find socat by name alone (see Step 5 above).
go build ./cmd/claude-ssh-daemonCreate ~/.config/claude-ssh-daemon/config.json.
Single-host (legacy, still fully supported):
{
"ssh_socket": "/tmp/ssh-ctrl-user@host.sock",
"mcp_socket": "/tmp/claude-ssh-daemon.sock",
"ssh_user": "ubuntu",
"ssh_host": "my.server.com",
"capabilities": {
"exec": true,
"file_read": true,
"file_write": true,
"port_forward": false
}
}Multi-host:
{
"mcp_socket": "/tmp/claude-ssh-daemon.sock",
"default_host": "prod",
"hosts": {
"prod": {
"socket": "/tmp/ssh-ctrl-ubuntu@prod.sock",
"user": "ubuntu",
"host": "prod.example.com",
"base_dir": "/srv/app",
"exec_allowlist": ["git ", "make ", "npm "]
},
"staging": {
"socket": "/tmp/ssh-ctrl-ubuntu@staging.sock",
"user": "ubuntu",
"host": "staging.example.com"
}
},
"capabilities": {
"exec": true,
"file_read": true,
"file_write": true,
"port_forward": false
}
}With multi-host config every tool accepts an optional host parameter. Omit it to target default_host. Each host needs its own ControlMaster session running against its socket path.
Per-host optional fields:
| Field | Default | Effect |
|---|---|---|
base_dir |
"" (unset) |
Absolute path. When set, all file operations (ssh_read_file, ssh_write_file, ssh_list_dir, ssh_upload_file, ssh_download_file) and ssh_exec cwd are confined to this directory tree by lexical path checking. Paths that resolve outside are rejected with isError: true. Symlinks on the remote are not resolved and may point outside base_dir. |
exec_allowlist |
null (allow-all) |
JSON array of command prefixes. When null or absent: all commands are allowed. When set to [] (empty array): all commands are denied. When set to ["git ", "make "]: only commands whose first token matches a listed prefix are allowed. |
Safeguards (optional):
{
"safeguards": {
"guard_disabled": false,
"allow_overwrite": false,
"allow_delete": false,
"patterns": []
}
}| Field | Default | Effect |
|---|---|---|
guard_disabled |
false |
When false, stdout/stderr from every tool is scanned for prompt-injection patterns. A warning is appended to the result but the operation is not blocked. |
allow_overwrite |
false |
When false, ssh_write_file refuses to write to paths that already exist on the remote. |
allow_delete |
false |
When false, ssh_exec blocks commands whose first token is rm, unlink, truncate, shred, or dd. |
patterns |
[] |
Additional regex strings appended to the built-in injection-detection ruleset. |
Capability toggles: only tools for enabled capabilities are registered. Disabled tools are invisible to Claude — they do not appear in tools/list.
./claude-ssh-daemonThe daemon logs to stderr in JSON format. It logs the socket path on startup and each connection/disconnection event. For persistent operation, use the service files in contrib/ (see Quick start above).
Claude Code speaks MCP over stdio. The daemon listens on a Unix domain socket. A socat bridge connects the two -- Claude Code launches socat, which forwards its stdin/stdout to the daemon's socket.
Important: Claude Code spawns MCP server processes with a restricted PATH that typically does not include Homebrew prefixes on macOS. Always use the absolute path to socat. Find it with which socat.
Global registration (recommended):
# macOS Apple Silicon — adjust path for your system (see `which socat`)
claude mcp add claude-ssh-daemon /opt/homebrew/bin/socat -- - UNIX-CONNECT:/tmp/claude-ssh-daemon.sockReplace /tmp/claude-ssh-daemon.sock with the mcp_socket value from your config. This makes the server available in all Claude Code projects.
Per-project registration:
Add a .claude/mcp.json file in your project directory:
{
"mcpServers": {
"claude-ssh-daemon": {
"type": "stdio",
"command": "/opt/homebrew/bin/socat",
"args": ["-", "UNIX-CONNECT:/tmp/claude-ssh-daemon.sock"]
}
}
}Adjust the command path for your platform (/usr/local/bin/socat on Intel Mac, /usr/bin/socat on Linux).
Note: Claude Code has no native Unix-socket transport type -- the stdio + socat bridge is the correct and only approach. A unix or socket transport key is not supported.
The MCP socket is created mode 0600 (owner-only), so the socat bridge is only reachable by the user who owns the daemon process.
Once the daemon is running and registered, you can talk to your remote hosts naturally. Here are some prompts that work well:
Explore and understand a remote service
Check the status of nginx on my prod server — is it running, what config is it using, and are there any recent errors in the logs?
Tail logs and diagnose issues
Read the last 100 lines of /var/log/app/error.log on prod and summarize any recurring errors or patterns you see.
Deploy a change
Upload my local ./dist/app.js to /srv/app/dist/app.js on the staging host, then restart the Node process with: systemctl --user restart myapp
Database access via port forwarding
Forward the Postgres port from db.internal:5432 so I can connect to it locally. Tell me the local port when it's ready.
After the forward is established, you can then connect with psql -h 127.0.0.1 -p <local_port> -U myuser mydb.
Inspect and edit a config file
Read /etc/nginx/sites-available/mysite on prod. The upstream timeout is too low — update it to 60s and reload nginx.
Multi-host workflow
Compare the app version currently deployed on staging vs prod — read /srv/app/package.json on each host and tell me if they differ.
Check connection health before starting work
Check the SSH connection status for all my configured hosts before we start.
v2.1 is complete. All 9 SSH tools work end-to-end: 7 core tools (connection check, exec, file read/write/list/upload/download) plus 2 port-forwarding tools (ssh_forward_port, ssh_list_forwards). Multi-host config, base_dir sandboxing, exec_allowlist, and safeguards (prompt-injection scanning, overwrite protection, delete blocking) are all shipped.