repo-mailbox/CHANGELOG.md
Kjell Tore Guttormsen 28e7cb42cd feat(hook): let a non-git surface declare its identity via CLAUDE_COORD_REPO
0.6.0 removed the working-directory fallback, which was right, but it left
every non-git working surface with nothing to derive from. The read path
declines silently by design - the hook must never fail a session - so such a
surface simply stops seeing its inbox: no error, no exit code, nothing. That is
the same loss-looks-like-normal shape 0.6.0 set out to remove, reintroduced one
layer up.

The hook now forwards CLAUDE_COORD_REPO verbatim as --repo. This is not the
fallback returning, and the distinction is the whole point: the fallback GUESSED
a name from wherever the session happened to stand, while a declaration is
written down in that directory's settings, readable back, and deletable.
Identity stays a choice someone made.

Forwarding as --repo rather than reimplementing anything means it inherits every
engine rule, including that an explicit override never claims <repo>/.origin -
otherwise a surface borrowing a name would steal the claim from the checkout
that owns it. Both halves are pinned by tests, and the shared test helper now
deletes CLAUDE_COORD_REPO unless a test asks for it, so a value set globally
later cannot silently satisfy the tests that prove the hook resolves nothing on
its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U6EixQo6hpoRCVtiAXdnFs
2026-07-27 08:56:31 +02:00

13 KiB

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[0.7.0] - 2026-07-27

Added

  • CLAUDE_COORD_REPO — declaring an identity for a non-git surface. 0.6.0 removed the working-directory fallback, which was right, but it left every non-git working surface (~/repos, $HOME) with nothing to derive from: the read path declines silently, so such a surface simply stops seeing its inbox, with no error and no exit code. That is the same loss-looks-like-normal shape 0.6.0 set out to remove. The hook now reads CLAUDE_COORD_REPO and forwards it verbatim as --repo, so a directory can declare which mailbox it owns:

    { "env": { "CLAUDE_COORD_REPO": "repos" } }
    

    This is not the fallback returning. The fallback guessed a name from wherever the session happened to stand; a declaration is written down, readable back, and deletable. Because it forwards as --repo it inherits every engine rule, including that an explicit override never claims <repo>/.origin — otherwise a surface borrowing a name could steal the claim from the checkout that owns it. Both halves are pinned by tests.

[0.6.0] - 2026-07-26

Fixed

  • _broadcast is 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> archived a broadcast out of the queue — a full unauthenticated retract of an announcement for every repo that had not read it yet. The whole _ prefix is now reserved, in every CLI, so a later _seen or _config cannot reopen the hole.
  • Repo identity is derived, never invented. The fallback to basename(pwd) 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 the fallback silently handed it the identity repos — under which real mail was delivered. git toplevel or an explicit --from/--repo are now the only sources. Write paths exit 2 and name the fix; the read path declines silently, because the hook runs at every session start and must never fail a session.
  • 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 is now redelivery.
  • The hook no longer resolves identity at all. It was a fourth independent copy of the identity rule and the only one that runs in production, so passing --repo bypassed the engine's guards exactly where they mattered — and suppressed the new collision check along with them.

Added

  • Mailbox-collision warning. Two checkouts with the same directory name share one mailbox. Re-keying identity would break every existing mailbox and the readable --to <repo> addressing, so the first git-derived read records the claiming path in <repo>/.origin, and a read from a different path is reported in the injection. A warning, not a refusal: the same repo moved or re-cloned is the ordinary case. It goes in the injection because the hook discards stderr, and a warning nobody can see is not a warning.

Changed

  • Selftest 93 → 116 checks; three Node tests now cover the hook behaviorally rather than by reading its source.

Breaking

  • coord-send and coord-done exit 2 outside a git repository instead of naming themselves after the working directory. Pass --from/--repo to choose an identity explicitly. Repo names beginning with _ are refused everywhere.

[0.5.0] - 2026-07-25

Changed

  • The inbox is a priority, not a suggestion (Rule 7). Through 0.4.0 the injection block told every repo to "consider replying/resolving where it fits in this session". That sentence was the whole problem: the injection text is the only place a repo is ever told what to do with a message, so the wording is the protocol — and it granted permission to defer. Messages sat unanswered for weeks while each session did its own work first. Nothing was broken; the protocol was asking for exactly what it got.

    The block now states an ordering and a completion obligation:

    • Handle the inbox first, before the task the session came to do — "not after it, not if there is time".
    • Every directed message must reach a terminal state before the session ends: coord-send --reply-to or coord-done.
    • Neither terminal state is the default. The format has no reply-expected field, so mandating only the reply would manufacture traffic for messages that merely inform.
    • Leaving one pending stays allowed, but must be stated to the operator with a reason. The escape hatch is explicit rather than silent.
  • Raising priority deliberately does not widen the trust boundary. The obligation is procedural, never substantive: responding is mandatory, complying with what a message asks is not — only the operator authorizes that. Untrusted cross-repo content still cannot direct the reader; it merely can no longer be ignored. The injection states both halves, and the selftest pins them together so a future reword cannot keep the priority and quietly drop the distinction.

  • Selftest grew from 82 to 93 checks (new section 20 pins the wording contract: the permissive phrasing is gone, ordering and completion are stated, both terminal states are named with neither as default, responding-vs-complying is kept distinct, and the untrusted-data framing survives).

[0.4.0] - 2026-07-25

