--- type: trekresearch-brief created: 2026-07-25 question: "How can a local-only inter-repo message mailbox deliver to repositories that are not currently being worked in, and what is this design missing?" confidence: 0.82 dimensions: 7 mcp_servers_used: [tavily] local_agents_used: [architecture-mapper, claude-code-guide] external_agents_used: [docs-researcher, community-researcher, security-researcher, contrarian-researcher] gemini_bridge: skipped (deep-research not requested) --- # Delivering to repositories nobody has open > Generated by trekresearch v1.0 on 2026-07-25 > **Snapshot note.** This brief describes the engine as of v0.4.0 and is left as > written. Since then v0.5.0 shipped the Rule 7 priority contract, and the > selftest moved from 82 to 93 checks — read every "82 checks" below as the count > at research time. Recommendation 4 (`reply-expected`) is now *coupled* to that > release rather than merely desirable: the obligation shipped on a format that > still cannot express "this one needs no answer." ## Research Question Delivery in `repo-mailbox` is 100% recipient-initiated: a message is only ever seen when a human starts a Claude Code session in that specific repository. A repository untouched for weeks never learns it owes a reply, and round-trip latency is both unbounded and invisible. What existing solutions address this? Specifically: can a sweeper running outside any repository surface pending mail across all inboxes, and can a stale repository answer autonomously without a human opening it? ## Executive Summary The cross-repo **digest** is well-founded, cheap, and blocked by one concrete engine defect nobody had noticed: reading is a *mutating* operation, so a naive `--all` would consume every repository's broadcast backlog on their behalf. The **autonomous reply** should not be built as specified — not on probabilistic grounds but architectural ones: the reply is itself the consequential action, it is unrecallable by design, and the ingestion path (a hook) sits *outside* the only isolation Claude Code's built-in sandbox provides, which is Anthropic's own documented position. Confidence is high on the mechanism inventory and the security analysis (vendor docs and CVEs, fetched directly), lower on the operational-threshold numbers, which are largely absent from the literature; the key caveat is that four verified defects in the *current* engine surfaced during this research and are worth fixing regardless of which direction is chosen. ## Dimensions ### 1. Delivery initiation — what can act outside a repository at all -- Confidence: high **Local findings:** - `SessionStart` in the recipient repository is the sole automatic delivery trigger. VERIFIED: `hooks/hooks.json:3-12` declares exactly one hook, and `hooks/scripts/session-start.mjs:35-39` is the only caller of the read path. No daemon, no cron, no other hook, no statusline integration exists. - The plugin's hook has no `matcher` (VERIFIED `hooks/hooks.json:4-5`), so it is registered for all `SessionStart` sources. - A statusline already exists as an integration surface at the user level (`statusLine.command` in user settings). It currently has no mailbox awareness. VERIFIED by settings inspection. **External findings:** - **No hook in Claude Code 2.1.220 fires without a Claude Code process starting in that project.** All 30 hook events were enumerated from ; the earliest (`Setup`) is triggered by *starting* Claude Code. VERIFIED by enumeration; note the doc contains no explicit sentence stating the requirement, so this rests on trigger semantics. - `FileChanged` + `watchPaths` looks like an exception but is not confirmed to be one: `watchPaths` is an *output of the `SessionStart` hook*, and whether the watcher survives outside a running process is **not documented — not verified**. - The mechanism inventory splits cleanly on local-vs-cloud: | Mechanism | Local? | Unattended? | Verdict | |---|---|---|---| | `claude -p` (headless) | Yes | Yes, if something invokes it | Viable; **no documented cron/launchd pattern exists** | | Routines / `/schedule` | **No — Anthropic cloud** | Yes | **Disqualified.** "Access to local files: No (fresh clone)"; requires claude.ai login + GitHub; research preview; no permission-mode picker | | `CronCreate` / `/loop` | Yes | **No** — "Tasks only fire while Claude Code is running and idle" | Cannot solve a stale repo | | Desktop scheduled tasks | Yes | Yes, while the app runs and the machine is awake | Viable; per-task permission mode; macOS build is Universal (Intel OK) | | `claude --bg` / agent view | Yes | Yes — "Background sessions don't need any terminal open" | Viable; **research preview**; conflicts with `-p` | | Statusline | Yes | No — session-only display | Surface, not a trigger | | Notifications | Hook+`osascript` local; push is cloud | No | **"Claude Code has no mechanism that notifies you when no session exists anywhere."** | All rows VERIFIED against pages cited in Sources. **Contradictions:** none. Local code and vendor docs agree that nothing in either layer reaches a repository nobody has open. The only local-and-unattended substrates are OS-level (launchd) or Desktop scheduled tasks. ### 2. The missing engine primitive — and the blocker inside it -- Confidence: high **Local findings:** - There is **no cross-repo read**. `coord-inbox.sh` resolves exactly one `$REPO` and constructs exactly two paths from it: `INBOX="$COORD/$REPO/inbox"` (`:45`) and `SEEN_FILE="$SEEN_DIR/$REPO"` (`:72`). No loop over `$COORD/*` exists. VERIFIED. - **The blocker: reading consumes.** `coord-inbox.sh:87` appends each emitted broadcast to the repository's seen file *as a side effect of reading*: `mkdir -p "$SEEN_DIR" && printf '%s\n' "$fname" >> "$SEEN_FILE"`. A sweeper that called `coord-inbox.sh --repo X` for every X merely to *count* pending mail would mark every repository's broadcast backlog as delivered, and those broadcasts would never be shown to anyone. VERIFIED. This is the single largest structural obstacle to reusing the existing script, and it makes a cross-repo digest a **refactor plus a new mode**, not a one-line flag. - Sizing: gating `:87` behind a delivery flag is ~5-8 changed lines; a separate `coord-status.sh` enumerating `$COORD/*/` is ~50-70 new lines; an in-place `--all` is ~35-50 lines plus the seen-gating refactor. New selftest checks: ~10-14. ASSUMED (estimate), derived from VERIFIED structure. - Enumeration is not trivial: the mailbox root also contains regular files (`README.md`, `register.md`), and `_broadcast` must be excluded explicitly. VERIFIED. - **Age is free.** The filename prefix is fixed-width UTC (`coord-send.sh:154-156`, `%Y%m%dT%H%M%SZ`), and `LC_ALL=C` (`coord-inbox.sh:16`) makes the glob chronological. Per-repo oldest-pending-age is computable from a directory listing with no file I/O and no parsing. VERIFIED. **External findings:** - Maildir solved the same shape 30 years ago and its answer is directly transferable: **delivered-vs-unseen is encoded by *directory* (`new` vs `cur`), while seen / replied / flagged are *filename flags* within `cur`.** Critically, `R` (replied-to) is a distinct flag from `S` (seen) — maildir does not collapse "I looked at it" and "I answered it." VERIFIED, (Courier formulation; djb's original at cr.yp.to was unreachable and is **not** cited). - The prior art says the reply-owed state is *a flag on an already-delivered message*, not a separate queue and not a property of the mailbox. **Contradictions:** none. ### 3. Notification surfaces -- Confidence: high on mechanics, medium on the choice **Local findings:** the existing `SessionStart` injection is the only surface wired up. Extending it to carry a cross-repo line is the smallest possible change and adds no new failure mode. ASSUMED (design inference from §1 VERIFIED facts). **External findings:** - **zsh startup semantics, VERIFIED** (`zsh(1)`, zsh 5.9, read on macOS 26.5.1): `~/.zshenv` is sourced by **every** zsh invocation including non-interactive scripts — every `zsh -c`, every shebang, every tool that shells out pays the cost. `~/.zshrc` is interactive-only; `~/.zprofile`/`~/.zlogin` are login-only. A pending-count line belongs in `.zshrc` or `.zprofile`, **never** `.zshenv`. The documented idiom for guarding is `[[ -o interactive ]]` / `[[ -o login ]]`. - **`osascript -e 'display notification'`, partially VERIFIED:** `osascript(1)` ships on macOS 26.5.1, but `display notification` is documented only in an *archived* Apple guide (2016-06-13). Attribution goes to the script or to Script Editor — there is no documented parameter to name it otherwise. There is **no documented way to attach a click action**; the only documented click behavior is "opens the app that displayed the notification." It is silenceable by Focus and by Script Editor's per-app toggle. Whether it works at all from a LaunchAgent context is **not verified** — no official source addresses it. - Claude Code's own documented macOS path is the `Notification` hook invoking `osascript`, with a documented gotcha: if Script Editor lacks notification permission "the command fails silently, and macOS won't prompt you to grant it." VERIFIED, . - **A plugin cannot ship the main statusline.** VERIFIED, : a plugin's `settings.json` supports "only the `agent` and `subagentStatusLine` keys." Installing a statusline requires a user-scope settings edit outside the plugin manifest. - **Habituation generalizes across surfaces.** Habituation to warnings sets in after only 2-3 exposures, measured by fMRI and eye-tracking, and — the finding that matters for a four-surface plan — it *carries over to novel stimuli similar in appearance*, so frequent non-essential notifications degrade response to important warnings the user has never seen before. VERIFIED, (SOUPS 2019), (CHI 2017). - **A count is not actionable.** Google SRE: "Every page should be actionable… If a page merely merits a robotic response, it shouldn't be a page." VERIFIED, . Applicability caveat stated by the source itself: this is interrupt-driven paging, not async notices; the transferable part is the *tests*, not the numbers. - **Convergent prior art on ambient surfacing:** todo.txt prompt integrations, independently reinvented in 2012 (zsh) and 2022 (fish), both render **an integer, never content**, and both **render nothing at zero**. VERIFIED, , . **Contradictions:** the operator's stated preference is all four surfaces; the evidence says ship one. This is a genuine tension and is resolved in the Recommendation, not here. ### 4. Autonomous reply — the trust boundary -- Confidence: high This is the decisive dimension. **Local findings:** - The project's own boundary rule states the mailbox is "transport, not state." An auto-reply makes the mailbox *generate* state. VERIFIED, `CLAUDE.md`. - `coord-done` exists so a message can be marked handled **without** replying — no-reply is a documented terminal state, not a defect. VERIFIED, `CLAUDE.md`, `scripts/coord-done.sh`. - Retraction is documented as "un-send and never recall." A delivered reply cannot be withdrawn from its recipient. VERIFIED, `CLAUDE.md`, `coord-send.sh:95-96`. - **The propagation vector needs no exploit.** An unattended agent in any repository is *already an authorized writer* to every other repository's mailbox via `coord-send`. Spreading requires no race, no symlink, no permission bug — only the intended happy path. A broadcast reaches all repositories in one write. VERIFIED by code structure. **External findings:** - **Anthropic's own documentation is unambiguous.** All VERIFIED by direct fetch of and : - "The sandboxed Bash tool on its own constrains only Bash, so **it is not sufficient for fully unattended runs in either mode**." - "**Always run `--dangerously-skip-permissions` sessions inside a container, a VM, or the sandbox runtime**, so that file tools, MCP servers, and hooks are also inside the boundary." - Under the built-in Bash sandbox, "**MCP servers and hooks are separate processes that run unconstrained on the host**." *The mailbox's ingestion path is a hook — i.e. the untrusted content enters through the one path the built-in sandbox does not cover.* - Auto mode's classifier "is a **per-action control, not an isolation boundary**"; permission rules are "a permission gate, not a sandbox." - "**`allowed_tools` does not constrain `bypassPermissions`.**" - **The governing principle from the peer-reviewed literature:** "once an LLM agent has ingested untrusted input, it must be constrained so that it is **impossible** for that input to trigger any consequential actions." VERIFIED, (Beurer-Kellner et al., *Design Patterns for Securing LLM Agents against Prompt Injections*, 2025). **In this design the auto-reply is itself the consequential action** — it writes under the operator's identity into another repository's trust boundary. - **OWASP LLM01:2025 Prompt Injection** is the #1 entry in the current version; named preventions include privilege control and "human approval for high-risk actions." VERIFIED, . - **NIST AI 100-2 E2025** takes the same position: assume injection succeeds, constrain by architecture. VERIFIED (metadata directly; ), body passage retrieved via search index and flagged as such by the researching agent. - **The recurring precondition in Anthropic's own advisories is "the ability to add untrusted content into a Claude Code context window."** Eleven Claude Code advisories were fetched individually from NVD/GHSA; the closest analogues: - CVE-2026-55607 (High, 7.7) — sandbox escape via git worktree path confusion, overwriting `~/.zshenv`; **required the user to run Claude Code against a repo containing prompt-injection content**. - CVE-2026-54316 (Moderate, 6.0) — out-of-band exfiltration through a *pre-approved* domain, no prompt shown. - CVE-2025-54794 (High, 7.7) — path restriction bypass via prefix matching instead of canonical comparison. - CVE-2025-55284, CVE-2025-64755, CVE-2025-54795 — all gated on injecting untrusted content into the context window. A mailbox that injects cross-repo content into an agent's context *is* that precondition, automated. - **The closest published analogue** is "Comment and Control" (Apr 2026): PR titles and issue bodies hijacking agents in GitHub Actions, confirmed against Claude Code Security Review, Gemini CLI Action and GitHub Copilot Agent, **auto-triggered by workflow events with no victim action**. The researcher's own generalization: "The pattern likely applies to any AI agent that ingests untrusted data and has access to execution tools in the same runtime as production secrets." VERIFIED. - **The lethal trifecta is fully closed here:** private data (repo contents, `~/.claude`, credentials) + untrusted content (the message, which this project's own docs label untrusted) + egress (`git push`, network commands, **and `coord-send` itself**). VERIFIED framing, . - **Published mitigation patterns that actually apply** (arXiv:2506.08837; CaMeL ; dual-LLM ): Action-Selector (untrusted content selects among N fixed non-consequential actions and cannot author output), Plan-Then-Execute (the reply's shape is fixed *before* ingestion), Dual LLM (a tool-less quarantined component processes the untrusted text and produces a draft). CaMeL quantifies the cost: 77% of AgentDojo tasks solved with guarantees vs 84% undefended — **security costs utility, measurably.** - **The one gate that always runs:** hooks precede every other permission step, and "a hook deny applies even in `bypassPermissions` mode." VERIFIED, . A `PreToolUse` hook is therefore the only reliable choke point and the natural place to emit an audit record. **Contradictions:** none between sources. The contradiction is between the evidence and the stated goal, and it is stated plainly in the Recommendation. ### 5. Loop and fan-out control -- Confidence: high Email learned this the hard way, and the countermeasures are specified, not folklore. **This is the section that makes a safe auto-acknowledgement possible at all.** **External findings — all VERIFIED:** - **RFC 3834** (Standards Track, 2004, ) exists precisely because of "mail loops or 'sorcerer's apprentice mode'." It *permits* automatic responders subject to conditions: | # | Requirement | Strength | |---|---|---| | 1 | Not the same response to the same sender more than once in several days; **7 days RECOMMENDED** | SHOULD | | 2 | Do not respond to any message carrying `Auto-Submitted` with **any value other than `no`** | SHOULD | | 3 | Include `Auto-Submitted: auto-replied` on your own responses | SHOULD | | 4 | **MUST NOT** respond where the destination would be a null address | **MUST NOT** | Requirements 2 and 3 are the pairing that makes the scheme terminate: *mark your output as machine-originated, and refuse to reply to anything so marked.* - **`vacation(1)`'s actual mechanism** is unglamorous: a keyed database of senders already responded to, default interval one week; `-r 0` means at most one reply ever. VERIFIED, . - **RFC 5230** formalizes it and adds the sleeper detail: the dedupe key is *(sender, response-identity)*, not sender alone, and **the minimum suppression window is clamped up, not bypassable** — the spec anticipated that implementors would set it too low. VERIFIED, . - **The counting valve — four independent systems, 1982→2020, all with two layers:** header-based suppression *plus* an unconditional counter. RFC 5321 §6.3 ("servers **MUST** contain provisions for detecting and stopping trivial loops"), Mailman `max_autoresponses_per_day` default 10, Zendesk 20/hr then reject at 40, Exchange Online reply-all storm protection. Mailman's docs state the rationale outright: "Mailman **already** inhibits automatic replies to any message labeled… **This is a fallback safety valve.**" That is a maintainer saying header suppression is not sufficient. - **A counter is only as strong as the identity it keys on.** Zendesk documents its own failure mode: "This limitation *won't* work if the other system doesn't use the same email address every time." **This lands directly on this project: `--from` redefines identity here, and `CLAUDE.md` already records the retract sender check as "an accident guard, not a security boundary."** A per-sender counter under a caller-declarable identity is an accident guard too. The mechanism that does *not* depend on peer identity is a hop/generation counter carried in the message — which is what SMTP chose. - **Broadcast is fan-out, not recursion** — multiplication, which a per-pair dedupe window does not bound. Documented storms: Reuters 2015 (~23M messages in 7 hours), Atos 2015 (379 messages → >34.5M), NHS England 2016 (~186M). VERIFIED via with contemporaneous press citations. A named *autoresponder-initiated* postmortem is **NOT VERIFIED** — vendors document the defenses, not the incidents. ### 6. Operational durability of a background job -- Confidence: high **External findings:** - **`StartInterval` and `StartCalendarInterval` behave oppositely on a laptop that sleeps**, documented only in the man page. VERIFIED, `launchd.plist(5)` read on macOS 26.5.1: `StartInterval` — "If the system is asleep during the time of the next scheduled interval firing, **that interval will be missed**"; `StartCalendarInterval` — "launchd will start the job the next time the computer wakes up. If multiple intervals transpire… those events will be **coalesced into one event**." Consequence: the sweeper cannot infer elapsed time from the fact that it ran. - **`WatchPaths` is discouraged by Apple, in Apple's own words.** VERIFIED, verbatim: "Use of this key is **highly discouraged**, as filesystem event monitoring is highly race-prone, and it is entirely possible for modifications to be missed." Whether it recurses into subdirectories is **not documented** in either official source — reported as undocumented, not as non-recursive. - **`QueueDirectories` is the wrong primitive for a retained mailbox.** VERIFIED: it "keeps the job alive as long as the directory… [is] not empty." It models a *drain queue*; a mailbox that deliberately retains messages until they are marked handled satisfies the keep-alive condition permanently. - **A launchd job's PATH is not the login-shell PATH.** VERIFIED by composition: `/etc/zprofile` runs `path_helper`, and `zsh(1)` states `/etc/zprofile` is read only by login shells. Note the widely-repeated claim "launchd jobs do not inherit the interactive shell environment" is **NOT VERIFIED** in any Apple source. Practical consequence either way: absolute paths for every binary. Also VERIFIED: launchd redirects stdio to `/dev/null` unless `StandardOutPath`/`StandardErrorPath` are set. - **TCC: not resolved.** `~/.claude/` is **not among** Apple's enumerated protected locations (Documents, Downloads, Desktop, iCloud Drive, network volumes) — VERIFIED as an absence. But no official source states that a home path outside those locations needs no approval, and `launchd.plist(5)` CAVEATS warns that "privacy sensitive files and folders in a launchd plist may not have the desired effect, and **may prevent the job from running**." Reported as an open gap, not filled. - **Jobs stop firing after OS upgrades — five independent reports across four macOS releases** (Catalina, Monterey, Ventura, plus a 2025 comment). VERIFIED as a pattern: "After upgrading, that LaunchDaemon started getting 'Operation not permitted' errors… Full Disk Access for the daemon got disabled by the upgrade." The common shape is that **the job does not report that it stopped.** - **The canonical way a background job's failure becomes invisible is that cron mails its output to a local mailbox nobody opens** — structurally the identical failure to "a message delivered to a repo nobody opens." A sweeper whose errors go to an unread channel reproduces the very bug it was built to fix, one level up. VERIFIED as a consistent claim across four independent sources (individually low-authority monitoring blogs; the convergence is the evidence). - **The established countermeasure is inverted alerting** (dead man's switch): alert on the *absence* of a success signal. "It keeps silent as long as pings arrive on time." VERIFIED, . Note the tension: the switch's own alert must land somewhere structurally different from where the job's ordinary output lands, or it returns to the unread-channel problem. **Local findings:** - The project's only correctness evidence is 82 synchronous selftest checks against a throwaway mailbox via `CLAUDE_COORD_DIR` (VERIFIED, `coord-selftest.sh:13-16`). That harness can pin filename grammar, frontmatter, delivery and archiving because they are pure and file-local. It **cannot** pin whether launchd fired, whether TCC granted access this boot, or whether two runs overlapped. A dead sweeper leaves all 82 checks green. ARGUMENT, derived from VERIFIED structure. **Contradictions:** none. ### 7. Message model gaps -- Confidence: high **Local findings — all VERIFIED:** - The frontmatter schema is exactly four fields, written at `coord-send.sh:165-173`: `from`, `to`, `subject`, `date`. The read side parses only `from:` and `subject:` (`coord-inbox.sh:54-55`). **`date:` and `to:` are never read by anything.** - **No priority field, no deadline, no reply-expected field, no age or expiry handling anywhere.** The `-> reply:` hint at `coord-inbox.sh:60` is emitted *unconditionally for every directed message*, so it carries zero signal about whether a reply is actually wanted. - **No reply linkage.** There is no `in-reply-to`, no thread id, no message id beyond the filename. `--reply-to` archives the original but writes nothing into the reply that references it; the only trace of a thread is the `Re:` subject convention, which is a heuristic and is overridable. Consequence: given an archived message you cannot mechanically determine *whether* it was replied to. - The only signal that a message is still owed something is that it sits in `inbox/` rather than `archive/` — and that cannot distinguish "expects a reply and hasn't got one" from "purely informational and nobody ran `coord-done`". **This is precisely the field a pending-reply digest most needs and does not have.** - Repo identity is `basename` of the git root, derived independently in **four** places (`coord-inbox.sh:34-37`, `coord-send.sh:48-51`, `coord-done.sh:31-34`, and again in JS at `session-start.mjs:28-36`) with no shared helper. Override is per-invocation only (`--repo`/`--from`); there is no identity env var. ## Local Context ### Four defects found in the current engine These surfaced during architecture analysis and are independent of the sweeper decision. Defect 1 is VERIFIED BY EXECUTION in a throwaway mailbox (`CLAUDE_COORD_DIR`); defects 2-4 are VERIFIED by code path. 1. **`_broadcast` is reserved only on the send side — reproduced.** `coord-send.sh:130` rejects `--to _broadcast`, but `coord-inbox.sh` has no such guard. Reproduced end to end against an isolated mailbox: - `coord-inbox.sh --repo _broadcast` reads the broadcast queue as a *directed* inbox. The same message is rendered **twice** in one output — once under `--- message: … ---` with a `-> reply:` / `coord-done` affordance, and again under `--- broadcast: … ---` — and the header reports "2 unread/unhandled" for what is one message. Exit 0. - `coord-done.sh --repo _broadcast ` reports "1 message(s) archived", exits 0, and the file is gone from `_broadcast/inbox/` and present in `_broadcast/archive/`. A subsequent read from a fresh repository identity no longer receives it. That is a **complete, unauthenticated retract** — the same end state `coord-send --retract` produces, reached without the sender check at `coord-send.sh:85-88`. Any enumerating sweeper must exclude `_broadcast`, and the engine should reject it on the read side regardless. Note this is an accident surface of the same class the project already documents for `--from`, not a privilege boundary being broken — but unlike `--from` it is undocumented and reachable by a plausible typo. 2. **The pwd-fallback silently captures broadcasts under a bogus identity.** With no git root, identity falls back to `basename(pwd)` (`coord-inbox.sh:36`). The live mailbox contains a seen-file for a *non-repository parent directory* — a session started there consumed broadcasts under a name no repository will ever use again. Those broadcasts are burned. A sweeper launched from `$HOME` would do this systematically. 3. **Basename collisions share one mailbox.** Two repositories at different absolute paths with the same basename share `$COORD//inbox` and one seen file. A directed message goes to whichever starts a session first; a broadcast consumed by one is marked seen for the other. Nothing detects it — there is no registration and no path recorded anywhere. At least one such collision exists in the current working set. 4. **Broadcasts are marked seen before output is printed.** `coord-inbox.sh:87` marks inside the emit loop; `:93` prints only at the end. The hook has a hard `"timeout": 10` (`hooks/hooks.json:9`). If the process is killed mid-loop, broadcasts already iterated are permanently marked seen while nothing was ever emitted. (That Claude Code kills the hook process at timeout is ASSUMED.) ### Concurrency Delivery is atomic against *crash* — `mktemp` inside the destination directory, dot-prefixed to stay out of the reader's `*.md` glob, then a same-filesystem `mv` (`coord-send.sh:160-176`, pinned by selftest §14). VERIFIED, and `rename(2)` on macOS guarantees "an instance of `new` will always exist, even if the system should crash in the middle of the operation." But there is **no concurrency control at all**: no `flock`, no lockfile, no `noclobber`, no `mkdir`-as-mutex. VERIFIED by grep. Every existing guarantee assumes one reader per identity at a time — true when the only reader was a `SessionStart` hook, and violated by a sweeper by construction. The concrete race is a TOCTOU on the seen set (`coord-inbox.sh:77` tests, `:87` appends, with message formatting in between), which for a sweeper is the *common* case rather than an edge case. Note also that `mkdir`-as-mutex is **NOT VERIFIED** as atomic in either POSIX or macOS `mkdir(2)` — both document only `EEXIST`. ## External Knowledge ### Best practice Assume injection succeeds and constrain by architecture (NIST, OWASP, arXiv:2506.08837). For automatic responders specifically, the standards are explicit and old: mark your own output as machine-generated, refuse to respond to anything so marked, keep a per-(sender, response-identity) suppression window with a non-bypassable minimum, and add an unconditional counter as a fallback because header suppression is known to be insufficient. ### Alternatives The genuinely cheap alternatives, none of which requires a scheduler: - **Sender-side responsibility.** The operator's continuity convention already has a mechanism whose entire job is to re-raise what must not be forgotten: the STATE.md next-step block, injected at every session start. If a sending repository needs a reply, the *sender* records that dependency in its own STATE.md. This targets the **active** repository the operator is actually visiting, rather than chasing the stale one they are not. ARGUMENT. - **A manually-invoked read-only digest.** One command, synchronous, in front of the operator, inside the existing bash boundary, no scheduler, no autonomy, no silent-death mode. - **Draft-not-send.** An unattended component prepares a reply but does not deliver it; the operator releases it. This removes the egress leg and breaks the trifecta while keeping most of the latency benefit. ### Security Covered in Dimension 4. The compressed version: an allowlist alone is insufficient by Anthropic's own documentation; the ingestion path is a hook and hooks run outside the built-in Bash sandbox; the propagation vector is intended functionality rather than an exploit; and `--from` means any per-sender rate limit is an accident guard, not a boundary. ### Known issues - Automation bias: participants followed **wrong** automated recommendations in ~65% of cases, with a 41% omission rate versus 3% unaided; training reduced commission errors but not omission errors, and two-person crews were no better than individuals. VERIFIED for the *mechanism*; the percentages come from flight-simulation tasks and must **not** be transplanted as a rate for this setting. , . The relevance: an auto-reply does not land in a vacuum — it is injected into the receiving repository's next session as context, pre-formatted and carrying machine authority. - Self-assessment of agent-assisted productivity is unreliable in this exact population: METR's RCT with 16 experienced developers on their own repositories found them **19% slower** while believing they were 20% faster. VERIFIED, . N=16, early-2025 tooling, wide CI — cited for the perception gap, not as proof that agents are net-negative. Consequence: "coordination feels better now" cannot be the success criterion. - Both git-native options fail as **silent non-delivery**: `git notes` are "not fetched or pushed by default" and require an explicit refspec; githooks are not transferred by clone and "hooks that don't have the executable bit set are ignored." The receiver observes an empty state indistinguishable from "nothing was sent" — the transport reproduces the very problem. VERIFIED, , . - Taskwarrior's maintainers name exactly these risks — "race conditions, **infinite loops** and runaway cascading effects" — and then ship only JSON validation. VERIFIED, . ## Gemini Second Opinion Not used. The `gemini-bridge` agent was deliberately skipped: the session's harness policy is that deep-research tooling is used only on explicit request, and the operator asked for research generally, not for Gemini specifically. This removes one independent triangulation path; the four external agents and the directly-fetched vendor documentation are the substitute. Noted as a limitation rather than silently omitted. ## Synthesis **The problem splits at the word "reply," and the two halves have opposite verdicts.** The digest reduces *ignorance* — the operator does not know a message is waiting. The autonomous reply reduces *latency* — the operator knows but has not answered. Only the first is the stated complaint. That distinction is what makes the recommendation asymmetric rather than a compromise. **The most useful new fact is local, not external.** Every plausible sweeper design assumed `coord-inbox.sh` could be called per-repo to count pending mail. It cannot: reading consumes broadcasts (`:87`). This was invisible from the outside and would have been discovered *after* the first sweep silently ate every repository's backlog. The seen-gating refactor is a precondition for anything in family (c), digest or agent. **The security objection is architectural, not probabilistic, and it composes badly with this project's own design choices.** Three of them: - The mailbox's ingestion path is a **hook**, and Anthropic documents hooks as running *outside* the built-in Bash sandbox. The one thing the sandbox does not cover is exactly the thing this design uses to ingest untrusted content. - `coord-send` is itself an egress channel, so the trifecta closes without the agent ever touching the network. Removing network access does not help. - Propagation requires no exploit. An injected agent is an authorized writer to every mailbox, and one broadcast reaches all of them. The worm shape is the happy path. **The 20-year-old email standards are the missing engineering, and they are missing on the read side too.** RFC 3834's terminating pair — mark your output `Auto-Submitted: auto-replied`, refuse to reply to anything marked — is exactly what this message format cannot express, because the format has four fields and none of them says "machine-generated." Note the sharper point: the *same* absent field, a reply-expected marker, is what the digest needs to count pending debt. **One schema addition unlocks both halves**, and it is additive and backward compatible because unknown frontmatter keys are simply not parsed today. **But the counting valve must not key on `from`.** Zendesk documents the failure ("won't work if the other system doesn't use the same email address every time"), and this project's own CLAUDE.md already concedes that `--from` redefines identity and that the sender check is "an accident guard, not a security boundary." A per-sender limit inherits that weakness exactly. SMTP's choice — a hop counter carried *in the message*, independent of peer identity — is the one that survives here. **Finally, the failure mode of the fix is the failure mode being fixed.** A dead sweeper and a quiet mailbox produce byte-identical output: nothing. The 82 selftest checks stay green. cron's canonical silent-death mode is that its output goes to a mailbox nobody reads — structurally identical to a message delivered to a repository nobody opens. Any background component must therefore carry a dead man's switch whose alarm lands on a *different* surface from its ordinary output, or it will reproduce the original bug one level up while the operator believes it is solved. ## Open Questions - **Does the operator actually have unanswered messages that mattered?** No measurement exists. The discriminating counts are cheap and read-only: messages sent per week; how many genuinely required a reply; how many never got one; and how many caused a downstream consequence. If the last number is zero, the premise itself is unsupported and the correct action is to build nothing. This is the single highest-value thing to establish before writing code. - **Would a stale repository even have enough context to answer correctly?** The agent would compose from that repository's STATE.md and git HEAD, which cannot know about decisions the operator has since made elsewhere. This is not fixable by running the sweeper more often. Cheap test: take the last few messages that needed replies and check whether the receiving repository's state at that moment contained enough to answer. If it did not, the autonomous half is unbuildable as specified regardless of every other consideration. - **Does `osascript display notification` work from a LaunchAgent context?** Undocumented; not tested. Determines whether the macOS surface is available at all without a third-party dependency. - **Does a LaunchAgent reading `~/.claude/` need a TCC grant?** Apple's enumeration does not include it, but absence from an enumeration is not a grant, and the man page warns privacy protections "may prevent the job from running." - **Does `FileChanged`/`watchPaths` survive outside a running Claude Code process?** Not documented. If it does, it changes the answer in Dimension 1. - **Whether Desktop scheduled tasks work with no account/network** — not documented, and it decides whether the one first-party local-and-unattended scheduler is usable under the privacy constraint. ## Recommendation ### RECOMMEND 1. **Fix the four engine defects (Local Context).** Independent of everything else. `_broadcast` rejected on the read side is a correctness *and* safety fix; the pwd-fallback should refuse rather than invent an identity; basename collisions should at minimum warn; and the seen-mark should move after emit or the timeout window should be documented. Cost: small, TDD-shaped, entirely inside the existing boundary. **This is the part with no downside.** 2. **Split reading from consuming** (`coord-inbox.sh:87` behind a delivery flag). Precondition for everything else, ~5-8 lines plus checks. 3. **Add a read-only cross-repo digest**, manually invoked. It is synchronous, inspectable, testable by the existing harness, has no scheduler and no autonomy, and it addresses the actual complaint — not knowing something is waiting. Age is already free from the filename prefix. Prefer a separate script so the injection path the 82 checks pin hardest stays untouched. 4. **Add a reply-expected marker and an `auto-submitted`-equivalent field to the frontmatter.** Additive, backward compatible, and it unlocks both halves: the digest can finally count *debt* rather than *unarchived messages*, and any future automation has the terminating pair RFC 3834 specifies. Follow maildir's lesson and keep replied-to distinct from seen. 5. **Ship exactly one notification surface first: extend the existing `SessionStart` injection with a cross-repo line.** Zero new code paths, zero new failure modes, and it fires when the operator is in a position to act. The habituation evidence says additional surfaces degrade the one that works, so add a second only after naming a specific occasion when the first was insufficient. If a second is added, `~/.zshrc` (an integer, nothing at zero, per the todo.txt convention) is the best-evidenced candidate — **never** `~/.zshenv`. **Do not implement this one standalone.** It makes the hook read N inboxes under the hard `"timeout": 10`, which multiplies the exposure window of defect 4 (broadcasts marked seen before output is printed) by the number of repositories. It is gated behind item 2 for that reason. ### RECOMMEND AGAINST, as specified 6. **Do not build unattended autonomous *sending*.** Three independent lines converge, and two of them come from this project's own documentation rather than outside opinion: it makes the mailbox generate state, violating the stated transport boundary; its errors are unrecallable by design while its only benefit is lower latency (wrong is permanent, slow is temporary); and it closes the lethal trifecta on the host in the configuration Anthropic documents as container-only, through a hook — the one ingestion path the built-in sandbox does not cover. 7. **Do not add a background scheduler yet, independently of the agent.** It converts an inspectable synchronous tool into a distributed system whose failure is byte-identical to success, on a platform with five documented cases of jobs silently losing permission across OS upgrades, and it moves the interesting behavior out from under the only correctness evidence the project has. If one is added later, it needs a dead man's switch reporting on a different surface from its ordinary output. ### The viable middle, if latency really is the problem 8. **Draft, do not send.** An unattended component may prepare a reply into a staging area that the operator releases. This removes the egress leg — the trifecta does not close — and keeps most of the latency benefit. Combined with: - **Action-Selector for anything that does send:** a fixed vocabulary of pre-written, non-semantic acknowledgements ("received, queued, a human will answer"), *selected* by classification but never *authored* from message content. Untrusted content then cannot compose output. - **The RFC 3834 terminating pair:** mark machine-generated output; never auto-respond to anything so marked. - **A hop counter carried in the message**, not a per-sender counter — because `--from` redefines identity and per-sender counting degrades to no counting. - **A hard propagation cap:** a message that arrived from another repository must never cause a send to a third, plus a per-sweep budget. This is the anti-worm rule, and it is the one that matters most. - **A `PreToolUse` hook as the single always-running gate** (it denies even under `bypassPermissions`) and the natural place to write the audit record. - **Real isolation if it ever sends unattended:** container/VM or the sandbox runtime, because hooks and MCP servers run unconstrained under the built-in Bash sandbox. **Sequencing:** items 1-2 are unconditionally worth doing. Item 3 is the 90% solution. Items 4-5 are cheap and make everything after them possible. Item 8 is a design to hold in reserve, and it should not start until the measurement in Open Questions shows a non-zero downstream cost from missed replies. ## Sources | # | Source | Type | Quality | Used in | |---|---|---|---|---| | 1 | `scripts/coord-inbox.sh`, `coord-send.sh`, `coord-done.sh`, `coord-selftest.sh`, `hooks/` | codebase | high | 1, 2, 7, Local Context | | 2 | | official | high | 1 | | 3 | | official | high | 1 | | 4 | | official | high | 1 | | 5 | | official | high | 1 | | 6 | | official | high | 1 | | 7 | | official | high | 1, 4 | | 8 | | official | high | 3 | | 9 | | official | high | 3 | | 10 | | official | high | 4 | | 11 | | official | high | 4 | | 12 | | official | high | 4 | | 13 | | official | high | 4 | | 14 | | official | high | 4 | | 15 | | official | high | 4 | | 16 | | peer-reviewed | high | 4 | | 17 | (CaMeL) | peer-reviewed | high | 4 | | 18 | | community | medium | 4 | | 19 | | community | medium | 4 | | 20 | GHSA-7835-87q9-rgvv (CVE-2026-55607) | official | high | 4 | | 21 | GHSA-fg94-h982-f3mm (CVE-2026-54316) | official | high | 4 | | 22 | GHSA-pmw4-pwvc-3hx2 (CVE-2025-54794) | official | high | 4 | | 23 | GHSA-x5gv-jw7f-j6xj (CVE-2025-55284) | official | high | 4 | | 24 | GHSA-4vp2-6q8c-pvq2 (CVE-2026-46406) | official | high | 4 | | 25 | | community | medium | 4 | | 26 | | official | high | 5 | | 27 | | official | high | 5 | | 28 | §6.3 | official | high | 5 | | 29 | | official | high | 5 | | 30 | | official | high | 2 | | 31 | | official | high | 5 | | 32 | | official | high | 5 | | 33 | | community | medium | 5 | | 34 | `launchd.plist(5)`, macOS 26.5.1 | official | high | 6 | | 35 | `zsh(1)` 5.9, macOS 26.5.1 | official | high | 3 | | 36 | `rename(2)`, `mkdir(2)`, `mv(1)`, `fsync(2)`, macOS 26.5.1 | official | high | Local Context | | 37 | Apple *Mac Automation Scripting Guide* (archived 2016-06-13) | official | medium | 3 | | 38 | | peer-reviewed | high | 3 | | 39 | | peer-reviewed | high | 3 | | 40 | | official | high | 3 | | 41 | | community | medium | External Knowledge | | 42 | , | peer-reviewed | high | External Knowledge | | 43 | , | official | high | External Knowledge | | 44 | | official | high | External Knowledge | | 45 | | official | medium | 6 | | 46 | , | community | medium | 6 | | 47 | , | community | medium | 3 | ### Explicitly not verified Carried forward rather than filled in: `WatchPaths` recursion; launchd runtime PATH (the *widely-repeated* claim is unsourced; the narrower composed claim is verified); TCC requirements for `~/.claude/`; whether `display notification` works from a LaunchAgent; whether `FileChanged`/`watchPaths` survives outside a session; whether Desktop scheduled tasks work without an account; a named autoresponder-initiated storm postmortem; a developer-tooling notification volume threshold; `mkdir`-as-mutex atomicity; APFS-specific rename guarantees beyond `rename(2)`'s own contract; djb's original maildir `link()` formulation.