AI-native SSH toolkit. Not a human terminal. Not an OpenSSH wrapper.
AgentSSH speaks SSH directly through russh — a pure-Rust async SSH implementation. One binary: client + daemon + proxy. No C library to install. No shell wrapping. All output in structured JSON. Built for agents, also works for humans.
🐙 GitHub · 📦 crates.io · 🔧 Quick Start · 🏗️ Architecture · 📋 Command Reference
Drop SKILL.md into your agent's skill directory to give it SSH superpowers.
Your agent gains: exec, file upload/download, session management, port forwarding, SOCKS5 proxy — all via structured JSON. See SKILL.md for the full capability definition.
ssh |
paramiko |
libssh2 |
AgentSSH | |
|---|---|---|---|---|
| Output | Raw terminal | Mixed string | App parses | ✅ Structured JSON |
| Connections | One shot | Manual | Manual | ✅ Daemon-pooled + reuse |
| File transfer | scp / sftp |
Separate impl | Separate impl | ✅ SFTP → exec built-in |
| Port forward | -L / -D flags |
Manual coding | Manual coding | ✅ Daemon-managed |
| PTY model | Screen-scraped | Blocking reads | Polling loops | ✅ Async drain task |
| Auth config | ~/.ssh/config |
Inline params | Inline params | ✅ JSON profiles |
| C dependency | Yes | Yes | Yes | None — pure Rust |
| Agent-first | ❌ | ❌ | ❌ | ✅ --json everywhere |
The last two rows are the whole point. No C library to fight with. No screen scraping. Programs call AgentSSH like they'd call an API.
# Install from crates.io (recommended)
cargo install agentssh
# Or build from source
cargo build --release
# → target/release/agentsshagentssh profile add prod \
--host example.com \
--username root \
--private-key ~/.ssh/id_ed25519
agentssh profile list
# tencent root@82.157.147.224:22
# prod root@example.com:22agentssh --output json exec --profile prod --retry 3 -- uptime
# {"ok":true,"status":"completed","exit_code":0,"stdout":"21:03:01 up 42 days\n","stderr":""}Commands run through /bin/sh -c — pipes, redirects, and shell chains work natively:
# Pipe and filter
agentssh --output json exec --profile prod -- grep ERROR /var/log/syslog \| tail -5
# Redirect output to remote file
agentssh --output json exec --profile prod -- cat \> /etc/config \</dev/null
# Chain commands
agentssh --output json exec --profile prod -- ls /etc \&\& systemctl status nginxTip: Escape
|,>,&&with backslashes so your local shell doesn't eat them.
exec automatically suspends long-running commands after 30 seconds (configurable):
# Short command → returns immediately
agentssh exec -p prod -- uptime
# → {"status":"completed", "stdout":"...", "exit_code":0}
# Long command → auto-suspends after 30s, returns session ID
agentssh exec -p prod -- cargo build
# → {"status":"suspended", "session_id":"s7", "commands":{...}}
# Check suspended command
agentssh session status --session-id s7
agentssh session read --session-id s7 --follow
# Start server without blocking
agentssh exec -p prod -- python3 app.py
# → {"status":"suspended", "session_id":"s8", ...}
# Server continues running on remote host
# Never suspend (wait forever)
agentssh exec -p prod --suspend-timeout 0 -- short-commandagentssh connect --profile prod --reconnect
# → session_id: s1 (auto-reconnect on disconnect)
# Clean command execution (no PTY echo — structured JSON)
agentssh session exec --session-id s1 -- uname -a
# → {"exit_code":0, "stdout":"Linux ...\n", "stderr":""}
# Interactive PTY mode
agentssh session send --session-id s1 --input $'ls -la\n'
agentssh session send --session-id s1 \
--input $'sudo systemctl restart nginx\n' \
--expect "[sudo] password" \
--respond $'mypassword\n'
> **⚠️ `--input` needs a real newline.** `"echo hello\n"` sends two literal characters `\` and `n` — the shell won't execute the command. Use `$'echo hello\n'` (ANSI-C quoting) or embed an actual line break so the shell gets a real Enter.
>
> ```bash
> # ✅ ANSI-C quoting — sends a real Enter to the PTY
> agentssh session send --session-id s1 --input $'ls -la\n'
>
> # ❌ This sends literal \ and n — shell just echoes, never runs
> agentssh session send --session-id s1 --input "ls -la\n"
> ```
agentssh session read --session-id s1 # latest output
agentssh session read --session-id s1 --follow # stream live
agentssh session spawn --from s1 # new PTY on same SSH conn
agentssh session ping --session-id s1
agentssh session close --session-id s1# Default auto: SFTP → exec (works on Linux, macOS, and Windows!)
agentssh file upload --profile prod --local ./app --remote /opt/app
agentssh file download --profile prod --remote /var/log/syslog --local ./syslog
agentssh file ls --profile prod --remote /var/www
# Force a specific protocol
agentssh file upload --profile prod --method sftp --local ./app --remote /opt/app# Local port forward: localhost:9999 → remote internal :8080
agentssh proxy create --profile prod \
--local 127.0.0.1:9999 \
--remote 127.0.0.1:8080
# SOCKS5 dynamic proxy: route all traffic through remote host
agentssh proxy create --profile prod --socks5 127.0.0.1:1080
curl --socks5 127.0.0.1:1080 http://internal-service/
agentssh proxy list
agentssh proxy ping --proxy-id p1
agentssh proxy close --proxy-id p1
agentssh proxy close --all┌──────────────┐ Unix Socket ┌──────────────────┐
│ CLI client │ ◄──────────────────► │ Daemon (serve) │
│ (one-shot) │ JSON-line IPC │ │
└──────────────┘ │ ┌─────────────┐ │
│ │ sessions │ │
┌──────────────┐ │ │ proxies │ │
│ CLI client │◄────────────────────►│ │ connections │ │
│ (session) │ │ └─────────────┘ │
└──────────────┘ └───────┬──────────┘
│
SSH (russh)
│
┌─────────▼─────────┐
│ Remote servers │
│ Linux · macOS · │
│ Windows │
└───────────────────┘
Daemon — auto-starts on first session/proxy command. Survives CLI invocations. Handles heartbeat, session health, connection pooling.
Connection pooling — sessions can share one TCP/SSH connection. session spawn --from <id> opens a fresh PTY on the same underlying connection.
Proxy tasks — each proxy create spawns an async listener task inside the daemon. Accepted connections get their own handler task with non-blocking bidirectional forwarding.
# ⚡ One-shot (auto-suspend after 30s if still running)
agentssh exec # Run command → stdout + exit code
agentssh exec --suspend-timeout 0 # Never suspend, wait forever
agentssh shell # Interactive PTY (human use)
# 🔄 Sessions
agentssh connect # Open PTY → session_id
agentssh session send # Send input (± expect/respond pairs)
agentssh session spawn --from s1 # New PTY on s1's SSH connection
agentssh session read # Read output from cursor
agentssh session resize # Change PTY dimensions
agentssh session signal # Send signal (INT, TERM, KILL…)
agentssh session status # Get session metadata
agentssh session ping # Check if session is alive
agentssh session list # List sessions + connection groups
agentssh session close # Close session
# 📁 Files
agentssh file upload # Upload (SFTP → exec)
agentssh file download # Download (SFTP → exec)
agentssh file ls # List remote directory
# 🌐 Proxy & tunnels
agentssh proxy create # -L forward or -D SOCKS5
agentssh proxy list # List active proxies
agentssh proxy ping # Check proxy health
agentssh proxy close # Close one or --all
# 🔐 Profiles
agentssh profile list | read | add | write | delete
# ⚙️ Daemon
agentssh daemon serve # Start daemon (auto-started)
agentssh daemon shutdown # Stop daemon + cleanup| Path | Purpose |
|---|---|
~/.config/agentssh/profiles.json |
SSH connection profiles |
$XDG_RUNTIME_DIR/agentssh-{user}.sock |
Daemon Unix socket |
/tmp/agentssh-daemon.log |
Daemon log (override: AGENTSSH_LOG) |
{
"profiles": {
"prod": {
"host": "example.com",
"port": 22,
"username": "root",
"private_key": "~/.ssh/id_ed25519",
"retry": 3,
"retry_delay_ms": 250
}
}
}Supported fields: host, port, username, password (prefix $ for env var), private_key (path, $ENV, or inline), passphrase (prefix $ for env var), ready_timeout_ms, retry, retry_delay_ms.
- Rust ≥ 1.85 (edition 2024)
- Unix only (uses Unix domain sockets)
- No C library required — russh is pure Rust
cargo build --release
# → target/release/agentsshAgentSSH uses Trust-On-First-Use (TOFU) with ~/.ssh/known_hosts:
- First connection: the server's host key is appended to
known_hostsautomatically. - Subsequent connections: the key is verified against the stored entry. A mismatch aborts with an error (host key changed).
- Non-standard ports use the
[host]:portbracketed format inknown_hosts.
When the exec fallback method is used for file operations, commands are constructed with shell_words::quote() to safely escape arguments and prevent shell injection.
MIT
⭐ Found this useful? Give it a star on GitHub.