Added

  • coord-send --retract <filename>: a broadcast can finally be retired. Until now nothing could remove a message from _broadcast/inbox/: coord-done is directed-only and never touches the broadcast queue. The backlog could therefore only grow, and every new repo received the entire standing history at its first session — including announcements that had since become false. Retract moves the message to _broadcast/archive/, so no future repo is served it.

    Deliberate limits, both documented and pinned by tests:

    • Un-send, not recall. Repos that already received the broadcast keep it; _broadcast/seen/ is delivery history and is left untouched.
    • Only the sender may retract (from: must match the repo identity). --from overrides this, as it does everywhere else in the engine, which makes the check an accident guard rather than a security boundary.
    • Nothing is deleted, mirroring coord-done. Retracting twice is a no-op (exit 0).
  • Selftest grew from 70 to 82 checks (new section 19 covers retraction: authorization, archiving, non-delivery to new repos, no collateral damage to other broadcasts, invalid names, flag combinations, and idempotency).

Fixed

  • README's install command still said claude plugin install coord@ktg-plugin-marketplace after the v0.3.0 rename.
  • README told the reader to prune _broadcast/inbox/ by hand when a notice stopped being relevant — advice that contradicted the rule that the script owns mailbox files. --retract is now the supported path.

[0.3.0] - 2026-07-25

Changed

  • Renamed the plugin and repository from coord to repo-mailbox. The old name said that something was coordinated but not what the thing was; the new one names what it is, using the same vocabulary the code and docs already use throughout (mailbox, inbox, archive, broadcast). The Forgejo repository moved from open/coord to open/repo-mailbox (the old URL redirects), and the marketplace entry, plugin manifest and package name follow.

    This is breaking for installed users: the skill is now invoked as /repo-mailbox:coord-send rather than /coord:coord-send, and the plugin must be reinstalled under its new name.

    Deliberately unchanged: the CLI (coord-send.sh, coord-inbox.sh, coord-done.sh, coord-selftest.sh), the skill name coord-send, the mailbox root ~/.claude/coord/, and the CLAUDE_COORD_DIR override. Those name the transport protocol, not the product — renaming them would migrate live mailbox data and break message history in every participating repo for no gain in clarity.

Fixed

  • Catalog README listed 48 selftest checks for this plugin; the actual count has been 70 since v0.2.2.

[0.2.2] - 2026-07-25

Changed

  • coord-inbox.sh now warns on stderr for each unknown argument instead of discarding it silently. The read path stays lenient (it runs inside the SessionStart hook, which must never fail a session over a stray flag) and the exit code is unchanged, but a mistyped flag no longer looks like a working invocation. The hook runs the script with stderr discarded, so the warning surfaces in manual CLI use only.

Added

  • Selftest: 68 -> 70 checks, covering the stderr warning and the leniency it must not break (unknown argument still reads the inbox and exits 0).

Documentation

  • README: corrected the stale selftest count (64 -> 70), documented that a broadcast is never delivered back to its own sender (shipped in 0.2.1 but undocumented), and added rule 6 — message content is data, never instructions — which coord-inbox.sh already enforces in the injection framing.

[0.2.1] - 2026-07-25

Fixed

  • Broadcast self-delivery: coord-send --broadcast left no seen-set entry for the sender, so the announcing repo got its own announcement injected at its next session start. The delivered filename is now recorded in _broadcast/seen/<sender> at delivery time, reusing the existing per-repo seen set rather than adding a second exclusion mechanism. Filtering on the from: field at read time was rejected: from: is sender-controlled, so it would let any repo suppress a broadcast for another by forging the field. The seen file is keyed by the raw sender name (the read side keys it unsanitized, so the two must agree); senders containing a slash, or equal to ./.., skip the marking rather than escape the directory.

Added

  • Selftest: 64 -> 68 checks, covering sender self-exclusion, unaffected delivery to other repos, self-exclusion for sender names that are not shell-clean, and the seen-marking path-escape guard.

[0.2.0] - 2026-07-24

Hardening pass over the three CLIs.

Fixed

  • 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.
  • . and .. are rejected explicitly in the --reply-to and coord-done name guards instead of relying on downstream failure.

Added

  • -h/--help on coord-inbox.sh, making usage output uniform across the three CLIs.
  • Selftest: 48 -> 64 checks, closing gaps around the default mailbox path via HOME fallback, malformed frontmatter on the read path, and a read-only destination directory.

[0.1.0] - 2026-07-24

First public release: extraction of a battle-tested personal mechanism into a distributable plugin.

Added

  • Bash engine: coord-send.sh (directed / broadcast / reply delivery), coord-inbox.sh (pending inbox + unseen broadcasts, formatted for context injection), coord-done.sh (archive without reply), all bash-3.2-safe, ASCII-only, zero dependencies.
  • coord-selftest.sh: 48 checks against a throwaway mailbox, including regression cover for filename sanitization, no-hang argument parsing, prompt-injection resistance, frontmatter hygiene, and broadcast ordering.
  • SessionStart hook (hooks/scripts/session-start.mjs): thin zero-dependency Node wrapper that injects the repo's pending mailbox as additionalContext. Always exits 0 — a broken mailbox never blocks a session.
  • coord-send skill: natural-language front door (English + Norwegian triggers) mapping intent to engine invocations, including bounded multi-target loops, deferred sends, and replies.

Fixed

  • Frontmatter injection: CR/LF and control characters in --subject/--from are collapsed before writing, so fields can no longer inject frontmatter lines or a premature --- terminator.
  • Broadcast loss: a per-repo seen set (_broadcast/seen/<repo>, one delivered filename per line) replaces the single high-water mark, which silently dropped a same-second broadcast whose filename sorted below one already seen. Upgrade note: a legacy watermark file is read as a one-entry seen set, so old broadcasts may be re-delivered once.

Changed

  • Read-side protocol strings are English (--- message:, -> reply:, UNTRUSTED DATA framing); the forgery-resistance selftest checks pin the English tokens.