<div align="center">
# `shmerm`
**Durable terminal sessions agents and humans share, one turn at a time.**
[](https://www.npmjs.com/package/shmerm)
[](LICENSE)
[](#install)
[](#status)
<sub><i>The Shtateful Agentic Tool Machine. terminal shmerminal! we got this.</i></sub>
</div>
---
## 60 seconds
```bash
$ shmerm run -- python
id: dusty-fern-9c1a url: https://gentle-piano.trycloudflare.com/view/<token>
# turn 1 โ load a dataframe
$ shmerm send dusty-fern-9c1a $'import pandas as pd; df = pd.read_csv("sales.csv")\r'
$ shmerm wait-idle dusty-fern-9c1a
# turn 2 โ query it. (the dataframe is still there.)
$ shmerm send dusty-fern-9c1a $'df.shape\r'
$ shmerm wait-idle dusty-fern-9c1a
$ shmerm tail dusty-fern-9c1a --lines 2
(48291, 12)
>>>
```
> [!NOTE]
> Same Python process. Four separate shell invocations. The dataframe survives between commands because the PTY is alive between commands. Open the URL on your phone to watch it live. The session keeps running after every terminal you have is closed.
---
## How it works
Inputs arrive one at a time. Anyone holding the session ID can take the next turn โ an agent, a shell script, you on your phone. The PTY doesn't know the difference.
```
Time โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโถ
โโโโโโโโโโโ one long-lived session (PTY alive) โโโโโโโโโโโ
โ โ
agent โ shmerm send "step 1" โโโถโ โ
โ โโโ tool prints output โโ โ
agent โ shmerm wait-idle โโโโโโโถโ โโโ (5s of quiet) โโโ โ
agent โ shmerm tail โโโโโโโโโโโโถโ โโโ reads scrollback โโ โ
โ โ
human โ web UI: Message agent โโถโ (lands in inbox) โ
agent โ shmerm inbox โโโโโโโโโโโถโ โโโ sees the message โโ โ
โ โ
agent โ shmerm send "step 2" โโโถโ โ
โ โโโ more output โโ โ
human โ web UI: Type tab โโโโโโโถโ (raw keystrokes) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
[ the agent's process can die and respawn anywhere on the X axis ]
[ the session keeps running ]
```
Three properties make it work:
- **Detached.** The session lives in its own host process, owns its own PTY, outlives whoever started it. Crashes, sleeps, context resets โ the tool keeps its place.
- **Explicit turns.** `shmerm send` is a discrete event, not a typing stream. Agents reason about one move at a time, send it, read what happened. So can humans. The next driver picks up where the last one left off โ they just need the session ID.
- **Idle-aware reads.** `shmerm wait-idle` blocks until the tool has been quiet for N seconds. Better signal than scraping for prompt strings. Robust to slow output. Race-free.
---
## Not a tmux
| | `nohup` | `screen` | `tmux` | `shmerm` |
| ----------------------------------------- | :-----: | :------: | :----: | :------: |
| Survives the launching shell exiting | โ | โ | โ | โ |
| Live web UI, phone-friendly | | | | โ |
| Public HTTPS URL with one flag | | | | โ |
| One-shot CLI per turn (no attach required)| | | | โ |
| `wait-idle` primitive for agents | | | | โ |
| Inbox channel separate from the keyboard | | | | โ |
> [!TIP]
> tmux is for humans. shmerm is for agents who sometimes hand off to humans.
---
## Why this exists
Agents do real work through CLIs now. Claude Code, Codex, Aider, Cursor โ every coding agent worth using drives a shell. But the tools agents invoke have state, and CLIs are stateless from the agent's perspective. Every command is fire-and-forget; any state inside a long-running tool dies the moment the agent's context window clears or its process restarts.
Three problems shmerm ends:
- **Sessions die when the agent's process exits.** Every restart was a cold start. Now the session outlives the agent.
- **There's no way to watch.** The middle was a black box. Now the live terminal streams to a phone-friendly web UI with a kill button always one tap away.
- **There's no way to gently steer.** You either Ctrl-C'd and started over, or you didn't intervene at all. Now the human watching from their phone has a separate channel โ an inbox the agent reads on its next poll. No keyboard fight.
Local-first. Install with npm. No service to operate, no account to create.
---
## The agent loop
The shape is always the same: send, wait for quiet, read, decide, send again.
```
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โผ โ
โโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโ โ
โ send โโโโถ โ wait-idle โโโโถ โ tail โ โ
โ (turn) โ โ (PTY quiet) โ โ (output) โ โ
โโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโ โ
โ โ
โผ โ
โโโโโโโโโโโโ โ
โ inbox โ โ any human
โ check โ โ message?
โโโโโโโโโโโโ โ
โ โ
โผ โ
โโโโโโโโโโโโ โ
โ decide โโโโโ
โ next moveโ
โโโโโโโโโโโโ
```
Five lines of bash:
```bash
ID=$(shmerm run --tunnel --json -- claude code | jq -r .id)
shmerm send $ID 'refactor src/auth.ts to use the sessions module' --enter
while shmerm status $ID --json | jq -e '.status == "running"' > /dev/null; do
shmerm wait-idle $ID --quiet-ms 5000 --timeout-ms 600000 --json
msgs=$(shmerm inbox $ID --json) # any human interjections?
[ "$msgs" != "[]" ] && handle "$msgs"
decide_next_action_and_send $ID
done
```
The loop above can be killed and restarted by a different process at any iteration. The session doesn't notice.
---
## The mobile UI
Three tabs. Not modes you forget you're in โ physical tabs you tap.
| Tab | What it does | Touches PTY? |
| ----------------- | ----------------------------------------- | ---------------------- |
| **Watch** | Read-only stream | No |
| **Type** | Send keystrokes directly to the terminal | Yes โ bypasses agent |
| **Message agent** | Sends a message to the agent's inbox | No |
Pending messages render grey. When the agent reads them, they turn green. If the agent calls `shmerm reply`, a reply bubble appears underneath. The UI keeps the human aware of whether their interjection actually landed.
---
## Architecture
```
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ~/.shmerminal/sessions/crimson-otter-7f3a/ โ
โ meta.json host pid, port, token, status โ
โ host.sock unix socket โ control plane โ
โ scrollback.log PTY output (rolling ~1MB) โ
โ inbox.json human โ agent messages โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โฒ โฒ โฒ
โ โ โ
โโโโโโโดโโโโโโ โโโโโโโดโโโโโโโ โโโโโโโโดโโโโโโโโ
โ host proc โ โ shmerm CLI โ โ web viewer โ
โ (detached)โ โ (any agent)โ โ (any phone) โ
โ โข PTY โ โ send/tail โ โ watch/type/ โ
โ โข HTTP โ โ inbox/ โ โ message โ
โ โข WS โ โ wait-idle โ โ โ
โ โข tunnel โ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโ
```
Per-session host processes, not a global daemon. Killing a session is local; one crashing host doesn't take others down.
PTY is the first backend. Browser sessions, Jupyter kernels, and MCP servers are natural next ones โ same session model, same inbox, same web viewer, different bytes.
---
## Install
```bash
npm install -g shmerm
# optional but recommended for public tunnels:
brew install cloudflared # macOS
# or apt install cloudflared # debian/ubuntu
```
<sub>Requires Node 18+. Works under Bun. PTY support via <code>node-pty</code>.</sub>
---
<details>
<summary><b>CLI reference</b> โ every command and flag</summary>
```
shmerm run [--tunnel] [--json] -- <cmd> [args...] start a session
shmerm list [--json] active sessions
shmerm attach <id> take over (Ctrl-] to detach)
shmerm urls <id> reprint URLs
shmerm send <id> <text> [--enter] write text to PTY
shmerm tail <id> [--lines 100] read scrollback
shmerm wait-idle <id> [--quiet-ms 5000] block until PTY quiet
[--timeout-ms 120000]
[--json]
shmerm status <id> [--json] session metadata
shmerm kill <id>
# agent-only inbox commands
shmerm inbox <id> [--json] read pending, mark delivered
shmerm inbox <id> --watch [--json] long-poll for new messages
shmerm reply <id> <msg_id> "..." attach a reply
```
`run`, `list`, `status`, `inbox`, and `wait-idle` accept `--json` for machine
consumption. The rest write human-readable lines to stderr; only `attach`,
`tail`, and JSON modes write to stdout.
</details>
---
## Status
Early. The substrate works; the surface area is still moving. Issues, ideas, and pull requests welcome.
## License
MIT