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:
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
commandprofiles. For bastion menus / OTP / multi-level hops, you point acommand-type profile at anexpect(orsshpass, orssh -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--jsonoutput) 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
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. -
commandprofiles run as the daemon user. Only run trusted commands, and protectSHELLINK_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
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)