Wire a fifth pull hook so peer messages also surface between todo
items, not only at user prompts and subagent stops. While here,
extend the manual `install-hook` CLI so it patches the full plugin
hook set (SessionStart/UserPromptSubmit/SubagentStop/TaskCompleted/
SessionEnd) instead of only UserPromptSubmit, mirroring what the
plugin's hooks.json registers. Mailbox name is auto-derived from
stdin, so --name is no longer required.
Also corrects stale docs that claimed SessionStart auto-bootstraps
the watcher — push delivery has been opt-in since the
mailbox-collaborate skill landed.
New SessionEnd plugin hook runs `claude-mailbox session-end`, which derives the session's auto-name from stdin and asks the daemon to delete the mailbox if it has no pending messages either direction. Renamed mailboxes are preserved (the auto-name no longer exists, so DELETE is a no-op). The daemon-side `SKIP_UPSERT_PATHS` prevents the request from auto-recreating the mailbox. Sweeper remains the safety net for sessions that exit ungracefully.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Make /collaborate slash command self-contained so it works without a registered mailbox-collaborate skill in the session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The SessionStart announce no longer forces a watch-loop bootstrap on every session — it now emits a short pointer instructing Claude to invoke the new mailbox-collaborate skill (or /collaborate slash command) when the user wants peers to wake them mid-task. Messages still surface on the next user prompt via the UserPromptSubmit hook even without the watcher, so nothing is lost; idle sessions just stop burning relaunch tokens.
The watch-loop protocol (exit codes, rename handling, mail handling) moves from the hook prose into the new skill body, where it only loads when actually needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Run-key autostart used to register node.exe directly, which made Windows pop a console window at every user logon. Now install-autostart writes a one-line WshShell.Run launcher with vbHide and points the Run-key value at wscript.exe, so the daemon starts truly hidden. Uninstall paths also clean the .vbs file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final review flagged that waiterCount looks like a public API but is
only called from the rename-watch flake fix test. The behavior is fine
to keep public (in-memory, safe), but the docstring now matches the
intent so future API-surface scans don't load-bear on it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a Push delivery (watch) section to the root README with exit-code
table, cross-process semantics, and the active-vs-idle latency caveat
that came out of the empirical Claude Code BashOutput test. Adds a brief
reference + cross-link in node/README.md, and notes the SessionStart
bootstrap behavior in plugin/README.md alongside the existing hook
table. Adds /v1/watch to the REST surface table and the watch verb to
the CLI listing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fold daemonError into SessionAnnounceOptions so the helper owns the
mutually-exclusive choice between peer list and daemon-down hint. Before
this fix, a session-announce against an unreachable daemon emitted both
"No other mailboxes seen within the last 60 minutes (0 total registered)."
(misleading — the daemon was never asked) AND the daemon-unreachable hint.
Now only the hint appears when the daemon is down.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The rename test relied on a fixed 300ms setTimeout to fire after the CLI
subprocess had registered its waiter — adequate in isolation but flaky
under full-suite load on Windows (CLI spawn + first HTTP request can
exceed 300ms). Add a tiny public MailboxStore.waiterCount(name) helper
so the test can poll until the waiter is actually registered before
triggering the rename. Also tighten the missing-name assertion from
not-zero to the contract-exact exit code 1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fastify's default connectionTimeout is 0 (no timeout). With /v1/watch
holding requests open for up to 300s, an OS-level cap prevents a stuck
socket from persisting forever even if app-level cleanup misses a case.
Set just above the watch max so a healthy long-poll never races the
socket timeout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wrap the long-poll handler in try/finally to detach the req.raw close
listener on every resolution path, and use reply.hijack() on the aborted
branch so Fastify does not attempt to write to a socket that's already
closed (which would otherwise emit per-disconnect log noise once the
watcher relaunch loop is busy). Behavior on the wire is unchanged for
the four documented status codes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Node port (@kuns/claude-mailbox) is the recommended runtime and
covers all supported features. Maintaining a wire-compatible .NET twin
adds dual-impl tax with no remaining users, so remove src/, tests/,
solution + MSBuild files, NuGet config, and both .NET CI workflows.
Docs updated: README "Path C" build-from-source section dropped,
architecture diagram + Development section simplified, and node/README
no longer subtitles itself as a port of the .NET daemon.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous skill assumed a happy-path environment and silently
broke on real installs. Hardened the flow:
- Always pass --@kuns:registry on every npm call, so the upgrade
survives an unreadable user .npmrc (e.g. roaming HOME on a
network share that npm can't access).
- Treat /health as the ground truth, not `claude-mailbox status`.
Status only reflects the autostart wrapper and silently lies
when the inner process crashed at boot.
- Capture state before changing anything (CLI version, autostart
state, port, daemon /health version) so failures can roll back
to "do nothing — old daemon keeps running".
- Detect the "daemon reachable but autostart NotInstalled" case
(manual foreground serve) and refuse to swap binaries from
under it.
- After restart, poll /health for up to 10s and verify the live
daemon's version matches LATEST; on timeout, dump the platform-
appropriate daemon log tail instead of just reporting "Stopped".
- Reorder: install first, then stop+start. Downtime is paid only
after the new binary is verified on disk.
Fastify forbids addHook after the instance is listening, so the
sweep-timer cleanup hook from 1.5.0 threw on every `serve` startup
and crashed the daemon. Register the hook first, then start
listening, and assign the timer through a ref.
Mailbox listings grew unbounded as old sessions ended without
unregistering. This adds two layers of cleanup, configurable via
mailbox.json or `serve` flags:
- Lazy filter: list responses (REST /v1/list, MCP list_mailboxes)
drop mailboxes idle longer than hideAfterMinutes (default 24h),
while always keeping the caller and any sender with messages
pending for them.
- Background sweep: startServer runs an initial prune on boot and
schedules an unref'd interval timer that hard-deletes mailboxes
idle longer than deleteAfterMinutes (default 7d) which have no
pending messages, and wipes their delivered history.
Importing server.js statically also imports db.ts, which pulls in
node:sqlite at startup. On Linux that emits an ExperimentalWarning to
stderr for every CLI invocation -- visible to users running the hook on
every prompt. Defer the server import into the serve action so check
--hook / session-announce / send / peek / list never touch sqlite.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mailbox names are now built as <project>-<session-short>, where <project>
is the sanitized git-repo basename (or cwd basename) — no more env-var
prefix step. Sessions can re-tag themselves at runtime via the new
mcp__mailbox__rename tool (POST /v1/rename), which transfers all
pending messages to the new name in a single transaction. Peers using
the old name re-discover via list_mailboxes.
BREAKING: \$CLAUDE_MAILBOX_NAME is no longer read. Existing setups that
relied on the env-var prefix should remove it from .claude/settings.json;
the prefix now comes from the working directory automatically.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Native binding caused install pain on every new Node major (no prebuilts +
node-gyp needs VS+Windows SDK to fall back). For this project's workload
(a few ops/day, no advanced SQLite features) better-sqlite3's perf edge is
irrelevant — node:sqlite's bundled, ABI-stable sync API is the better fit.
- db.ts: DatabaseSync, db.exec("PRAGMA …"), explicit BEGIN/COMMIT helper to
replace db.transaction(); row casts go through unknown because node:sqlite
returns Record<string, SQLOutputValue>.
- package.json: drop better-sqlite3 + @types/better-sqlite3, bump
engines.node to >=24, vitest 2 → 4 (2.x couldn't resolve `node:sqlite`).
- mailbox-doctor: add Step 1 that enforces Node ≥24 with a concrete fix
message, renumbers downstream steps.
Node 1.2.0 → 1.3.0. 35 transitive packages removed from the lockfile.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Delivers peer messages that arrive during a long-running subagent into the
parent context the moment the Task tool returns, instead of waiting until
the next user prompt. Reuses the existing `claude-mailbox check --hook` so
the mailbox identity stays consistent with UserPromptSubmit.
Plugin 1.2.0 → 1.3.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
plugin.json was stuck at 0.1.0 across every release, so Claude Code's
per-version plugin cache never refreshed and clients kept running the
original .mcp.json (http://127.0.0.1:47822/mcp). Bump to 1.2.0 to match
the node package and add a release-workflow step that derives plugin.json
from the tag on every future release.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before registering the Scheduled Task / Run-key / launchd / systemd unit,
probe /health on the resolved port. If a non-claude-mailbox service
answers, refuse with a helpful hint (`--port <n>` or mailbox.json) so
users don't end up with autostart firing against an occupied port.
Pass --skip-port-check to bypass.
The doctor already had this logic in Step 2; now standalone
install-autostart invocations are protected too.
47822 collided with ClaudeDo.Worker.exe on at least one user's machine.
37849 is high, registered to nobody, and avoids the prior conflict.
Both the Node port and the .NET port move together (still
wire-compatible). Defaults change only — if a user has a custom port
in mailbox.json, that stays.
Two production-readiness fixes so colleagues can install cleanly:
1. Plugin's MCP server now spawns `claude-mailbox mcp-stdio`, a small
stdio MCP wrapper that proxies tool calls to the daemon's REST API.
Claude Code does not support env-var substitution in HTTP MCP `url`
fields (issue #46889), so the wrapper is the only way to make the
daemon URL configurable per machine via CLAUDE_MAILBOX_URL.
2. Windows `install-autostart` now falls back from `schtasks /Create`
to an HKCU\Software\Microsoft\Windows\CurrentVersion\Run entry
when Group Policy blocks the Scheduled Task path. Both modes are
per-user, no admin, persist across logoffs. The chosen mode is
recorded in ~/.claude-mailbox/autostart-mode so status/start/stop/
uninstall-autostart pick the right cleanup path.
Also bumps the npm version to 1.0.1 to align with the published 1.0.0
plus this patch.
The plugin's UserPromptSubmit and SessionStart hooks call `claude-mailbox`
with no --url flag, so they previously always hit the hardcoded
http://127.0.0.1:47822/mcp default. If port 47822 was held by another
local service (e.g. ClaudeDo), the daemon couldn't bind there and every
hook was talking to the wrong process.
CLI default for --url now resolves to $CLAUDE_MAILBOX_URL when set,
falling back to http://127.0.0.1:47822. Doctor gained a Step 2 that
probes /health on 47822, identifies foreign occupants, picks a free
port, writes both ~/.claude-mailbox/mailbox.json and the
CLAUDE_MAILBOX_URL entry in .claude/settings.json env so the hooks
follow along automatically.
Also adds a fallback hint when Windows schtasks /Create fails with
Access is denied (Group Policy restricts non-admin task creation): run
install-autostart from an elevated shell, or accept an ephemeral serve
for the current session.
session-announce now calls /v1/list with the session's X-Mailbox header,
which both registers the session with the daemon and returns all known
mailboxes in one round-trip. The output appends an "Active peers" block
listing mailboxes seen within the last hour (configurable via
--peer-window-minutes), capped at 10 entries by default. Self is
filtered out; the list is sorted most-recent-first.
So when the user says "I started a second session, coordinate with it",
Claude already has the peer's mailbox name in context — no manual
list_mailboxes call needed.
The peer-formatting logic is extracted into formatActivePeerList for
unit testing; CLI tests now pin --url to an unreachable port to keep
assertions stable on machines that have a real daemon running.
MCP tools (send/check_inbox/peek_inbox/list_mailboxes) now accept the
caller's mailbox name as an explicit argument (from/name), falling back
to the X-Mailbox header for legacy single-session HTTP setups. This
unblocks multi-session coordination through a shared .mcp.json — each
Claude session passes its own session-derived name on every call,
instead of relying on a single transport header that all sessions
would share.
The plugin now ships .mcp.json (no header), and the SessionStart
announcement spells out the exact args to pass to each mcp__mailbox__*
tool so Claude wires it up automatically.
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
The doctor command runs entirely inside Claude Code and walks through:
binary install via npm, daemon autostart, mailbox-name prompt with write
to per-project `.claude/settings.json` env, and a self → self smoke test.
Also adds `/claude-mailbox:mailbox-status` as a read-only health check.
Reduces the colleague onboarding to: add marketplace, install plugin,
run the doctor — no terminal context-switch required.
Adds a /plugin marketplace at the repo root and a `claude-mailbox` plugin under
plugin/ that wires the UserPromptSubmit hook without needing the per-user
`install-hook` step. The hook command (`claude-mailbox check --hook`) now reads
the mailbox name from $CLAUDE_MAILBOX_NAME when --name is omitted and emits a
one-line setup hint when the daemon is unreachable, so a missing daemon is loud
instead of invisible.
The plugin only contains the Claude Code glue — the daemon binary is still a
separate prerequisite (`npm i -g @kuns/claude-mailbox` + install-autostart),
and the plugin/README plus main README spell out the three-step setup.
Adds `install-hook` / `uninstall-hook` subcommands that idempotently patch
~/.claude/settings.json (or .claude/settings.json with --project), plus a
`--hook` flag on `check` that emits human-readable output and stays silent
on empty inbox or unreachable daemon.
The auto-generated secrets.GITEA_TOKEN lacks write:package scope,
causing npm publish to fail with E401. Use a dedicated repo secret
NPM_PUBLISH_TOKEN with a personal access token that has write:package.
Introduces @kuns/claude-mailbox under node/, a wire-compatible TypeScript
port of the .NET daemon that distributes via the public Gitea npm registry.
The .NET project stays in src/ClaudeMailbox/ untouched; users pick whichever
flavor they prefer.
- node/ project: fastify + @modelcontextprotocol/sdk StreamableHTTPServerTransport
+ better-sqlite3, schema and wire surface match the C# version (port 47822,
X-Mailbox header, MCP tool names, snake_case SQLite columns)
- Cross-platform autostart: Scheduled Task (Win, no admin) / Windows Service
(Win, --service) / launchd (mac) / systemd --user (linux)
- 9/9 vitest tests pass; end-to-end /health + send/check round-trip verified
- CI split: existing ci.yml/release.yml renamed to *-dotnet.yml with path
filters, new ci-node.yml and release-node.yml publish to Gitea npm registry
- install.ps1 / install.sh bootstrap one-liners at repo root; homebrew/
contains a tap formula template
- README install section reordered: npm path primary, dotnet publish secondary
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>