agentreach README

The project's README from github.com/bojieli/agentreach. As one markdown file: README.md.

AgentReach

Point your coding agent at any box you can SSH into. The server never gets your agent.

CI Go 1.25.8+ License Zero dependencies

The coding agent runs on your machine. reach catches its Bash, Read, Write and Edit calls before they touch the local disk and runs them over one ssh connection on the target instead. The API key and the conversation never leave your machine, and the target needs nothing but the sshd it already runs.

Why this exists

Think about the last time you SSHed somewhere to fix something. A build box, a client's staging VM, a container you spun up this morning and will throw away tonight. You'd get through it faster with an agent riding along.

So you install one there. That's a 300 MB Node binary on a machine that isn't yours, plus an API key pasted in so the thing can actually do anything. Now your key lives on a box where other people have root, in a shell history you won't clear, in an env file you'll forget about by Friday. If that server gets popped next month, your key goes with it.

Dev containers are the same story on repeat. New branch, new container, and each one wants its own copy of the agent. Baking it into the Dockerfile bloats an image that should have stayed small. Mounting it in from the host works right up until the morning it doesn't.

What reach actually does

The agent stays where it is. What moves is the work it hands off. An agent only touches a machine two ways — it runs shell commands, and it reads or writes files — and reach gets in the middle of both, on your side, before either one reaches your disk.

Commands. reach takes over whatever the harness spawns as a shell, unwraps the command, runs it over ssh on the target, and hands back stdout and an exit code. As far as the model can tell, it called Bash. Every harness offers some door in — a seam, a place where behaviour can be changed without editing the binary:

Files. Depends on the mode:

What this isn't. No syscall tracing. No filesystem in any of it: no SSHFS, no FUSE, no mount, no file synchronisation between the two machines. reach sits at the seam where the agent hands work to the operating system, one request and one response at a time.

HOW-IT-WORKS.md walks a single tool call through the whole path, in figures.

What it looks like

$ reach build-box claude
probing build-box ...
session "build-box" -> build-box:/home/you
  target   Linux x86_64
  fileops  pipe (negotiated; nothing written to the target)
  search   ripgrep (fast, structured)
  connect  multiplexed (one authenticated connection, reused)
reach: Claude Code -> build-box:/home/you (bash runs on the target)

> what's eating disk on this box?

Claude Code is running locally. The df and du and grep it decides to run happen on build-box. Files it reads come from build-box. If it rewrites a config, the new bytes land on build-box. Your own filesystem is untouched unless you specifically ask for something local.

Everything it did is on the record:

