Content
# stitchpad
<img width="1672" height="941" alt="640f76be-bf59-4d27-8261-c8466fdca162" src="https://github.com/user-attachments/assets/64024299-f882-47bf-9675-4aa768828c7d" />
**A cross-communication channel for CLI coding agents that's just a markdown file.**
`stitchpad.md` is a self-describing markdown file. The roster of who's in the
room lives *inside the file* as a fenced ` ```roster ` block. You talk to a
teammate by writing a line that starts with `@their-name`. When that teammate's
agent next finishes a turn, its **runtime wake hook** reads the pad, sees the new
`@mention`, and feeds it back in as the agent's next turn — so it picks the
message up and replies. Works for Claude Code, Codex, and pi today; any runtime
with a turn-end hook is a one-file adapter away. Every message is one commit in
an isolated git repo, so the whole conversation has blame and diff. A TUI renders
it live like a chat client.
The simplicity is the whole point: **it's a markdown file + a wake hook.** Open the
file and you see both who's in the room and the entire conversation. Any agent
joins by adding one line.
```
stitchpad.md (the bus — roster lives inside it)
│
├── stitchpad MCP server ← agents connect here; `join` registers their
│ tools: join · say · read · who name + runtime (no wake itself)
│
└── runtime wake hook (per agent)
at turn-end → `stitchpad wake @me` → any new mentions become
the agent's next turn (claude/codex Stop hook · pi extension)
```
## The wake: native runtime hooks
Every modern coding agent fires a hook when it finishes a turn — Claude Code and
Codex both have a **Stop hook** with an identical contract, and pi has an
**`agent_end`** extension event. stitchpad hangs one tiny script on that hook.
When the agent goes idle, the hook runs `stitchpad wake <me>`, which prints any
pad messages addressed to `@me` since the last drain. If there are some, the hook
tells the runtime "don't stop — treat this as a new prompt," and the agent reads
and replies. Nothing new → it stops normally, no model turn burned.
One brain, three adapters: claude (Stop hook), codex (the *same* Stop hook
script), and pi (extension) all shell out to the **same** `stitchpad wake`
command. The only per-runtime part is how the result is fed back in.
## Quickstart
Once per machine — install, add the MCP server, wire the wake hook:
```bash
# 1. install: symlinks the CLI/TUI onto PATH, points ~/.stitchpad at this checkout.
./tool/install.sh
# 2. add the stitchpad MCP server (identity + talk) to each runtime:
claude mcp add stitchpad --scope user -- node ~/.stitchpad/mcp/server.mjs
# Codex — add to ~/.codex/config.toml:
# [mcp_servers.stitchpad]
# command = "node"
# args = ["/Users/you/.stitchpad/mcp/server.mjs"]
# pi — pi install ~/.stitchpad/adapters/stitchpad (also gets the wake; see below)
# 3. wire the wake hook (claude/codex only — pi's extension covers both):
# Claude ~/.claude/settings.json · Codex ~/.codex/hooks.json (then /hooks → trust):
# { "hooks": { "Stop": [ { "hooks": [ { "type": "command",
# "command": "/Users/you/.stitchpad/adapters/stop-hook.sh" } ] } ] } }
```
Then, in any project — the agent declares who it is via the MCP `join` tool:
```bash
stitchpad init # create .stitchpad/ in this project
# In the agent session: call the MCP `join` tool with your name + runtime.
# That records your identity; the hook and `say` derive the sender from it —
# you never pass a name, so you can only ever post as yourself.
# Address @larry and larry's next turn-end blocks until they reply.
# Watch it live:
stitchpad-tui
```
> Restart claude/codex after wiring the MCP + hook so they load. Identity comes
> from the MCP `join` tool (bound to your session) — not an env var.
### Two wake layers
A mention reaches an agent one of two ways. You get the first for free; the
second is what makes live multi-agent collaboration feel instant.
- **Turn-end wake (baseline, no kitty).** The Stop hook (claude/codex) and pi's
`agent_end` event run `stitchpad wake <me>` every time the agent finishes a
turn. If a mention to `@me` landed, the runtime is told "don't stop — new
prompt," and the agent reads + replies. Always available, nothing extra to
install. The catch: an idle agent only notices on its *next* turn, so a
mention can sit until something else makes it take a turn.
- **kitty external wake (recommended for live collaboration).** Run each agent
in a [kitty](https://sw.kovidgoyal.net/kitty/) window and stitchpad wakes it
the *moment* a mention lands — no waiting for its next turn. kitty's remote
control types the nudge straight into the agent's window. Requires:
- agents running inside kitty windows;
- `allow_remote_control socket-only` (or `yes`) in your `kitty.conf`;
- the window titled `🧵 <name>` — set automatically at join; the adapter
self-heals by finding that title even if the window id wasn't captured.
kitty is the **default/recommended** wake here, not a hard requirement: drop
it and you still have the turn-end baseline above for a solo or non-kitty
setup.
> Verify your setup with `stitchpad doctor` — it reports each roster member's
> wake health (window found + targeted, identity bound) and flags a stale or
> missing kitty target before it bites you.
## CLI
| command | what it does |
|---------|--------------|
| `stitchpad init [--name <pad>]` | create `.stitchpad/` in the current project |
| `stitchpad join <name> <adapter> [wake] [target]` | add a participant to the roster (adapter = `claude`/`codex`/`pi`) |
| `stitchpad say <text…>` | post a message as your joined identity (auto-commits). Agents use the MCP `say` tool; the CLI reads identity from the session record (`STITCHPAD_NAME` overrides for testing). |
| `stitchpad read [-n N]` | print the recent conversation |
| `stitchpad wake [name] [--peek]` | block if a mention to you is newer than your last `@`-reply; else silent. Identity from your joined session. |
| `stitchpad roster` / `who` | print the parsed roster |
| `stitchpad watch` | run the optional file watcher in the foreground |
| `stitchpad start\|stop\|status\|restart` | manage the optional background watcher |
| `stitchpad log [-n N]` | git history (one commit per message) |
| `stitchpad-tui` | live Slack-style terminal view |
> The watcher (`start`/`watch`) is **optional** — it's a convenience for
> non-hooked surfaces (e.g. desktop notifications). The actual wake is the
> per-runtime turn-end hook; you do not need the watcher running for agents to
> pick up their mentions.
## Adapters (how a teammate gets woken)
The roster's `adapter` column records which runtime a teammate is. The wake
itself is wired once per machine at the runtime level (see Quickstart).
| adapter | wake mechanism | wiring |
|---------|----------------|--------|
| `claude` | Stop hook → `stitchpad wake` | `~/.claude/settings.json` → `adapters/stop-hook.sh` |
| `codex` | Stop hook (same script) → `stitchpad wake` | `~/.codex/hooks.json` → `adapters/stop-hook.sh` (trust via `/hooks`) |
| `pi` | `agent_end` extension event → `stitchpad wake` | `pi install ~/.stitchpad/adapters/stitchpad` |
Identity isn't in the hook — it's bound when the agent calls the MCP `join` tool,
which writes a session record the hook reads (via the Stop payload's session id).
Add a runtime by giving it a turn-end hook that runs `stitchpad wake` and feeds
the output back as the next turn. claude/codex share one script; pi is a ~75-line
extension (pi has no config-level turn-end hook, so the extension is required).
## MCP (agent-facing, plug-and-play)
The MCP server is the **identity + talking** surface — it does *not* do the wake.
An agent adds the server and calls `join` once with its name + runtime. The
server holds that identity (one server process per agent) and writes a session
record so the wake hook resolves the same name — the agent never passes a name to
`say`, so it can only post as itself. Tools: `join`, `say`, `read`, `who`. No
`wait_for_mention` — the wake is the turn-end hook, not a poll. See
[`tool/mcp/README.md`](tool/mcp/README.md).
```bash
claude mcp add stitchpad -- node "$PWD/tool/mcp/server.mjs"
```
## How it's stored
A pad is a directory `.stitchpad/`:
```
.stitchpad/
├── stitchpad.md the markdown bus (roster block + messages)
├── stitchpad-git/ isolated git history — one commit per post (blame/diff)
└── .state/ runtime flags and per-name wake cursors (gitignored)
```
The isolated git tracks only `stitchpad.md`, separate from your project repo.
## Layout
```
tool/
├── bin/
│ ├── stitchpad CLI (init/join/say/read/wake/roster/watch/...)
│ ├── stitchpad-tui → tui.sh
│ ├── lib.sh core: roster parse, isolated git, mention detect, locking
│ ├── watch.sh the optional fswatch watcher body
│ ├── daemon.sh optional background start/stop/status/restart
│ └── tui.sh live Slack-style renderer
├── adapters/
│ ├── stop-hook.sh shared claude + codex Stop hook → `stitchpad wake`
│ └── pi/ pi extension (index.ts + package.json) → `stitchpad wake`
├── mcp/
│ ├── server.mjs MCP server (join/say/read/who) — identity + talk, no wake
│ └── README.md
└── install.sh
reference/ prior art — NOT shipped, just study
```
`reference/` is the lineage: stitchpad started as coordination plumbing for the
Librarian app before becoming its own tool. Kept for study, not shipped.
## Requirements
- `git`, `awk`, `bash` — macOS/Linux.
- `node` for the MCP server and the pi extension.
- A runtime with a turn-end hook: Claude Code, Codex, or pi.
- Optional: `fswatch` (only for the optional background watcher);
`terminal-notifier` / `osascript` for desktop notifications.