feat(plugin): per-session mailbox identity + mailbox-update command

The hook now derives a unique mailbox name from the session_id supplied
on hook stdin, so two parallel Claude Code sessions in the same project
get distinct mailboxes (e.g. `claude-a8b3c1d2`, `claude-d4e5f6a7`)
instead of colliding on a shared env value. An optional
CLAUDE_MAILBOX_NAME base prefix flavors the names as `<base>-<sid>`.

Adds:
- `claude-mailbox session-announce` subcommand for the new SessionStart
  hook, which prints the current session's mailbox name to context
- `/claude-mailbox:mailbox-update` slash command for `npm update` +
  daemon restart
- stdin parsing helpers (parseHookStdin, deriveSessionName) with unit
  tests; the doctor no longer needs a mandatory name prompt
This commit is contained in:
Mika Kuns
2026-05-19 11:39:14 +02:00
parent c231f8c18c
commit 462d6561e1
9 changed files with 385 additions and 94 deletions

View File

@@ -1,8 +1,8 @@
# claude-mailbox plugin
Lets Claude Code pull unread messages from a local `claude-mailbox` daemon before every prompt and inject them into the conversation context.
Lets Claude Code pull unread messages from a local `claude-mailbox` daemon before every prompt and inject them into the conversation context. Each Claude session gets a **unique mailbox identity** auto-derived from its session id, so two sessions in the same project never collide.
## Setup (two steps, all inside Claude Code)
## Setup (three prompts, all inside Claude Code)
```
/plugin marketplace add https://git.kuns.dev/releases/ClaudeMailbox
@@ -10,50 +10,59 @@ Lets Claude Code pull unread messages from a local `claude-mailbox` daemon befor
/claude-mailbox:mailbox-doctor
```
The doctor command walks the rest:
The doctor walks the rest:
1. checks whether the `claude-mailbox` binary is on `PATH` — installs it (`npm install -g @kuns/claude-mailbox`) if missing, asks before doing anything that might need elevation
2. checks the daemon status — runs `install-autostart` and/or `start` until it reports `Running`
3. ensures `CLAUDE_MAILBOX_NAME` is set in `.claude/settings.json` env — prompts for a name if not, writes it idempotently
4. runs a self → self smoke test to verify the round-trip works
1. installs the `claude-mailbox` binary via `npm install -g @kuns/claude-mailbox` if missing (asks first)
2. registers the daemon for autostart and starts it if needed
3. health-probes `http://127.0.0.1:47822/health`
4. optionally lets you set a **base prefix** (e.g., `backend`) — without one, mailbox names are anonymous (`claude-XXXXXXXX`)
5. runs a self → self smoke test
Restart Claude Code after the doctor finishes (only needed if the mailbox name was newly written). Unread messages will then appear in context before every prompt.
Restart Claude Code only if step 4 wrote a new prefix. After that, every prompt auto-pulls unread messages.
## Why a mailbox name?
## Mailbox identity (the important bit)
Each Claude session has an identity used to address peer sessions — like an email address. If you run a `backend` session and a `frontend` session in parallel, they need different names so they can send messages to each other.
Each Claude Code session gets its own mailbox name, derived from the session's UUID:
For a single Claude Code instance just wanting notifications, any stable kebab-case name works. The name lives in **per-project** `.claude/settings.json` env, so different worktrees / projects automatically get different mailboxes.
| Configuration | Resulting mailbox name |
|---|---|
| No `CLAUDE_MAILBOX_NAME` set | `claude-a8b3c1d2` (first 8 hex chars of session_id) |
| `CLAUDE_MAILBOX_NAME=backend` in `.claude/settings.json` env | `backend-a8b3c1d2` |
## What the hook actually does
So if you open two Claude Code sessions in the same project, they'll be e.g. `backend-a8b3c1d2` and `backend-d4e5f6a7` — distinct, addressable, no manual setup.
Before every prompt the plugin runs `claude-mailbox check --hook`, which:
The `SessionStart` hook announces the current session's mailbox name in the conversation context on startup. Peers discover each other via `claude-mailbox list` or the `mcp__mailbox__list_mailboxes` MCP tool.
- prints unread mailbox messages in a Claude-friendly format and marks them delivered,
- stays **silent** when the inbox is empty or `CLAUDE_MAILBOX_NAME` is not set,
- emits a one-line setup hint when the daemon is unreachable, so a missing daemon is loud, not invisible.
## What the hooks do
Cost: one local HTTP round-trip plus Node coldstart per prompt (~100ms on Windows).
| Hook | Command | Effect |
|---|---|---|
| `SessionStart` | `claude-mailbox session-announce` | Prints `"Claude-Mailbox: this session is mailbox \`X\`"` so Claude knows its own identity. |
| `UserPromptSubmit` | `claude-mailbox check --hook` | Pulls unread messages for the session's mailbox and injects them as context. Silent on empty inbox; emits a one-line setup hint when the daemon is unreachable. |
## Commands
Cost: one local HTTP round-trip per prompt + Node coldstart (~100ms on Windows).
## Slash commands
| Command | What it does |
|---|---|
| `/claude-mailbox:mailbox-doctor` | Diagnose + auto-fix the full setup. |
| `/claude-mailbox:mailbox-status` | Read-only health check. No changes. |
| `/claude-mailbox:mailbox-update` | Update the daemon to the latest npm version and restart it. |
## Smoke test (manually, after doctor finishes)
## Sending a message to a peer session
From inside Claude Code, use the MCP tool (the daemon already exposes `mcp__mailbox__*`). From any shell:
```sh
claude-mailbox send --from probe --to <your-mailbox-name> --body "hello"
claude-mailbox list # find the recipient's mailbox name
claude-mailbox send --from probe --to backend-a8b3c1d2 --body "hi"
```
Then start a new Claude Code prompt — the message should appear in context before Claude's first reply.
## Uninstall
```
/plugin uninstall claude-mailbox@claude-mailbox
npm uninstall -g @kuns/claude-mailbox
claude-mailbox uninstall-autostart # if you used it
claude-mailbox uninstall-autostart # if you registered it
```

View File

@@ -1,85 +1,92 @@
---
description: Diagnose and auto-fix the Claude-Mailbox setup (binary install, daemon autostart, mailbox name, smoke test).
description: Diagnose and auto-fix the Claude-Mailbox setup (binary install, daemon autostart, smoke test, optional base-prefix).
allowed-tools: Bash, Read, Edit, Write
---
You are running the **Claude-Mailbox doctor**. Walk through these checks in order. After each check, print a one-line status (✓ / ✗) and the action you took. At the very end, print a final summary block.
You are running the **Claude-Mailbox doctor**. Walk through these checks in order. After each step, print a one-line `✓` / `✗` with the action you took. End with a summary block.
Throughout, prefer the dedicated tools (`Read`, `Edit`, `Write`) for files. Use `Bash` only for `claude-mailbox` subcommands, `npm`, `where`/`which`, and `cat /etc/os-release` style lookups. Never run `sudo` automatically — if elevation is needed, stop and ask the user how to proceed.
---
Use `Bash` only for `claude-mailbox` subcommands, `npm`, `where`/`which`, and HTTP probes. Use `Read`/`Edit`/`Write` for `.claude/settings.json`. Never run `sudo` automatically — if elevation is needed, stop and ask.
## Step 1 — daemon binary on PATH
Run: `claude-mailbox --version`
- **Exit 0** → binary present. Record the version string. ✓ continue.
- **Command not found / non-zero exit** → binary missing. Tell the user the install command for their platform and ask before running it:
- **Exit 0** → ✓ record the version. Continue.
- **Command not found** → binary missing. Install path:
| Platform | Install command |
| Platform | Command |
|---|---|
| Windows | `npm install -g @kuns/claude-mailbox` (no admin) |
| macOS / Linux | `npm install -g @kuns/claude-mailbox` (may need `sudo` depending on Node setup; ask the user if `npm install` fails with EACCES) |
| macOS / Linux | `npm install -g @kuns/claude-mailbox` (may fail with EACCES — never run sudo automatically; ask the user) |
Prerequisite: `npm config get @kuns:registry` should return `https://git.kuns.dev/api/packages/releases/npm/`. If it doesn't, set it first:
Prerequisite: `npm config get @kuns:registry` must point at `https://git.kuns.dev/api/packages/releases/npm/`. If not:
```
npm config set @kuns:registry=https://git.kuns.dev/api/packages/releases/npm/
```
After install, re-run `claude-mailbox --version`. If it still fails, stop and report the error.
After install, re-run `claude-mailbox --version`. If it still fails, stop and report.
## Step 2 — daemon autostart and running state
Run: `claude-mailbox status`
- **`Running`** → ✓ continue.
- **`Stopped`** → run `claude-mailbox start`. Re-check status.
- **`NotInstalled`** → run `claude-mailbox install-autostart`, then `claude-mailbox start`. Re-check status.
- `Running` → ✓ continue.
- `Stopped` `claude-mailbox start`, re-check.
- `NotInstalled` `claude-mailbox install-autostart`, then `claude-mailbox start`, re-check.
If status doesn't become `Running` after the fix, stop and report what `status` and `start` printed.
If status doesn't reach `Running`, stop and report.
Sanity check: `curl -sf http://127.0.0.1:47822/health` (or use Bash to fetch it). Expect a JSON body with `"status":"ok"`.
## Step 3 — health probe
## Step 3 — mailbox name in project settings
Hit `http://127.0.0.1:47822/health`. Expect a JSON body with `"status":"ok"`. If unreachable, stop and report — the daemon claims it's running but isn't accepting connections.
The hook reads the mailbox name from `$CLAUDE_MAILBOX_NAME`. Claude Code injects env vars from `.claude/settings.json` into hook commands, so the cleanest place to set it is per-project.
## Step 4 — mailbox identity
1. Read `.claude/settings.json` in the current working directory (it may not exist yet — that's fine).
2. Check if `env.CLAUDE_MAILBOX_NAME` is set.
3. If **set**: ✓ continue, record the name.
4. If **not set**:
- Ask the user for a mailbox name. Suggest a default based on the cwd basename (e.g., for `C:\Private\Claude-Mailbox` suggest `claude-mailbox`). Names should be short, kebab-case-ish, unique among parallel Claude sessions.
- Read existing `.claude/settings.json` if present, otherwise start with `{}`.
- Set/merge `env.CLAUDE_MAILBOX_NAME` to the chosen name. Preserve any other existing settings.
- Write back with 2-space indentation.
- Tell the user they need to **restart this Claude Code session** for the env to take effect in the hook — but the smoke test below can still run because we'll pass `--name` explicitly.
**No prompt by default.** Each Claude Code session now gets a unique mailbox name auto-derived from its `session_id` (e.g., `claude-a8b3c1d2`), so two parallel sessions can never collide.
## Step 4 — smoke test
Read `.claude/settings.json` in the current working directory and look for `env.CLAUDE_MAILBOX_NAME`.
Use the resolved name from step 3 (either pre-existing or just chosen). Run:
- If set → ✓ this is a **base prefix**. The real name will be `<base>-<short_session_id>`. Tell the user "Mailbox prefix is set to `X`."
- If unset → ✓ tell the user "Mailbox name will be auto-derived (`claude-<short_session_id>`)."
Then **ask** the user (one question, not a deep prompt):
> "Want to flavor your mailbox names with a memorable prefix like `backend` or `frontend`? (yes / no / change to `<x>`)"
If they say yes or give a value:
1. Read `.claude/settings.json` (empty `{}` if missing).
2. Merge `env.CLAUDE_MAILBOX_NAME` = chosen value, preserving anything else.
3. Write back with 2-space indentation.
4. Mark this as `restart_needed = true`.
If they say no or skip → leave as-is.
## Step 5 — smoke test
Use two ephemeral names (`doctor-probe-a` / `doctor-probe-b`) — we don't need the real session name here, we just need to prove the daemon round-trips:
```
claude-mailbox send --from doctor-probe --to <name> --body "ping from /claude-mailbox:mailbox-doctor"
claude-mailbox check --name <name>
claude-mailbox send --from doctor-probe-a --to doctor-probe-b --body "ping from doctor"
claude-mailbox check --name doctor-probe-b
```
- The `check` output should be a JSON array containing exactly one message with `"from": "doctor-probe"` and that body.
- If yes: ✓ smoke test passed.
- If no (empty array, error, or wrong message): ✗ report what was returned.
The `check` output must be a JSON array with one message: `from: doctor-probe-a`, body matches. If yes ✓. If no ✗ and report what came back.
## Step 5 final summary
Print a compact block with these fields, one per line:
## Step 6 — summary
```
Claude-Mailbox doctor
binary: <version>
daemon: Running | Stopped | NotInstalled (and what you did)
health: ok | unreachable
mailbox name: <name> (source: existing | newly written to .claude/settings.json)
smoke test: passed | failed
restart hint: <yes if name was newly written, otherwise no>
binary: <version>
daemon: Running (and what you did, if anything)
health: ok
base prefix: <name from settings, or "auto-derived (anonymous)">
smoke test: passed | failed
restart hint: yes if restart_needed, otherwise no
```
If everything is ✓ and `restart hint: yes`, end with: "Restart Claude Code (or open a new session) so the UserPromptSubmit hook picks up `CLAUDE_MAILBOX_NAME`." If `restart hint: no`, end with: "You're good to go — unread messages will appear before your next prompt."
End with one of:
- All ✓ and no restart needed → "Setup is healthy. Your mailbox name this session will be revealed by the SessionStart hook on next session start (or run `claude-mailbox list` to see active mailboxes)."
- All ✓ and restart needed → "Restart Claude Code (or open a new session) so the SessionStart hook picks up the new base prefix."
- Anything ✗ → "Setup incomplete: <first failure>."

View File

@@ -0,0 +1,56 @@
---
description: Update the Claude-Mailbox daemon to the latest published npm version and restart it.
allowed-tools: Bash
---
You are running the **Claude-Mailbox update** command. Update the `@kuns/claude-mailbox` npm package and restart the daemon. Never run `sudo` automatically — if elevation is needed, stop and ask.
## Step 1 — current version
Run: `claude-mailbox --version`
- Exit 0 → record the version string as `CURRENT`.
- Non-zero → tell the user the daemon binary is not installed yet. Suggest `/claude-mailbox:mailbox-doctor` to do a full setup and stop.
## Step 2 — latest published version
Run: `npm view @kuns/claude-mailbox version`
If the npm registry config is missing, the call may fail with a 404. Fall back to:
```
npm view --registry=https://git.kuns.dev/api/packages/releases/npm/ @kuns/claude-mailbox version
```
Record the result as `LATEST`.
## Step 3 — compare
- If `CURRENT === LATEST`: print "Already up to date (vX.Y.Z)." and stop. Do not run any further steps.
- Otherwise: tell the user `CURRENT``LATEST` and ask for confirmation before proceeding.
## Step 4 — perform the update
On user confirmation, run these in order. Stop on the first failure and report it:
1. `claude-mailbox stop`
2. `npm install -g @kuns/claude-mailbox@latest`
- On Linux/macOS this may fail with EACCES. **Do not run sudo automatically.** Ask the user how they want to proceed (e.g., `sudo npm install -g …`, or switch to a user-scoped Node setup with nvm/fnm).
3. `claude-mailbox start`
4. `claude-mailbox --version` to verify the upgrade landed.
5. `claude-mailbox status` to verify the daemon is `Running`.
## Step 5 — summary
Print exactly this block:
```
Claude-Mailbox update
previous version: <CURRENT>
new version: <whatever --version now reports>
daemon: Running | Stopped | NotInstalled
pending messages survived: <count of messages still in inbox via `claude-mailbox list`, if applicable>
```
If the new version matches `LATEST` and daemon is `Running`, end with: "Update complete."
Otherwise, end with the first thing that went wrong.

View File

@@ -1,5 +1,15 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "claude-mailbox session-announce"
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [