DEV Community

jie123108
jie123108

Posted on

I built SSH session middleware so AI agents can work behind jump hosts

I build a lot with AI coding agents these days. Writing code, editing docs, poking at web pages — an agent can mostly handle it. The place it consistently falls apart is servers.

Most of the machines I care about sit behind one or more bastion / jump hosts, with menus, OTP prompts, and the occasional legacy login flow. An agent can't just ssh user@host its way in, so the automation breaks right at the boundary. And debugging is worse: I end up logging in by hand, copying a log down to my laptop, pasting it into the agent, getting back a list of steps, then going back to the server to run them. Over and over.

So I built Shellink: a local daemon that owns SSH and local PTY sessions, with one stable CLI (plus a TUI, a Web UI, and HTTP/WebSocket APIs) on top. The idea is a "session middleware" that both a human and an agent can drive.

Repo (MIT): https://github.com/jie123108/Shellink

Here's an agent hopping through a jump host and working on the target server, entirely through the Shellink CLI:

An AI agent hops through a jump host and runs commands on the target server

What it actually does

  • SSH and custom command sessions. Connect directly over SSH, or run any command/script inside a PTY.
  • Multi-hop login via command profiles. For bastion menus / OTP / multi-level hops, you point a command-type profile at an expect (or sshpass, or ssh -J) script. Shellink itself does not log in for you — it manages the session after you're in.
  • File transfer over the existing PTY, not SFTP. Upload/download keep working through a multi-hop, expect-driven login where you have no direct SFTP/SCP endpoint. This is the feature I needed most; it's what killed the "copy the log down by hand" loop.
  • A real session state machine. CONNECTING / OUTPUTTING / WAITING_INPUT / IDLE / DISCONNECTED, exposed as machine-readable output, so an agent can decide whether to run the next command, send input, wait, or hand off — instead of guessing from scraped text.
  • AUTO vs MANUAL mode. The agent works in AUTO; a human can grab the terminal for OTP entry or anything sensitive, then hand it back. Session I/O is retained as history for audit/replay.
  • An agent skill (shellink-cli). The agent just calls the CLI (stable --json output) instead of reimplementing SSH/jump-host logic.

A quick taste of the CLI

# create a profile without leaking credentials into shell history
shellink profile create --input - --json <<'JSON'
{"name":"local-shell","connectType":"command","command":"/bin/sh"}
JSON

shellink session create --profile <profile-id> --json
shellink session exec <session-id> --command 'uname -a' --json
shellink session download <session-id> --path /var/log/app.log --output ./app.log --json
shellink session close <session-id> --json
Enter fullscreen mode Exit fullscreen mode

The interesting technical bits

The transfer-over-PTY part was the fun/hard one. When your only channel to a box is a PTY that went through two bastions and an expect script, you don't get SFTP. So transfers are encoded over the same terminal stream and reassembled on the other side, which means the exact same code path works for a direct SSH box and a three-hop legacy one.

The state machine matters more than it sounds. The difference between "the command finished" and "the shell is waiting for input" is what lets an agent act instead of sleep-and-hope. Shellink derives state from a quiet-period heuristic plus prompt detection and exposes it explicitly.

Stack: TypeScript, Node 22, Fastify, ssh2, node-pty, SQLite (better-sqlite3 + Drizzle), MessagePack + Zod on the wire, Vue 3 + xterm.js for the Web UI. Single-file Bun binaries for macOS/Linux.

Honest limitations

  • It doesn't do the login for you. Multi-hop / menus / OTP are your expect (or ssh/sshpass) script; Shellink manages what happens after.
  • command profiles run as the daemon user. Only run trusted commands, and protect SHELLINK_TOKEN.
  • Letting an agent touch production is genuinely risky. I'd point it at read-only/inspection tasks, or non-prod, first.
  • No Windows yet (macOS/Linux binaries only).

Try it

curl -fsSL https://raw.githubusercontent.com/jie123108/Shellink/main/install.sh -o /tmp/shellink-install.sh
cat /tmp/shellink-install.sh   # read before you run it
sh /tmp/shellink-install.sh
shellink cli
Enter fullscreen mode Exit fullscreen mode

What I'd love feedback on

If you live behind bastions: does the command + expect approach actually cover your login chain, or does your setup have something it wouldn't survive? And if you wire up agents: is the CLI/--json surface enough, or what's missing?

Repo again: https://github.com/jie123108/Shellink — issues and PRs welcome.

Top comments (0)