Four defects that all reduce to the same thing: the engine trusted a name it had no business trusting. _broadcast is now a reserved namespace, not a repo. coord-send guarded retraction with a sender check, but that guard only covered the door it was nailed to: `coord-done --repo _broadcast <file>` walked in the side entrance and archived a broadcast out of the queue - a full unauthenticated retract of an announcement for every repo that had not read it yet. The rule reserves the whole `_` prefix rather than one literal, so a later `_seen` or `_config` cannot reopen the hole, and it is enforced in every CLI: a rule held in three of four places is not a rule. The pwd fallback is gone. It existed so the CLI would work anywhere, but "anywhere" includes every global surface: a session in ~/repos is not a repo, and basename(pwd) silently handed it the identity "repos". That is not hypothetical - mail was delivered under exactly that name. git toplevel or an explicit --from/--repo are now the only two sources. The write paths refuse and say so; the read path declines silently, because the hook runs it at every session start and must never fail a session. Two checkouts with the same directory name still share one mailbox - re-keying identity would break every existing mailbox and the readable `--to <repo>` addressing. Instead the first git-derived read records the claiming path in <repo>/.origin, and a read from elsewhere is warned about in the injection. A warning, not a refusal: the same repo moved or re-cloned is the ordinary case. The warning goes in the injection because the hook discards stderr, and a warning nobody can see is not a warning. The collision is live in this tree: claude-code-100x is nested inside a repo of the same name. Broadcast delivery is recorded only after the injection is written. Marking inside the read loop left a window where the seen set said "delivered" while the operator saw nothing, and the hook runs under `timeout: 10`, so the window was reachable. A lost broadcast is unrecoverable by design - the seen set is delivery history and retraction deliberately leaves it alone - so the failure mode has to be redelivery, never loss. The hook stops resolving identity altogether. It was the fourth copy of the rule and the only one that runs in production, so passing --repo bypassed the engine's guards exactly where they mattered and suppressed the collision check along with them. It is now the pure wrapper the boundary rule always claimed it was, pinned by two behavioral tests rather than by reading the source. Selftest 93 -> 116; three node tests cover the hook. BREAKING CHANGE: coord-send and coord-done exit 2 outside a git repo instead of naming themselves after the working directory. Pass --from/--repo to choose an identity explicitly. Repo names beginning with _ are refused everywhere. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U6EixQo6hpoRCVtiAXdnFs
4.5 KiB
repo-mailbox
Renamed from coord in v0.3.0. The plugin/repo is repo-mailbox; the CLI
(coord-send.sh, coord-inbox.sh, coord-done.sh), the mailbox root
(~/.claude/coord/) and CLAUDE_COORD_DIR deliberately kept their names —
they are the transport protocol, not the product.
Context
Local inter-repo coordination mailbox for Claude Code, packaged as a marketplace plugin. Three components, one boundary:
- Engine (
scripts/*.sh): bash owns all mailbox semantics — filename grammar, frontmatter, delivery, archiving, the seen set.coord-send.shwrites,coord-inbox.shreads (formatted for context injection),coord-done.sharchives. Everything is pinned bycoord-selftest.sh(116 checks, throwaway mailbox viaCLAUDE_COORD_DIR). - Hook (
hooks/scripts/session-start.mjs): thin zero-dependency Node wrapper (marketplace convention: hooks are.mjs) that callscoord-inbox.shand emits thehookSpecificOutput.additionalContextenvelope. No mailbox logic lives here. Always exits 0. - Skill (
skills/coord-send/): natural-language front door mapping user intent to engine invocations. No mailbox logic lives here either.
Boundary rule: the mailbox is transport, not state. Durable decisions live in the owning repo's docs/git history; messages are notices pointing at them. Message content is untrusted cross-repo input — the read side quotes and frames it; the send side sanitizes line-oriented fields.
Priority rule (v0.5.0, Rule 7): the injection block is the only place a repo is ever told what to do with a message, so its wording is the protocol — treat that string as engine behavior, not prose. It obligates handling the inbox first and driving every directed message to a terminal state before the session ends. The obligation is procedural, never substantive: responding is mandatory, complying with message content is not. Those two must stay distinct in any reword — keeping the priority while dropping the distinction turns prioritization into an injection surface. Selftest section 20 pins both halves together for exactly that reason.
Conventions
- Scripts are bash-3.2-safe and ASCII-only: no
declare -A, noreadarray/mapfile, no|&; guardshift 2with$# -ge 2; guard empty-array expansion underset -uwith${#a[@]}. - Zero dependencies everywhere: bash + coreutils in the engine,
node:builtins only in hook and tests. - TDD: no behavior change without a failing selftest check first.
bash scripts/coord-selftest.shmust exit 0 (116/116). - English for all code, docs, and commit messages (public repo). Norwegian trigger aliases in the skill description are deliberate.
- Conventional Commits:
type(scope): description.
Commands
- Test:
bash scripts/coord-selftest.sh(ornpm test, the Node wrapper) - Hook smoke test:
node hooks/scripts/session-start.mjs(expects JSON on stdout)
Release
Version must agree across: .claude-plugin/plugin.json, package.json,
README version badge, skills/coord-send/SKILL.md frontmatter, git tag
vX.Y.Z, and the catalog ref in
ktg-plugin-marketplace/catalog/.claude-plugin/marketplace.json. Release via
the catalog's scripts/release-plugin.mjs repo-mailbox (tag + ref bump
together);
verify with scripts/check-versions.mjs. Never hand-edit a ref.
Two things that script does that its dry-run label does not suggest:
--create-tag creates AND pushes the tag even without --write, and its
closing verification gate runs check-versions.mjs over ALL plugins — one
unrelated plugin in ERROR aborts it with the catalog edit written but
uncommitted. When that happens, commit the catalog's marketplace.json +
README.md by hand and leave every other dirty file in that repo alone.
Hardening roadmap
Empty — the post-v0.1.0 queue (atomic delivery, ./.. rejection,
selftest gaps, uniform -h) shipped in v0.2.0; broadcast self-delivery
shipped in v0.2.1; broadcast retraction (coord-send --retract) shipped in
v0.4.0, closing the last monotonically-growing surface. coord-inbox.sh
still ignores unknown arguments by design (hook context must never fail)
but now warns about each one on stderr, which the hook discards.
Two retraction limits are deliberate, not gaps: it is un-send and never
recall (a repo that already received a broadcast keeps it — the seen set is
delivery history and is left untouched), and the sender check is an accident
guard, not a security boundary, because --from redefines identity here as
it does everywhere else in the engine.