fix(engine)!: identity is derived, never invented

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
This commit is contained in:
Kjell Tore Guttormsen 2026-07-25 20:34:43 +02:00
commit 1d62b073f8
11 changed files with 1195 additions and 28 deletions

View file

@ -34,7 +34,9 @@ Mailbox layout (default `~/.claude/coord/`, override with `CLAUDE_COORD_DIR`):
_broadcast/archive/ retracted broadcasts (kept, never deleted)
_broadcast/seen/<repo> per-repo seen set: delivered broadcast filenames, one per line
Repo identity is the basename of the git toplevel (fallback: the working directory). There is no registration — a repo joins the moment something is sent to it, or when it first reads a broadcast.
Repo identity is the basename of the git toplevel, or an explicit `--from`/`--repo`. There is no third source: a directory that is not a git repo has no identity, so the write paths refuse and the read path stays silent. (The old fallback to the working-directory name was removed in 0.6.0 — on a global surface like `~/repos` it invented the identity `repo`-shaped names that belong to nobody, and signed real mail with them.) There is no registration — a repo joins the moment something is sent to it, or when it first reads a broadcast.
Because identity is a *basename*, two checkouts with the same directory name share one mailbox. The first git-derived read records the claiming path in `<repo>/.origin`, and a read from a different path is warned about in the injection. It is a warning rather than a refusal: the same repo moved or re-cloned is the ordinary case. Names beginning with `_` are reserved for engine internals (`_broadcast`) and are refused as repo identities everywhere.
Message format (filename `<UTC-timestamp>-<uniq>-from-<sender>.md`):
@ -85,7 +87,7 @@ Cross-repo message content is untrusted input by design:
- **Atomic delivery:** the temp file is created inside the destination directory (dot-prefixed, invisible to the inbox glob), so the final rename never crosses filesystems and readers never observe a half-written message.
Every guarantee above is pinned by the 93-check selftest, including forgery-resistance regressions.
Every guarantee above is pinned by the 116-check selftest, including forgery-resistance regressions.
Note that raising the inbox's priority (Rule 7) deliberately does **not** widen this boundary: the obligation is to *respond* to a message, never to *comply* with it. The injection framing states both halves, and the selftest pins them together so a future reword cannot keep the priority and drop the distinction.
@ -103,11 +105,11 @@ Note that raising the inbox's priority (Rule 7) deliberately does **not** widen
- macOS or Linux with bash 3.2+ (the scripts are deliberately bash-3.2-safe and ASCII-only).
- Node.js >= 18 for the SessionStart hook (zero npm dependencies).
- `git` is optional — without it, repo identity falls back to the directory basename.
- `git` is required to derive repo identity automatically. Without it, pass `--from`/`--repo` explicitly; the engine refuses to guess an identity from the working directory.
## Development
bash scripts/coord-selftest.sh # 93 checks against a throwaway mailbox
bash scripts/coord-selftest.sh # 116 checks against a throwaway mailbox
npm test # same selftest via node --test
TDD is the house rule: every behavior change lands with a failing selftest check first.