SSH wrapper that sends every command to an LLM for approval before execution. Give your AI agents SSH access without giving them the keys to the kingdom.
ssh-guard prod-server 'ls -la /etc/nginx/'
# drwxr-xr-x 8 root root 4096 Mar 10 14:22 .
# -rw-r--r-- 1 root root 1482 Mar 10 14:22 nginx.conf
# ...
ssh-guard prod-server 'rm -rf /etc/nginx/'
# ssh-guard: DENIED (risk=9) - Recursive deletion of system config directory.Zero output on approval. Denied commands print the reason and risk score.
AI agents (Claude Code, Aider, OpenHands, CrewAI, LangChain, etc.) increasingly need SSH access to remote servers for debugging, log analysis, and ops tasks. But a single hallucinated rm -rf or kubectl delete namespace can take down production.
ssh-guard sits between the agent and SSH. Every command gets evaluated by a fast, cheap LLM call (Gemini Flash by default via OpenRouter, ~0.001c per decision) before it reaches the server. Approved commands run silently. Denied commands return an error with explanation.
curl -fsSL https://raw.githubusercontent.com/morgaesis/ssh-guard/main/install.sh | bashOr build from source: cargo install --path .
See INSTALL.md for all options (manual download, specific versions, provider setup, agent integration).
# Set your API key (OpenRouter, or any OpenAI-compatible endpoint)
export SSH_GUARD_API_KEY="your-key-here"
# Or: export OPENROUTER_API_KEY="your-key-here"
# Use it like ssh
ssh-guard myserver 'uptime'
ssh-guard myserver 'cat /var/log/syslog'
ssh-guard myserver 'sudo systemctl status nginx'
# These will be denied in readonly mode:
ssh-guard myserver 'rm -rf /tmp/*'
ssh-guard myserver 'systemctl restart nginx'Three built-in policies, set via SSH_GUARD_MODE:
| Mode | Default | Use case |
|---|---|---|
readonly |
Yes | Agents that only need to observe. Blocks all writes, installs, service changes. |
paranoid |
Like readonly, but also blocks reading file contents, env vars, logs. Only structural metadata (ls, ps, df, etc). Prevents secret exfiltration. | |
safe |
Agents that need to do work. Allows targeted writes and service restarts, blocks destructive/broad operations (rm -rf, reboot, kubectl delete namespace). |
SSH_GUARD_MODE=safe ssh-guard server 'systemctl restart myapp' # allowed
SSH_GUARD_MODE=paranoid ssh-guard server 'cat /etc/passwd' # deniedAll modes evaluate sudo by the underlying command, not the keyword itself:
sudo ls /etc/nginx/ # readonly: allowed (read operation)
sudo rm -rf /etc/nginx/ # readonly: denied (write operation)
sudo systemctl restart app # safe: allowed (targeted restart)All configuration via environment variables or .env files.
ssh-guard walks up from your current directory to / looking for .env files (closest wins), so you can scope config per project.
| Variable | Default | Description |
|---|---|---|
SSH_GUARD_API_KEY |
$OPENROUTER_API_KEY |
LLM API key (required) |
SSH_GUARD_API_URL |
https://openrouter.ai/api/v1/chat/completions |
Any OpenAI-compatible endpoint |
SSH_GUARD_MODEL |
google/gemini-2.0-flash-001 |
Model for command evaluation |
SSH_GUARD_API_TYPE |
openai |
openai or anthropic |
SSH_GUARD_MODE |
readonly |
readonly, paranoid, or safe |
SSH_GUARD_PROMPT |
(per mode) | Custom system prompt (overrides mode) |
SSH_GUARD_PASSTHROUGH |
(none) | Always-allow commands, comma-separated |
SSH_GUARD_LOG |
(none) | Audit log file path |
SSH_GUARD_REDACT |
true |
Redact secrets from command output |
SSH_GUARD_SSH_BIN |
/usr/bin/ssh |
Path to real ssh binary |
SSH_GUARD_TIMEOUT |
30 |
LLM call timeout (seconds) |
SSH_GUARD_MAX_TOKENS |
512 |
Max LLM response tokens |
See .env.example for a copyable template.
Point your agent's SSH command at ssh-guard instead of ssh.
Claude Code (CLAUDE.md)
# SSH Access
Use `ssh-guard` instead of `ssh` for all remote commands.
Never use interactive SSH sessions.OpenHands / SWE-Agent
# In agent config or sandbox setup
export SSH_GUARD_API_KEY="..."
export SSH_GUARD_MODE=readonly
alias ssh=ssh-guardLangChain / CrewAI tool definition
import subprocess
def ssh_command(host: str, command: str) -> str:
"""Execute a command on a remote host via ssh-guard."""
result = subprocess.run(
["ssh-guard", host, command],
capture_output=True, text=True, timeout=60,
env={**os.environ, "SSH_GUARD_MODE": "readonly"}
)
if result.returncode != 0:
return f"DENIED: {result.stderr.strip()}"
return result.stdoutGeneric: alias ssh to ssh-guard
# In the agent's shell init or .env
alias ssh=ssh-guard
export SSH_GUARD_API_KEY="..."When SSH_GUARD_REDACT=true (default), command output is filtered through pattern-based redaction before reaching the agent:
DB_PASSWORD=hunter2 -> DB_PASSWORD=[REDACTED]
export API_TOKEN="sk-..." -> export API_TOKEN="[REDACTED]"
-----BEGIN PRIVATE KEY---- -> -----BEGIN PRIVATE KEY---- [REDACTED]Patterns matched: *_TOKEN, *_KEY, *_SECRET, *_PASSWORD, *_CREDENTIAL, bearer, PEM blocks, sk-* prefixed strings, JWTs.
export SSH_GUARD_LOG=/var/log/ssh-guard.logLogs denials always. Logs approvals only when risk score >= 4. Format:
[2025-03-12T14:22:01+00:00] DENIED risk=9 cmd=rm -rf / reason=Recursive deletion of root filesystem
[2025-03-12T14:22:15+00:00] APPROVED risk=4 cmd=sudo cat /etc/hosts reason=Reading system config file
Be honest about what this is and isn't:
- Not a sandbox. ssh-guard is a policy gate, not an isolation boundary. A sufficiently creative command chain might get past the LLM. Use it as defense-in-depth alongside proper IAM, restricted users, and network segmentation.
- No interactive sessions. Agents get command execution only. Humans should use
sshdirectly. - LLM latency. Each command adds ~0.5-2s for the LLM call. Use
SSH_GUARD_PASSTHROUGHfor commands you know are safe to skip the check. - Fail-closed. If the LLM call fails or returns unparseable output, the command is denied.
A VHS tape file is included for generating the terminal recording:
# Install VHS: https://github.com/charmbracelet/vhs
vhs demo.tape # produces docs/demo.svgMIT