Skip to content
manytokensPublic

About

Let your agents on the remote server control your computer via reverse SSH tunnel

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

agent_ssh

Version 0.1.0 · MIT License © 2026 middlered

assh lets an agent on a Linux server work on your computer as if it were sitting at it. Your computer can run Windows, Linux or macOS; Windows is the primary target.

On your computer, assh starts an SSH server for your own user. Your normal OpenSSH client then dials out to the Linux server (the relay), and the reverse tunnel maps that SSH server onto the relay's loopback interface. Anything on the relay can then run PowerShell, move files, forward ports or speak MCP to your computer over plain SSH. No inbound port is opened, no key is copied, and nothing is installed or written on the relay.

your computer                                    relay (Linux)
┌──────────────────────────────┐               ┌───────────────────────────┐
│ assh                         │  ssh.exe -R   │ sshd                      │
│  ├ embedded SSH server  ◄────┼───────────────┼── 127.0.0.1:22222         │
│  │  PowerShell, SFTP, tools  │               │        ▲                  │
│  └ in-memory agent key ──────┼── forwarded ──┼─► SSH_AUTH_SOCK           │
└──────────────────────────────┘     agent     │  agent: assh run '...'    │
                                               └───────────────────────────┘

Platforms

Your computer Commands run in Status
Windows 10 1809+ / 11 PowerShell 7, falling back to Windows PowerShell 5.1 Primary target; covered by the full integration suite
Linux bash (change with --shell) Tested end to end: tunnel, tools, file transfer, interactive login with MOTD and resize
macOS bash (change with --shell) Unit tests in CI on macos-latest; a full SSH QA pass on an arm64 runner with the system Bash 3.2 (tools, files, SCP/SFTP, forwarding, MCP, reconnect)

These features exist on Windows only:

  • clip get/set
  • the Git Bash and WSL entry points (assh bash on Linux and macOS is the native bash)
  • the crash-recovery cleanup of escaped processes. It addresses a Windows job-object gap; on Unix, process groups end whole trees.

The relay is always a Linux (or other POSIX) server.

Quick start

Requirements:

  • On your computer: Go 1.26.6+ to build, and the system OpenSSH client 8.7+. Windows also needs PowerShell 7 or Windows PowerShell 5.1.
  • On the relay: OpenSSH with TCP and agent forwarding allowed, a POSIX login shell, and bash, base64 and gzip. Python 3.9+ is needed only for assh put/get/sync.
go build -o bin/assh.exe ./cmd/assh       # Linux/macOS: go build -o bin/assh ./cmd/assh

# Once: make sure non-interactive key login to the relay works.
.\bin\assh.exe login user@relay

# Connect. You land in a Bash shell on the relay.
.\bin\assh.exe user@relay -p 52000 -o ServerAliveInterval=3

In that relay shell, the assh function reaches back to your computer:

assh run 'Get-Location; Get-Date'
assh put ./build.zip 'D:/drop/build.zip'
assh status

Exiting the relay shell stops everything: the tunnel, the embedded server and every process it started.

How it works

  • One process, one session. assh user@relay runs the embedded SSH server, the reverse tunnel and your interactive login in a single process. start, run and stop provide a background service instead.

  • Your OpenSSH, your config. The tunnel and the login use the system ssh with your ~/.ssh/config, keys, ssh-agent and jump hosts. The tunnel runs with BatchMode=yes and StrictHostKeyChecking=yes, so it never prompts and never accepts an unknown relay host key.

  • In-memory authorization. Each start creates an Ed25519 key that exists only in assh's memory. It is offered to the relay through a dedicated forwarded agent, and the embedded server trusts it. The relay never sees a private key, and authorization ends with the process. authorize FILE adds further keys for agents elsewhere.

  • Strict in both directions. The relay side verifies your computer's persistent host key, which is passed on the command line (KnownHostsCommand), so no known_hosts file is involved.

  • Readiness. Before the tunnel counts as online, the relay checks two things in parallel. ssh-keyscan must find your computer's host key behind the forwarded port, and the forwarded agent must answer ssh-add -l. Without ssh-keyscan, a full login to the in-process ping tool is used.

  • Native login. ssh runs directly on your terminal, so resizing, wrapping, full-screen programs and Ctrl+C behave exactly as with plain ssh. The relay environment arrives as a compressed rcfile inside the session command (about 10 KB). Nothing is typed into the shell. Bash 4+ reads the rcfile from a pipe. Bash 3 (as on macOS) cannot, so it gets a private temporary file that deletes itself before any profile runs. The rcfile:

    1. prints /run/motd.dynamic and /etc/motd;
    2. loads /etc/profile and the first of ~/.bash_profile, ~/.bash_login and ~/.profile, once;
    3. defines the helpers.

    Last login is not shown, and the shell is interactive rather than a login shell; logout and ~/.bash_logout still work.

  • Reconnects. A lost tunnel is retried with backoff (1 s doubling to 30 s, with jitter). The relay port is kept, so the helpers stay valid. With the default --remote-port 0, the first free port from 22222 to 22285 is used.

On the relay

The login shell defines assh and asscp, plus these variables. Agents started from that shell inherit them.

Variable Meaning
ASSH_HOST, ASSH_PORT, ASSH_USER Where your computer's SSH server is reachable on the relay
ASSH_HOST_KEY Its host key, for strict verification
ASSH_SSH_COMMAND, ASSH_SCP_COMMAND Complete ssh/scp prefixes with port and host verification
ASSH_CONNECTION_JSON host, port, user, host_key, target_os as JSON
ASSH_AGENT_HELP The capability hint shown at login
SSH_AUTH_SOCK The forwarded agent socket (set by OpenSSH)

Calls from one shell share a single SSH connection through OpenSSH ControlMaster. The control socket goes in $XDG_RUNTIME_DIR when that directory is yours and short enough for a socket path; ASSH_NO_MUX=1 disables sharing. Processes outside this shell can use the relay_command printed by status. Re-read it after a reconnect, because the agent socket changes.

Tools

Command Purpose
assh run [--timeout 30s] [--json] [--cwd DIR] 'script' Run a script: PowerShell on Windows, bash on Linux and macOS
assh bash [-lc 'script'] Git Bash on Windows (never the WSL bash.exe shim), native bash elsewhere
assh wsl [-d DISTRO] [-u USER] [--cwd DIR] -- ARGS Run inside WSL (Windows only)
assh put SRC DEST / assh get SRC DEST Verified, resumable file or directory transfer
assh sync SRC DEST [--exclude PATTERN] Upload only changed 1 MiB blocks; extra target files are kept
assh fs ls|stat|hash|find PATH --json Structured filesystem queries
assh watch DIR Recursive change events as JSON lines
assh session new|send|read|attach|close A long-lived shell that keeps cwd, variables and modules
assh job start|log|wait|kill|list Background jobs that survive the calling SSH connection
assh info, assh ps, assh ports Environment, processes and listening ports as JSON
assh status Tunnel phase, reconnects, uptime, last error, metrics, live sessions and jobs
assh clip get|set Plain-text clipboard (Windows only; needs --allow-clipboard)
assh mcp MCP server over SSH stdio
assh dry-run OP ... / --dry-run Show what an operation would do without doing it
assh help, assh capabilities --json Discovery
asscp SRC DEST scp with the right port, host key and drive-path conversion

assh -N -L 127.0.0.1:8080:127.0.0.1:3000 and assh -N -R ... forward ports. Any other first argument is passed to ssh as is, so assh 'Get-Process' runs a plain exec.

Execution

On Linux and macOS, scripts run as bash -c, and exit codes and timeouts work as usual; process groups end whole trees. The rest of this section describes Windows.

  • PowerShell setup. Scripts run as a fresh pwsh -NoProfile -NonInteractive with UTF-8 I/O and $ErrorActionPreference='Stop'. The script travels in a temporary file that is deleted once read, so length is unlimited, and syntax errors report your own line and column.
  • Exit code. An explicit exit N wins. Otherwise the last statement decides: a failing native command passes its $LASTEXITCODE, a failing cmdlet gives 1, and success gives 0. Codes are signed, so exit -1 is -1.
  • --json. Returns stdout, stderr, exit_code, duration, timed_out and truncated, keeping the last 4 MiB per stream. Non-UTF-8 output adds binary and a byte-exact stdout_base64.
  • Timeouts and kills. --timeout, job kill, session close and a dropped client end the whole process tree. That includes processes started by a Microsoft Store pwsh.exe, which escape the job object. If assh itself is killed, the next start ends the processes it recorded in processes.json, matched by PID and creation time.
  • Sessions. A multi-line session send gets the blank line that PowerShell needs to run the block.
  • No translation. Scripts are not translated between PowerShell and Bash.

Files and paths

  • Tool paths. On Windows, put/get/sync/fs accept D:/x, /D:/x and /d/x. Relative paths start at --workdir, which defaults to the directory assh was started in.
  • SFTP paths. For a Windows computer, plain SFTP and scp understand drives only as /D:/x. asscp converts D:/x, D:\x and /d/x for you; raw scp, sftp and $ASSH_SCP_COMMAND do not.
  • How transfers work. Files move in 1 MiB SHA256-checked blocks through a .part file, which is renamed once the whole file verifies. Interrupted transfers resume, an identical target is left untouched, and directories stream as tar. All RPCs of one transfer share one SSH connection, and failed connections are retried.
  • Names Windows would mangle are refused: a : (alternate data stream), *?"<>|, a trailing dot or space, and device names such as CON or nul.txt.
  • sync. It honours the source .gitignore and --exclude patterns, including *, ?, **, !, anchors, character classes and escapes. --text converts CRLF to LF; otherwise bytes are kept exactly.

MCP

assh mcp speaks newline-delimited JSON-RPC on stdout, with no banner. It offers the tools command, run, put, get, ls, hash, info, fs, session, job, ps, ports, clip and status.

  • Concurrency: calls run concurrently.
  • Output: each stream is capped at 8 MiB and flagged truncated. stdout_base64 is present only for binary output.
  • put: requires stdin_base64.
  • Cancellation: a cancelled call is killed and, per the spec, not answered.
{"mcpServers":{"my-pc":{"command":"bash","args":["-c","assh mcp"]}}}

On your computer

assh user@relay [options]              # tunnel + interactive login, one process
assh start  [options] user@relay       # background service only
assh run    [options] user@relay       # service in the foreground (Ctrl+C stops)
assh status [--json]                   # state, metrics, relay_command
assh stop
assh login  user@relay                 # plain interactive ssh, e.g. to accept the host key
assh authorize [FILE]                  # trust extra agent keys (default ~/.ssh/authorized_keys)
assh exec [--json] 'script'            # run a tool locally
Option Meaning
-p, -i, -F, -o Passed to OpenSSH (port, identity, config, any option)
--remote-port N Fixed relay port; 0 (default) picks 22222-22285
--workdir DIR Initial directory for commands and relative paths
--shell PATH Default shell (pwsh/powershell on Windows, bash on Linux)
--state-dir DIR Profile directory; one connection per profile
--allow-clipboard Enable clip get/set
--no-audit Do not write audit.jsonl
--events-file PATH, --event-webhook URL Where connection events go
--connect-timeout N OpenSSH connect timeout in seconds

The profile lives in %APPDATA%\agent_ssh on Windows, ~/.config/agent_ssh on Linux, and ~/Library/Application Support/agent_ssh on macOS. It holds:

  • the persistent host key, which is your device identity
  • extra authorized_keys
  • assh.log
  • events.jsonl with connection state changes
  • audit.jsonl
  • processes.json
  • job logs under jobs/

Every remote operation is appended to audit.jsonl, rotated beyond 10 MiB:

  • connects and disconnects
  • each tool call with its arguments truncated to 512 bytes
  • each plain exec with its script

Each entry carries the exit code, error and duration. Input data is never logged. SFTP, scp and port forwards appear only as their connection's connect and disconnect.

Security notes

  • Agents act as your local user with your full permissions. --workdir is a starting directory, not a sandbox, and the SSH user name is only a label.
  • Only the relay account that holds the forwarded agent (or a key you authorize) can connect, and only through the relay's loopback interface.
  • Nothing persistent is written on the relay; only a Bash 3 relay briefly holds the self-deleting rcfile. The relay still keeps its normal SSH logs and shell history.
  • The temporary agent socket disappears with the session. assh connection --write FILE writes an explicit snapshot when you want one. It is not a credential and goes stale after a reconnect.

Limits

  • No Windows service, start-at-boot or self-update. A crashed or logged-off session does not restart itself.
  • Jobs and sessions end with the service that owns them and are not resumed after a restart.
  • Directory transfers are committed file by file, not atomically. ACLs, owners and modes are not preserved.
  • Legacy scp -O is not supported. Use the default SFTP mode.

Development

go build ./... && go vet ./... && go test -timeout 90s ./...
GOOS=windows go build -o bin/assh.exe ./cmd/assh

tests/integration/windows.ps1, run in PowerShell 7 on Windows, builds everything and starts a disposable Docker OpenSSH relay. It then exercises the real path Windows client → relay → embedded SSH:

  • PowerShell 7 and 5.1, exit codes and process-tree cleanup
  • SCP/SFTP, forwards, reconnection after a relay restart
  • the native login (MOTD, profile loaded once, no bootstrap leaking)
  • the tools over SSH and MCP

It uses only credentials under work/integration and removes its container afterwards.

Package Role
internal/app CLI, service lifecycle, local control API
internal/tunnel OpenSSH arguments, readiness, reconnects, relay login rcfile and helpers
internal/localssh Embedded SSH server (sshd-lite), exec, audit hooks
internal/reverseagent In-memory agent over a unix socket or a per-user named pipe
internal/execsvc Execution, PowerShell wrapper, PTY/ConPTY, sessions, jobs
internal/filesvc File tools, block transfer, tar, sync, watch, Python relay client
internal/tools, internal/toolrpc Tool dispatch, argv protocol, MCP
internal/platform Job objects, process-tree kill and tracking, profile lock
internal/state, internal/audit, internal/sysinfo, internal/telemetry Keys and state files, audit log, system queries, events and metrics

Dependencies: github.com/jpillora/sshd-lite at commit 42b505a0cdf50f782210060f9d836974d291dcc3 (MIT), and upstream github.com/jpillora/sftp v1.13.15 (BSD), unpatched.

About

Let your agents on the remote server control your computer via reverse SSH tunnel

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages