claudemesh
v0.1.1
Published
Local side-channel between Claude Code instances: discover, message, and read other claude CLI sessions on the same machine.
Downloads
249
Maintainers
Readme
claudemesh
Let your local Claude Code sessions find and talk to each other — in realtime, with zero compromises to the Claude Code CLI you already use.
Run claude in three folders. Today they don't know the others exist. Install claudemesh and they do: each session gets a stable ID, can list its peers, send them tasks that wake them up the instant they arrive, and read each other's conversation history — all over a tiny localhost daemon. No cloud, no network exposure, no auth, no behavior changes inside claude itself. Stop using it any time and the original claude binary is exactly as it was.
claudemesh # launch claude inside the realtime supervisor
claudemesh -c # claude -c (continue most recent session)
claudemesh --resume # any other claude flag works the sameAnything that isn't a recognized subcommand (list, send, install, …) is forwarded straight to the wrapped claude binary, so claudemesh is a drop-in alternative entrypoint to claude — no alias, no shadow, no plugin install inside Claude. You get every feature of Claude Code (sessions, hooks, plugins, MCP, sub-agents, slash commands, IDE integration) plus the inter-instance side channel.
Status: v0.2 — realtime PTY supervisor + folder-name addressing + idle tracking.
Why
You probably run claude in several folders at once. They have no idea about each other. Claudemesh gives every session a stable identity and a realtime side channel:
- Find peers: list which other Claudes are running on this machine, where, and for how long.
- Hand off work:
claudemesh send api-service "rerun the migration check"— the recipient wakes up and acts on it within a second if it was launched viaclaudemesh, or on its next user prompt otherwise. - Inspect: peek at another instance's history (recent turns, search, fetch a specific turn) when you need cross-session context.
- Notice: the receiver's status line ticks up (
✉2) when messages are waiting.
Everything is purely additive. Hooks and MCP are installed at the user level (~/.claude/), so existing project-level Claude Code config is untouched. Removing claudemesh is one command (claudemesh uninstall) and leaves no trace inside claude itself.
How it works
There are two layers, and you can use either independently:
┌─────────────── one machine ───────────────┐
│ │
│ ┌── claudemesh (PTY supervisor) ────────┐ │ ← Layer 2: realtime injection
│ │ ↑↓ proxy user terminal ↔ claude │ │ (when launched via `claudemesh`)
│ │ subscribes to daemon /events │ │
│ │ writes "[inbox]\n" to PTY master │ │
│ └─┬─────────────────────────────────┬─┘ │
│ │ (PTY pair) │ │
│ ┌─▼──────────────── claude ──────────▼─┐ │ ← Layer 1: hooks + MCP
│ │ SessionStart/End hooks │ │ (works with bare `claude` too)
│ │ UserPromptSubmit hook (drains inbox)│ │
│ │ Stop hook (blocks on tasks) │ │
│ │ MCP server: claudemesh │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ │
│ │ claudemesh daemon │ │
│ │ HTTP @ 127.0.0.1:7878 │ │
│ │ • registry + idle flag │ │
│ │ • inboxes │ │
│ │ • events long-poll │ │
│ │ • liveness sweep │ │
│ └─────────────────────────┘ │
└────────────────────────────────────────────┘The daemon binds to 127.0.0.1 only. State persists to ~/.claudemesh/.
Realtime via the supervisor
claudemesh spawns claude inside a PTY (via node-pty) and proxies your terminal byte-for-byte to/from it — so it looks and feels exactly like running claude directly. Any flag you pass to claudemesh (e.g. claudemesh -c, claudemesh --resume <id>) is forwarded to claude. In parallel the supervisor long-polls the daemon for new inbox messages addressed to this session. When one arrives:
- Wait for the daemon's
idleflag on this session to betrue(theStophook sets it when claude finishes a turn; theUserPromptSubmithook clears it). - Wait for ≥500ms of stdin silence (so we don't corrupt typing).
- Write
[inbox]\nto the PTY master.
That sentinel is read by claude as if you typed it. UserPromptSubmit fires, the hook drains the inbox, and the messages arrive as additional context attached to that turn — with explicit instructions to act on kind="task" bodies, not just acknowledge them. End-to-end latency from send to claude starting work is sub-second on a healthy machine.
Without the supervisor (bare claude)
If you keep running claude directly, the inbox + hooks still do most of the work:
- Messages still queue in the inbox.
- Status line still shows
✉N. - The
Stophook still blocks on pending tasks if it happens to fire while messages are queued. UserPromptSubmitstill drains messages into the next turn.
What you lose is the wake-up for fully-idle claude: messages that arrive after Stop has already fired sit in the inbox until you submit any prompt. With the supervisor, that gap goes away.
Status line
[abc12345 · 3 peers · ✉2]abc12345— this session's ID.3 peers— count of other live Claudes (drops out at zero).✉2— unread inbox messages (drops out at zero).
Install
Requires Node ≥ 20. The supervisor needs
node-pty(native, prebuilt for darwin/linux/win).
npm i -g @ktamas77/claudemesh
claudemesh installclaudemesh install patches two files (each backed up first):
~/.claude/settings.json— adds the four hooks and the status-line script.~/.claude.json— adds theclaudemeshMCP server undermcpServers.
After this, just use claudemesh everywhere you'd type claude:
claudemesh # interactive session in the supervisor
claudemesh -c # resume the most recent session
claudemesh --resume xyz # resume a specific session idThe original claude binary is untouched — both work side by side. Use claudemesh when you want realtime inbox delivery; use claude when you don't (you still get the inbox + ✉ status line, just no auto-wake).
If you'd rather invoke the supervisor under a shorter name, drop a tiny script anywhere on your PATH:
mkdir -p ~/.local/bin
cat > ~/.local/bin/clc <<'EOF'
#!/bin/sh
exec claudemesh "$@"
EOF
chmod +x ~/.local/bin/clcNow clc -c is claudemesh -c is claude -c-with-supervisor.
claudemesh uninstall # reverses the installIf node-pty fails to install (older toolchain), bare claudemesh errors out with a fix-it message but the subcommands (claudemesh list, claudemesh send, etc.) still work.
Adding the badge to an existing status line
If you already have a custom statusLine script in ~/.claude/settings.json, claudemesh install detects it and leaves it alone rather than clobbering your customizations. To add the [abc12345 · 3 peers · ✉2] badge yourself, capture stdin once and pipe it to claudemesh statusline:
#!/bin/bash
input=$(cat)
# ... your existing parts ...
out="$host | $model | $ctx | $folder"
# Claudemesh badge (only renders if claudemesh is installed and a session is registered)
if command -v claudemesh >/dev/null 2>&1; then
badge=$(printf '%s' "$input" | claudemesh statusline 2>/dev/null)
[ -n "$badge" ] && out="$out | $badge"
fi
printf '%s\n' "$out"claudemesh statusline reads the same JSON Claude Code already pipes to your script (it needs session_id), prints [id · peers · ✉N], and exits silently with no output if the daemon is down or the session isn't registered yet.
CLI
claudemesh [claude args...] launch claude inside the realtime supervisor
(e.g. `claudemesh -c`, `claudemesh --resume <id>`)
claudemesh list show all live Claude sessions
claudemesh whoami this shell's parent claude (best-effort by ppid)
claudemesh send <target> <message> send a task message; target = id or folder name
claudemesh history <target> [-n N] tail another session's transcript
claudemesh search <target> <query> search another session's transcript
claudemesh status daemon health + instance count
claudemesh install / uninstall patch / unpatch ~/.claude config
claudemesh help show subcommand help
claudemesh run [claude args...] explicit form of the default supervisor launch
claudemesh daemon / mcp / hook / statusline internal (used by Claude Code)Anything that's not a recognized subcommand from this list is treated as args for the supervisor and forwarded to claude — that's how claudemesh -c and friends work.
CLAUDEMESH_CLAUDE_BIN overrides which binary the supervisor launches (default: claude on PATH). Useful if you have multiple Claude Code builds installed.
Addressing sessions: id or folder name
Anywhere a target is expected — CLI commands and MCP tools alike — you can pass either the 8-char claude_id or the basename of that session's working directory. The resolver:
- Tries an exact
claude_idmatch against the live registry. - Falls back to a case-insensitive match on
basename(cwd).
# These two are equivalent when one live session is running in /Users/me/dev/api-service
claudemesh send 7k3p9q2x "rerun the migration check"
claudemesh send api-service "rerun the migration check"If the folder name matches more than one live session (e.g. ~/orgA/api and ~/orgB/api both running), the resolver errors out with both claude_ids and full paths so you can disambiguate by id. If nothing matches, it prints all live sessions.
MCP tools
The claudemesh MCP server exposes the following inside every claude session. Wherever a tool takes a target session, you can pass either an 8-char claude_id or a folder basename (see Addressing sessions).
| Tool | Purpose |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| whoami | Your own claude_id, cwd, started_at. |
| list_instances | All live sessions: id, cwd, last_active, pending count, idle. |
| send_message(to, body, kind?) | Send to another session. kind: "task" (default) wakes the recipient via the supervisor / Stop hook; kind: "note" is silent until next prompt. |
| read_inbox(drain?) | Pull pending messages for the current session (defaults to draining). |
| read_history(id, last_n_turns?, redact?) | Recent turns from another session, redacted by default; pass redact: false for raw. |
| search_history(id, query, options?) | Substring/regex search over another session's transcript with surrounding context. |
| get_turn(id, turn_index) | Pinpoint full-content fetch of one turn after read_history / search_history flagged it. |
Message delivery
Two kinds:
- task (default) — receiver acts on the body as if you typed it. Three delivery paths, in priority order:
- Supervisor injection (realtime) — if the receiver was launched via
claudemesh, the supervisor writes[inbox]\ninto the PTY the moment the message arrives and the receiver is idle + not actively typing. Sub-second wake-up. - Stop hook (between turns) — if the receiver is just finishing a turn when the message lands, the
Stophook blocks the stop and feeds the message in as the continuation directive. - UserPromptSubmit (next prompt) — if neither of the above caught it (bare
claude, fully idle), the message sits in the inbox until the user submits any prompt to that receiver, at which point it's appended as additional context for that turn.
- Supervisor injection (realtime) — if the receiver was launched via
- note — surfaces only via path #3 (next prompt). Doesn't block stops, doesn't wake the model.
Inbox is at-least-once and drained on read.
Idle tracking
The daemon stores an idle: boolean per session, kept in sync by the hooks:
SessionStart(or first register) →idle: falseUserPromptSubmit(user just submitted) →idle: falseStop, when allowing the stop (no inbox tasks pending) →idle: true
The supervisor reads this flag before injecting, ensuring it never types into a session that's mid-turn.
Privacy & security
- Daemon binds to
127.0.0.1only — never0.0.0.0. Single-user assumption matches the rest of~/.claude. read_historyredacts large tool results / file contents above a per-line byte cap by default. Passredact: false(or useget_turn) for raw.- Hooks fail open: any daemon error is logged and the hook exits 0. Claude Code keeps working with no claudemesh integration if the daemon is down.
- The supervisor never writes to the PTY without (a) idle flag set and (b) ≥500ms stdin silence — so it can't corrupt input you're typing.
Development
git clone [email protected]:ktamas77/claudemesh.git
cd claudemesh
npm install
npm run build # tsc → dist/ (also chmod +x the bin)
npm run dev # tsc --watch
npm run lint # eslint
npm run format # prettier --write
npm run typecheck # tsc --noEmit
npm test # vitest runPre-commit (husky) runs typecheck → test → lint-staged. A broken type or failing test blocks the commit.
Layout
src/
├── bin/claudemesh.ts # entrypoint, dispatches all subcommands
├── cli/
│ ├── run.ts # PTY supervisor (default `claudemesh` entrypoint)
│ ├── send.ts, history.ts, search.ts, list.ts, whoami.ts, status.ts
│ ├── install.ts, statusline.ts
│ └── ...
├── daemon/
│ ├── server.ts # HTTP routes incl. /events long-poll, PATCH idle
│ ├── registry.ts # id ↔ session ↔ pid ↔ cwd, idle flag
│ ├── inbox.ts # per-recipient jsonl
│ ├── waiters.ts # pub-sub for the events long-poll
│ └── liveness.ts # prune dead PIDs
├── mcp/server.ts # stdio MCP server
├── hooks/ # session-start, session-end, user-prompt-submit, stop
└── shared/ # paths, ids, http client, transcript reader, resolverRoadmap
- [x] Architecture & repo scaffold (TS + ESLint + Prettier + Husky)
- [x] Phase 1 — daemon + register/list + status line
- [x] Phase 2 — MCP tools (
whoami,list_instances,send_message,read_inbox) - [x] Phase 3 — task-mode wake-up,
read_history/search_history/get_turn - [x] Phase 4 — liveness sweep + lazy daemon autostart
- [x] Phase 5 — folder-name addressing, sharper imperative inbox prompts
- [x] Phase 6 —
claudemeshPTY supervisor for realtime injection (with subcommand passthrough) - [ ] Real-world soak across many concurrent supervisor sessions
- [ ] Reply chains: auto-set
fromwhen a recipient callssend_messageafter waking - [ ] Optional desktop notification fallback for un-supervised idle sessions
- [ ] Inject the message body itself instead of an
[inbox]sentinel (bracketed paste for multi-line; prependfrom <id> ·for attribution; preserve existing path as fallback when paste mode unavailable)
License
MIT © Tamas Kalman