$ reach log
WHEN      ACTION  STATUS  DETAIL
14:02:11  exec    ok      df -h /
14:02:19  exec    ok      du -sh /var/log/*
14:02:30  write   ok      /srv/app/logrotate.conf
14:02:31  exec    exit 1  systemctl reload rsyslog

Install

go install github.com/bojieli/agentreach/cmd/reach@latest

Pre-built binaries are on the releases page. From source:

git clone https://github.com/bojieli/agentreach
cd agentreach
make install

There is also a container image on GitHub Packages, for when the machine you drive agents from is itself disposable — a CI job, a devcontainer, a jump box:

docker run --rm -v "$HOME/.ssh:/root/.ssh:ro" ghcr.io/bojieli/agentreach version

It carries an ssh client and the helper binaries for every target platform. Your keys and known_hosts have to be mounted in; there is nothing useful to bake into an image, and reach will not invent credentials it was not given.

The only thing reach needs is the ssh binary you already have. It shells out to that rather than speaking SSH itself, so ProxyJump, IdentityFile, Match blocks, hardware tokens and 2FA all keep working the way you've configured them.

You can sit in front of Linux, macOS or Windows. Targets can be anything POSIX you can reach over user@host:path, ssh://, docker:// or podman://.

Quick start

Name a machine, then name an agent:

reach build-box claude                        # an ssh_config alias; work where a login lands
reach build-box:src/app claude                # ... or in a directory you name
reach client-box:/srv/app codex               # somebody else's server

reach opens one multiplexed SSH connection, asks the host what it can do, and launches the agent locally with its shell quietly redirected there. You talk to it like you always do. Nothing is written to that host unless you ask for the helper tier by name, and reach doctor tells you exactly what was found, which tier got picked, and whether anything is sitting on the target.

Any command takes a target, not just the agents:

reach build-box exec -- go test ./...
reach build-box doctor

Each target gets its own session, named after it, so several machines can be open at once — one terminal per box, and no bookkeeping to keep them apart:

$ reach status
NAME            TARGET                    MODE  FILEOPS  CWD
build-box-app   build-box:/srv/app        exec  pipe     /srv/app
client-box-app  client-box:/srv/app       exec  posix    /srv/app

A second command against a target reuses that session instead of probing again, so running one costs no more than typing it.

When you're finished:

reach down build-box-app

That closes the connection and leaves nothing behind.

For a session meant to outlive one agent — several commands, several days, a name you chose — bind it up front and address it by name:

reach up build-box:/srv/app --name build
reach claude --session build
reach down build

Agents that work today

Agent Command Where reach gets in Status
Claude Code reach claude CLAUDE_CODE_SHELL_PREFIX, a hook it already ships verified end-to-end (2.1.233)
Codex reach codex its remote-environment protocol, which carries every tool it has verified end-to-end (0.148.0)
Kimi Code reach kimi a patched npm bundle plus KIMI_SHELL_PATH verified (0.37.2)
opencode reach opencode custom tools that shadow the built-in bash and read verified (1.18.18)
Goose reach goose GOOSE_SHELL, a documented override verified
Crush reach crush its own server mode, run on the target verified
Gemini CLI reach gemini a bash earlier on PATH, plus excludeTools for the rest verified
Grok Build reach grok $SHELL, plus an agent profile that removes the file tools verified (1.0.5)

Those fall into three groups, plus one exception, and the group decides how much of the agent survives the trip.

Codex and opencode are the clean ones. Both document a way to change the machine their tools act on, so reach answers at the other end and the model keeps every tool it started with. Codex is the best fit reach has, because it has no file tools at all: apply_patch and the rest run as commands inside exec_command, so intercepting that one protocol leaves nothing behind to deny.

Claude Code, Goose and Kimi hand over the shell and only the shell. Their file tools call straight into Node's fs or Rust's std::fs, so reach denies them and the agent works through its shell instead, or in Claude Code's case mirrors them if you ask for mirror mode.

Gemini gives you no hook at all, so reach wins the PATH lookup for bash and hides the rest of the built-ins through excludeTools in a managed settings.json.

Crush is the exception to the nothing-installed rule. Its server mode is exactly the seam reach wants, and reach crush starts crush server on the target and tunnels the client to it, which means crush itself has to already be there.

Whichever group you land in, nobody logs in again. Subscription logins, OAuth tokens and API keys keep working exactly as they do now, because reach never touches them.

Commands

reach <target> <cmd>    bind a session to the target and run the command there
reach up <target>       bind a session to a target and probe what it supports
reach down [session]    close a session and leave no trace
reach status            show active sessions
reach doctor            explain what a target supports, and why
reach log               what reach has run and changed on the target
reach exec -- <cmd>     run a one-off command there
reach fs read <path>    work with remote files directly
reach helper uninstall  remove anything reach put on the target

Targets look like this:

build-box                      an ssh_config alias, or a name in /etc/hosts
[user@]host:path               scp's spelling
ssh://[user@]host[:port]/path  OpenSSH's URI spelling, when a port is needed
docker://container/path
podman://container/path
local:///abs/path              (this machine, mostly for testing)

Paths mean what the tool each form is borrowed from means by them. reach invents nothing here, because someone who knows what scp box:app copies should not have to learn a second convention to use it.

For a host, a path is relative to where a login lands unless it starts with a slash, and a leading ~ is the target's to expand:

build-box:app          ~/app on build-box
build-box:/srv/app     /srv/app
build-box:~deploy/app  deploy's home, wherever the target says that is
ssh://build-box/app    ~/app — same rule, same as scp
ssh://build-box//app   /app

The second slash in the URI form is not a typo. OpenSSH's own parser treats the slash that ends the host as a delimiter, so what reaches scp and sftp as the path has no leading slash and a second one is what makes it absolute; curl reads sftp:// URLs the same way. Note that git reads ssh:// the other way round, absolute from the first slash. Where the two disagree, reach follows the ssh binary it is shelling out to.

For a container, docker cp's rule holds instead: paths are relative to the container's root, so the leading slash is optional and docker://c/srv/app and docker://c//srv/app are the same directory.

Leave the path off any of them — build-box, ssh://build-box, build-box: — and the session works wherever a login on that machine lands, which reach asks the machine for rather than guessing. A bare word is read as a host only when your own ssh configuration or hosts file names it, or when it is an address or a dotted name; anything else is reported as a mistyped command, which is what it almost always is.

Session flags go between the target and the command, and everything from the command onwards belongs to the command:

reach build-box --mode mirror --fresh claude --resume

A flag reach does not define is the harness's, and reach hands it over untouched — no -- needed:

reach build-box claude --dangerously-skip-permissions
reach build-box codex --model gpt-5-codex

-- is still there for the one case that needs it: a flag whose name reach also defines. reach claude --session build picks reach's session, and reach claude -- --session build gives the flag to Claude Code instead. reach claude --help lists which names are reach's for that harness.

Why not SSHFS, an MCP server, or just SSH?

Four things look like they should work. Each breaks somewhere specific.

A mount turns one tool call into a stream of syscalls through the kernel's VFS and a FUSE daemon, so a stalled link parks the agent in uninterruptible sleep with no error it can see. reach answers each tool call with one request and one response over ssh, so a timeout comes back as a tool error the model can read, retry or route around. Nothing is mounted and no kernel is involved.

How it works

Three layers, and only the top one knows which agent you're running.

harness  (claude · codex · kimi · opencode · goose · crush · gemini · grok)
    │     native tool calls, no new tools in the model's view
adapter   one per harness, config or plugin, never a fork
    │
reach     session state · cwd · capability probe
    │     file-op tier: posix · pipe · helper
    │  ssh · docker · podman · local
target    stock sshd, nothing installed

There's no daemon. A session is just a file on disk. SSH's ControlMaster already multiplexes the connection at four to five times the speed of reconnecting, so a daemon would buy lifecycle complexity and nothing else.

By default nothing gets written to the target. Two of the three file-operation tiers touch its disk zero times, and those two are the only ones reach will choose on its own. The third uploads a small helper, and only if you ask for it by name.

tier needs on target writes to target chosen automatically
posix a POSIX shell nothing yes, as fallback
pipe python3 nothing yes, preferred
helper can run an uploaded binary one cached binary never

TRANSPORTS.md has the numbers, measured over real links rather than loopback. HOW-IT-WORKS.md follows a single Bash call all the way down and back — the envelope reach takes apart, how cd survives between calls, and where the exit status hides.

Choosing a mode

exec is the default, and it's the one to stay on unless you have a reason not to. --mode mirror earns its keep during heavy editing sessions, when you'd rather use Claude Code's native file tools than push every change through a shell redirect:

reach build-box:/srv/app --mode mirror claude

Mirror keeps a content digest for each file it hands over, so if the file changed on the server between the read and the write, the write is refused rather than clobbering someone else's work. Go in knowing what it is, though: reads can be a little stale, and it copies on demand rather than syncing. For shell-shaped work exec is simpler and has no staleness window at all.

Security

reach assumes the target might be hostile, and the design reflects that.

Your credentials never make the trip. The agent, its API key and the whole conversation stay on your machine.

SSH agent forwarding is off and stays off. A forwarded agent socket on a hostile server lets whoever controls that server authenticate as you everywhere else you can reach, which is a much bigger blast radius than the session you thought you were opening. reach never enables it and there is no flag that does.

Nothing is installed by default. Tier 0 needs a POSIX shell and writes nothing at all.

The agent's shell snapshot gets stripped out of forwarded commands. Claude Code's command envelope sources a file from your home directory, and shipping that to the server would hand over your username and directory layout for no benefit.

What reach can't do anything about is prompt injection. Content from the target flows into the agent's context, and a compromised server can absolutely try to talk to your agent. Keep secrets out of the agent's local environment, and read anything it writes to your local disk during a remote session. The full threat model is in docs/SECURITY.md.

Docs

HOW-IT-WORKS.md the implementation in figures, and what reach is deliberately not
ARCHITECTURE.md how the pieces fit, and which alternatives got thrown out
TRANSPORTS.md the three file-operation tiers, benchmarked on real links
RESEARCH.md what each agent does internally, with transcripts
SECURITY.md threat model, and what reach won't save you from
WINDOWS.md running reach from Windows
CONTRIBUTING.md the standard of evidence, and the design rules
harnesses/ per-agent notes: the seam, verified versions, known limits

Development

make check        # vet plus unit tests. No network, no API key.
make integration  # every file-operation tier against a real sshd
make bench        # what each tier actually costs
make conformance  # do the agent seams still have the shape reach expects?
make e2e          # real agents against a real target (spends tokens)

make integration starts an sshd owned by your own user on a high port. No root, no Docker, no outbound network. If you'd rather test against a box you already have, set REACH_TEST_SSH_HOST=my-box.

Status

The Claude Code path is verified end-to-end against a real agent on real hosts across three continents. The file-operation layer has been through three tiers, six hosts, three userlands and a fuzzer, and it's solid. Every other agent is exactly as far along as its notes say it is.

Interfaces may still move before 1.0. Three things won't: the file-operation protocol, the session format, and the promise that nothing lands on your server unless you asked for it.

Acknowledgements

reach exists because Zihan Zheng (@zzh1996)

agentreach (bojieli/agentreach)

Searching Database…

What is this?AgentReach (reach) is a CLI that lets you point your local coding agent (Claude Code, Codex, etc.) at any machine you can SSH into. The agent itself keeps running on your laptop, but every shell command and file read/write it makes is transparently redirected to the remote box over SSH, so your API key and the agent binary never touch the server.
Is this a daemon running on the server, or a local CLI tool?It is a local CLI tool with no background daemon; a session is just a file on disk, and SSH's own ControlMaster handles connection multiplexing.
How do I get it running against a remote box?Grab a pre-built binary from the releases page, build from source, or use the container image on GitHub Packages, then run something like 'reach build-box claude' to open an SSH session and launch Claude Code locally with its shell redirected there.
What does the target machine need installed?Nothing special — just an SSH server and ideally python3 for the preferred file-op tier; reach itself only needs the ssh binary already on your local machine and shells out to it, so ProxyJump, IdentityFile and 2FA keep working as configured.
Do all agents keep their full toolset when redirected?Only Codex and opencode expose a clean seam that keeps every tool intact. Claude Code, Goose and Kimi hand over only their shell; their native Read/Write calls get disabled (or mirrored, for Claude Code) since those tools call the filesystem directly with no redirect point. Crush is an exception and actually needs its own binary present on the target.
Can I run reach from Windows, or only Linux/macOS?You can run reach from Linux, macOS or Windows, and target anything POSIX reachable via user@host:path, ssh://, docker:// or podman://.
Does my API key or conversation ever reach the remote server?No. reach assumes the target could be hostile: your credentials, API key and the whole conversation stay on your local machine, and SSH agent forwarding is permanently disabled so a compromised target can't impersonate you elsewhere.
Which coding agents does reach currently support?Claude Code, Codex, Kimi Code, opencode, Goose, Crush, Gemini CLI and Grok Build, each wired up through a different harness-specific seam like CLAUDE_CODE_SHELL_PREFIX or GOOSE_SHELL.
What's the difference between exec mode and mirror mode?exec mode (the default) turns off the agent's native file tools so everything goes through the remote shell; mirror mode, available for Claude Code, keeps native Read/Write/Edit working by fetching and pushing files over the same SSH connection, refusing a write if the file changed on the server since the last read.
Why not just mount the target with SSHFS or run an MCP file server?SSHFS/FUSE can hang a tool call forever if the link stalls, with no error the model could retry, and an MCP server exposes odd tool names like mcp_remote_readfile instead of the Read the model expects. reach avoids both by intercepting the agent's existing Bash/Read/Write calls transparently.
Does reach install anything on the target machine by default?No. By default it picks a tier that writes nothing to the target — a plain POSIX shell or python3 for file ops — and only uploads a small helper binary if you explicitly request the helper tier by name.
Creator
bojieli
Size
1.2 MB
Stars
179
Age
2mo
Commits · 90 Days