Skip to main content
Glama
README.md
# claude-mesh

claude-mesh lets two live Claude Code instances on the same workstation exchange a synchronous question/answer. One Claude can call `ask_peer` and block until a peer Claude replies via `reply_to_peer` — coordination happens through a shared local **SQLite file** that each instance opens directly and **polls** for replies. No database server, no daemon.

See the [design spec](docs/specs/2026-05-22-claude-mesh-design.md) for the architecture, and the [SQLite backend addendum](docs/specs/2026-06-04-sqlite-backend-addendum.md) for the current storage/wakeup mechanism (which supersedes the Postgres + `LISTEN`/`NOTIFY` backend the original spec describes).

---

## Install

claude-mesh isn't published to npm. Clone it once on your workstation and build:

```bash
git clone <repo-url> ~/src/claude-mesh
cd ~/src/claude-mesh
npm install
npm run build
```

`npm install` fetches a prebuilt `better-sqlite3` binary (no compiler needed on mainstream macOS/Linux x64/arm64; Alpine/musl would need a build). That leaves you with executable artifacts at:

- `~/src/claude-mesh/dist/mcp-server.mjs`
- `~/src/claude-mesh/dist/hooks/session-start.mjs`
- `~/src/claude-mesh/dist/hooks/session-end.mjs`
- `~/src/claude-mesh/dist/hooks/inbox-poll.mjs`

