If youβve spent any time pair-programming with AI coding agents youβve likely handed them remote server permissions.
It feels productive. You ask the agent to inspect a failing deployment, check container health across your cluster, or tail application logs, and it obliges in seconds.
I learned this the hard way sitting at my terminal, watching my AI agent do remote server triage across my own machines.
I asked it to investigate an anomaly in my fleet of Pi5s. I watched as it opened the same connection time after time to talk to the same remote server, checking one small thing at a time. Then it asked the exact same question of each server individually, spinning up fresh SSH handshakes for every single command.
Worse, every single docker logs or system check dumped hundredsβsometimes thousandsβof tokens of raw terminal noise and ANSI escape codes straight into the context window. Whenever an SSH connection dropped mid-query, the agent would lose its place and spin up retry loops.
And then I realized what I had actually given it.
In the background, the AI assistant wasnβt just checking my staging box. It had complete, uninhibited read and execute access to my entire ~/.ssh/config. That included jump hosts, production database bastions, client servers, and private key identities.
One hallucinated shell expansion, one prompt injection hidden inside an untrusted Git repo, or one poorly scoped bash command, and your agent is executing arbitrary commands directly on production infrastructure.
Talking of hallucinations, we should point out that the one thing we didnβt add was individual command checking as this was deemed slightly out of bounds. We allow you to apply security by disallowing host access in general. You could also be secure by allowing a more restricted user logging into your host.
You can integrate something like destructive_command_guard which as it suggests guards against destructive commands in your shell(s). Because agentic_ssh maintains an open connection, this allows the history to be followed correctly too, which is one of the key elements of dcg.
π₯ The Two Hidden Problems of Raw Agent SSH
When AI agents interact with remote servers via raw bash, two massive failure modes emerge:
1. The βBlast Radiusβ Problem (Zero Guardrails)
Standard AI terminal tools treat all SSH hosts equally. If itβs in your ~/.ssh/config, the model can reach it. There is no concept of βthis server is for development, but that server holds the customer database.β
Asking an agent to βclean up lingering Docker containers across our machinesβ can turn into an unrecoverable disaster in milliseconds.
2. The βToken Avalancheβ & Reconnect Lag
Whenever an agent executes a command like ssh server 'journalctl -n 500' or docker ps -a, two things happen:
- Connection Latency: Every single command undergoes a full SSH TCP handshake, key exchange, and authentication negotiation (typically 1.2s β 2.0s per invocation).
- Context Poisoning: The command dumps 15,000 tokens of raw ASCII, ANSI color escape codes, and repeated column headers straight into the LLM context window.
Within 3 queries, your model hits its context limit, reasoning degrades, and your API bill explodes.
π Benchmark: Raw Bash vs. Agentic SSH
Letβs use an example we actually check for, and show comparisons between using agentic_ssh and not.
We need to see what containers are running in docker on 3 hosts. Sounds simple, and with agentic_ssh it can be :)
1. The Tool Call Issued by the Agent
When an agent decides to check Docker status across 3 hosts (e.g. cartman, stan, and kyle), it issues a standard tool call to the MCP server.
Agent Tool Invocation
{
"name": "check_docker_status",
"arguments": {
"hosts": ["cartman", "stan", "kyle"]
}
}
Underlying MCP JSON-RPC Wire Request (tools/call)
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/call",
"params": {
"name": "check_docker_status",
"arguments": {
"hosts": ["cartman", "stan", "kyle"]
}
}
}
For local testing or scripts, the equivalent CLI call is
$ agentic_ssh json check_docker_status '{"hosts": ["cartman", "stan", "kyle"]}'
2. The Result Returned to the Agent
agentic_ssh executes the command across all 3 hosts concurrently using connection pooling.
The custom tool definition formats Docker output as {{.Names}}|{{.Status}}|{{.Ports}}:
{
"jsonrpc": "2.0",
"id": 42,
"result": {
"content": [
{
"type": "text",
"text": {
"cartman": {
"status": "success",
"data": "traefik|Up 3 days (healthy)|0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp\npostgres|Up 3 days|5432/tcp"
},
"stan": {
"status": "success",
"data": "nextcloud|Up 12 days|80/tcp\nredis|Up 12 days|6379/tcp"
},
"kyle": {
"status": "error",
"error": "Connection failed: SSH connection timed out after 5 seconds"
}
}
}
],
"isError": false
}
}
3. Why This Structure is Easy for LLMs to Parse
- Per-Host Key Partitioning: Rather than interleaving stdout streams or requiring terminal prompt regexes, each host is isolated in a key (cartman, stan, kyle).
- Explicit Status Tagging (status): Each target carries βstatusβ: βsuccessβ, βerrorβ, or βtimeoutβ. The agent can branch deterministically without inspecting raw error strings.
- Partial Failure Isolation: If one host is down or unreachable (like kyle above), the overall RPC succeeds and returns the working hosts intact while cleanly reporting the failure for the unreachable node.
- Structured Line Delimiters: Because the underlying tool command formats container output with pipe delimiters ({{.Names}}|{{.Status}}|{{.Ports}}), the model can reliably parse container name, uptime, and exposed ports without column misalignment or header confusion.
Without agentic_ssh
Without agentic_ssh, an agent typically relies on a generic local bash execution tool (e.g. execute_command or bash). To query 3 remote hosts, the workflow degrades into either multiple sequential round-trips or brittle shell orchestration.
Here is what that looks like in practice.
Option A: The Sequential Multi-Turn Route (3 Tool Calls)
Most agents default to executing commands one host at a time, creating a multi-turn conversation loop.
Turn 1: Query cartman
The agent issues a bash command:
{
"name": "run_command",
"arguments": {
"command": "ssh -o BatchMode=yes -o ConnectTimeout=5 cartman \"docker ps -a\""
}
}
Raw Output Returned to Agent:
Linux cartman 6.8.0-45-generic #45-Ubuntu SMP PREEMPT_DYNAMIC Wed Sep 11 18:25:23 UTC 2024 x86_64
Welcome to Ubuntu 24.04.1 LTS (GNU/Linux 6.8.0-45-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
System information as of Thu Sep 17 17:22:30 UTC 2026
System load: 0.15 Processes: 182
Usage of /: 34.2% of 48.29GB Users logged in: 1
Memory usage: 42% IPv4 address for eth0: 192.168.1.10
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c8e1a90f23b1 traefik:v3.1 "/entrypoint.sh traeβ¦" 3 days ago Up 3 days (healthy) 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp traefik
4b0098f411cd postgres:16 "docker-entrypoint.sβ¦" 3 days ago Up 3 days 5432/tcp postgres
Turn 2: Query stan
The agent waits for Turn 1 to complete, parses the MOTD and table text, then issues:
{
"name": "run_command",
"arguments": {
"command": "ssh -o BatchMode=yes -o ConnectTimeout=5 stan \"docker ps -a\""
}
}
(Waits another round trip for SSH handshake and response).
Turn 3: Query kyle (Failure / Timeout)
{
"name": "run_command",
"arguments": {
"command": "ssh -o BatchMode=yes -o ConnectTimeout=5 kyle \"docker ps -a\""
}
}
Output Returned to Agent:
ssh: connect to host kyle port 22: Operation timed out
Option B: The Brittle Bash Orchestration Route (1 Tool Call)
To avoid 3 round-trips, a sophisticated agent might try to script concurrency directly in bash:
Tool Call
{
"name": "run_command",
"arguments": {
"command": "for host in cartman stan kyle; do (echo \"=== $host ===\"; ssh -o BatchMode=yes -o ConnectTimeout=5 -T \"$host\" \"docker ps -a --format '{{.Names}}|{{.Status}}|{{.Ports}}'\" 2>&1) & done; wait"
}
}
Raw Output Returned to Agent:
=== cartman ===
=== kyle ===
=== stan ===
ssh: connect to host kyle port 22: Operation timed out
nextcloud|Up 12 days|80/tcp
redis|Up 12 days|6379/tcp
traefik|Up 3 days (healthy)|0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
postgres|Up 3 days|5432/tcp
Key Comparison: Why the Raw Approach Fails or Bloats
| Feature | Raw SSH via Generic Bash | agentic_ssh MCP |
|---|---|---|
| Turns & Latency | 3 sequential round-trips (or complex backgrounded subshell code). | 1 tool call, executed concurrently across all nodes. |
| Connection Overhead | Performs TCP handshake, key exchange (KEX), host verification, and user authentication from scratch for every single command. | Persistent connection pool (auto-reused, auto-closed after 5 min idle). |
| Output Cleanliness | Polluted with MOTD banners, system stats, last login info, and shell warnings. | Zero shell noise; only returns stdout or clean errors. |
| Parsing Reliability | Non-deterministic whitespace tables that wrap on narrow viewports, or race conditions where backgrounded outputs interleave. | Deterministic JSON dictionary mapped per host ({ βcartmanβ: { βstatusβ: βsuccessβ, βdataβ:β¦ } }). |
| Prompt Traps | Can hang the session indefinitely on host key verification prompts (Host key verification failed) or password prompts unless exact -o flags are remembered. | Non-interactive by default, controlled timeouts, and strict error isolation. |
| Token Consumption | High token waste from repeated prompts, tool definitions, MOTDs, and back-and-forth turns. | Minimal token payload directly consumable by downstream reasoning. |
π‘οΈ The Architecture of agentic_ssh
To solve the problems described above, we built Agentic SSHβa dedicated, high-performance Model Context Protocol (MCP) server written in Rust.
Instead of letting agents execute unconstrained shell commands, agentic_ssh acts as a zero-trust cryptographic gateway between the AI and your servers.
βββββββββββββββββββ MCP Tool Call ββββββββββββββββββββββββββ Pre-Warmed SSH βββββββββββββββββββ
β AI Assistant β ββββββββββββββββββββββββ> β agentic_ssh (Rust MCP) β βββββββββββββββββββββββββ> β Remote Server β
β (Claude/Cursor) β <ββββββββββββββββββββββββ β β <βββββββββββββββββββββββββ β β
βββββββββββββββββββ Structured JSON ββββββββββββββββββββββββββ 0ms Latency βββββββββββββββββββ
(185 Tokens) β’ Strict Host Allowlist
β’ Stream Parser & Truncation
β’ Keepalive Connection Pool
β‘οΈ Key Architectural Pillars
1. Cryptographic allow_hosts Guardrails
By default, agentic_ssh operates on a strict Zero-Trust model. Even if your local SSH config lists 50 servers, the AI can only interact with hosts explicitly enumerated in .agentic_ssh.toml:
# Only staging and local nodes are accessible to the AI
allow_hosts = [
"staging-app-01",
"staging-db-01",
"aruba-node"
]
# Production servers are invisible and strictly unreachable
ignore_hosts = ["prod-*", "bastion-primary"]
If the agent attempts to target an unauthorized host, the tool call is rejected locally before a single network packet leaves your machine.
If you donβt specify allow_hosts then agentic_ssh will complain and fail to execute commands, we need to know which hosts we can run on!
2. Pre-Warmed Rust Connection Pooling (0ms Handshakes)
Rather than spinning up and tearing down SSH processes on every command, agentic_ssh maintains an asynchronous in-memory connection pool built on russh and tokio.
- Active Reuse: Consecutive tool calls to the same server execute in < 1ms with zero reconnect overhead.
- Auto-Teardown: Idle sessions automatically close after 5 minutes of inactivity.
3. Squeezing 15,000 Tokens Down to size
AI models donβt need 500 lines of kernel timestampsβthey need actionable status. agentic_ssh parses raw remote system outputs into compact, structured JSON telemetry:
{
"staging-app-01": {
"status": "success",
"data": {
"load_averages": [
0.31,
0.47,
0.4
],
"memory": {
"total_kb": 8122336,
"free_kb": 4254188,
"available_kb": 6668804,
"used_kb": 1453532
},
"disks": [
{
"filesystem": "/dev/nvme0n1p2",
"size_kb": 491678424,
"used_kb": 118604208,
"available_kb": 348079296,
"use_percent": 26,
"mount_point": "/"
}
]
}
}
}
By compressing terminal noise into structured payloads, we slashed prompt token consumption, often dramatically reducing model reasoning latency and operational costs.
4. Interactive Live-Stream TUI (agentic_ssh watch)
When you want to oversee what your AI assistant is doing on your servers in real-time, agentic_ssh includes a high-performance terminal UI built with ratatui:
agentic_ssh watch stan,cartman,kyle "docker ps"
You get a split-pane live stream of active SSH sessions, followed by a summary of each hostβs logs across multiple hosts concurrently. (eg this example would invoke the command docker ps on your hosts stan, cartman & kyle)

π¦ Getting Started in 30 Seconds
agentic_ssh is officially verified on the Anthropic Model Context Protocol Registry and available across macOS and Linux:
1. Install via Homebrew or Cargo
# macOS & Linux (Homebrew)
brew install sandbanks/tap/agentic_ssh # no need for rust to be installed
cargo binstall agentic_ssh # no need for rust to be installed
# Rust toolchain
cargo install agentic_ssh # requires rust toolchain
2. Updates
# macOS & Linux (Homebrew)
brew upgrade sandbanks/tap/agentic_ssh # no need for rust to be installed
# If you use cargo-update:
cargo install-update agentic_ssh
# Or re-install directly:
cargo binstall agentic_ssh
# or: cargo install --force agentic_ssh
3. Auto-Register with Your AI Tools
Run the self-install command to automatically configure Claude Desktop, Cursor, and other MCP clients:
agentic_ssh install
4. Configure Your Allowed Hosts
Create a simple ~/.config/agentic_ssh/config.toml or .agentic_ssh.toml in your project root:
allow_hosts = ["my-dev-box", "staging-cluster"]
π― Final Thoughts
Autonomous AI agents are only as safe as the boundaries we give them. Stop letting agents wander through your ~/.ssh/config unrestricted.
Give them dedicated, high-speed, zero-trust tools that keep your production infrastructure safe and your context windows clean.
- βοΈ GitHub: github.com/sandbanks/agentic_ssh
- π Live Docs & Interactive Demo: sandbanks.tech/agentic-ssh
- π Sponsor on GitHub: github.com/sponsors/sandbanks
Richard Scorer
Independent software engineer building high-performance, sovereign tools and agentic primitives at sandbanks.tech.