You reference these by absolute path from your **user-scoped** config so every project picks up the mesh automatically (see [Global setup](#global-setup) below). If you'd rather use short binary names, link them globally:

```bash
cd ~/src/claude-mesh
npm link
```

That puts `claude-mesh-server`, `claude-mesh-session-start`, `claude-mesh-session-end`, and `claude-mesh-inbox-poll` on your `PATH`.

---

## Database

**No setup required.** The shared SQLite file is created automatically on first run at `~/.claude-mesh/mesh.db` (the directory is created if missing, and the server runs its migrations on boot).

To put the file somewhere else, set `MESH_DB_PATH` in a `.env` inside the claude-mesh checkout — the binaries walk up from their own location at runtime to find it, regardless of which project Claude Code spawned them from:

```bash
cd ~/src/claude-mesh
cp .env.example .env
# edit MESH_DB_PATH if you don't want ~/.claude-mesh/mesh.db
```

You can also override `MESH_DB_PATH` in the MCP server's `env` block — values already in `process.env` take precedence over the mesh `.env`. All instances that should see each other must point at the **same** file.

---

## Global setup

Register the mesh **once at user scope** and every Claude Code project on the workstation joins automatically — there is nothing to copy into individual repos beyond naming each peer (step 3). This is the recommended layout; a single MCP server entry and a single hooks block cover all projects.

### 1. Register the MCP server (user scope)

Add `claude-mesh` to the top-level `mcpServers` block in `~/.claude.json`. Because it lives at user scope, every project inherits it:

```json
{
  "mcpServers": {
    "claude-mesh": {
      "type": "stdio",
      "command": "node",
      "args": ["/home/you/src/claude-mesh/dist/mcp-server.mjs"],
      "env": {}
    }
  }
}
```

Or use the CLI with user scope:

```bash
claude mcp add --scope user claude-mesh node /home/you/src/claude-mesh/dist/mcp-server.mjs
```

If you ran `npm link`, the `command`/`args` become `"command": "claude-mesh-server"` (drop `args`).

No `CLAUDE_MESH_PROJECT_PATH` is needed for a user-scoped install: Claude Code sets `CLAUDE_PROJECT_DIR` per project, and the server resolves identity in the order `CLAUDE_MESH_PROJECT_PATH` → `CLAUDE_PROJECT_DIR` → `process.cwd()`. Set `CLAUDE_MESH_PROJECT_PATH` explicitly only if you register the server per-project instead.

### 2. Wire the hooks (user scope)

Add the hooks to `~/.claude/settings.json` so they run for every project. Claude Code's hook schema nests `hooks` inside a matcher entry. There are **four** hook entries across three scripts — note that `inbox-poll.mjs` runs on **both** `UserPromptSubmit` and `Stop`:

```json
{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          { "type": "command", "command": "node /home/you/src/claude-mesh/dist/hooks/session-start.mjs" }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          { "type": "command", "command": "node /home/you/src/claude-mesh/dist/hooks/inbox-poll.mjs" }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "node /home/you/src/claude-mesh/dist/hooks/inbox-poll.mjs" }
        ]
      }
    ],
    "SessionEnd": [
      {
        "hooks": [
          { "type": "command", "command": "node /home/you/src/claude-mesh/dist/hooks/session-end.mjs" }
        ]
      }
    ]
  }
}
```

What each does:

- **`SessionStart`** → `session-start.mjs` registers the peer and marks it active.
- **`UserPromptSubmit`** → `inbox-poll.mjs` polls the inbox before each turn, surfacing any pending questions in a `<system-reminder>` block.
- **`Stop`** → `inbox-poll.mjs` polls again when Claude tries to finish a turn; if peer questions are pending it **blocks the stop** so this Claude answers them instead of going idle.
- **`SessionEnd`** → `session-end.mjs` deactivates the peer.

Claude Code passes each hook a `session_id` on stdin, which scopes deactivation to the owning session (a late `SessionEnd` from a prior session can't clobber a freshly-registered live peer). Hook scripts exit `0` even on failure (with a stderr warning) so Claude Code never refuses to start.

If you ran `npm link`, replace the `command` strings with `claude-mesh-session-start`, `claude-mesh-inbox-poll` (for both `UserPromptSubmit` and `Stop`), and `claude-mesh-session-end`.

### 3. Name each peer

Create `.claude-mesh.json` at each project root (commit it — it identifies this project in the mesh):

```json
{ "name": "backend" }
```

Choose a short, unique name per project (e.g. `frontend`, `backend`, `infra`). If a project has no `.claude-mesh.json`, the peer name falls back to the project directory's basename — so an "empty" mesh config still joins the mesh under that name; it isn't unconfigured.

---

## Per-project setup (alternative)

Prefer to scope the mesh to specific projects instead of the whole workstation? Put the same MCP server entry in `.mcp.json` at the project root and the same hooks block in `.claude/settings.json` (or `.claude/settings.local.json` to keep them untracked). When registering per-project, set `CLAUDE_MESH_PROJECT_PATH` in the MCP `env` block (e.g. `"${workspaceFolder}"`) so the long-lived server resolves the right `.claude-mesh.json` rather than relying on `process.cwd()`.

---

## Tools reference

| Tool | Description |
|---|---|
| `register` | Upsert this peer into the mesh. Called automatically by the `SessionStart` hook. |
| `list_peers` | List all known peers and whether each is stale (not seen for > 30 minutes). |
| `ask_peer` | Ask another peer a question. Blocks until a reply arrives or a 5-minute timeout. |
| `reply_to_peer` | Reply to a question received via `poll_inbox`. |
| `poll_inbox` | Return pending questions addressed to this peer; marks them delivered. Called automatically by the `UserPromptSubmit` hook. |

---

## Typical flow

1. Both Claude Code instances start — the `SessionStart` hook calls `register` on each.
2. On each user prompt, the `UserPromptSubmit` hook calls `poll_inbox`. If questions are waiting, Claude sees them in a `<system-reminder>` block and can call `reply_to_peer`.
3. When a Claude tries to end a turn, the `Stop` hook polls again and **blocks the stop** if peer questions are still pending — so the receiving Claude answers them rather than going idle.
4. To ask a peer directly, Claude calls `ask_peer { to: "backend", question: "..." }`. The call blocks until the peer replies (or 5 minutes pass).

---

## Known limitations

- **5-minute timeout.** `ask_peer` returns `{ status: "timed_out" }` if no reply arrives within 5 minutes. The peer may not be running or may be busy.
- **Both peers must be live.** There is no queuing for offline peers beyond the `pending` message status — a reply must arrive in the same session.
- **Local only / single machine.** The transport is a shared SQLite file on local disk. All instances must share the same file (and therefore the same machine); remote or cross-machine coordination is not supported.
- **Reply latency.** A reply is picked up on the asker's next poll tick (~1s) rather than instantly — imperceptible against the 5-minute blocking budget.