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

@ -0,0 +1,777 @@
---
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
<https://code.claude.com/docs/en/hooks>; 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 <https://code.claude.com/docs/en/> 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,
<https://man.freebsd.org/cgi/man.cgi?query=maildir&sektion=5> (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, <https://code.claude.com/docs/en/hooks-guide>.
- **A plugin cannot ship the main statusline.** VERIFIED,
<https://code.claude.com/docs/en/plugins-reference>: 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,
<https://www.usenix.org/conference/soups2019/presentation/vance> (SOUPS 2019),
<http://library.usc.edu.ph/ACM/CHI%202017/1proc/p2215.pdf> (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,
<https://sre.google/sre-book/monitoring-distributed-systems>. 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,
<https://blog.xargs.io/2012/05/27/todo-txt-count-in-rprompt-zsh>,
<https://www.seanh.cc/2022/11/04/todo.txt>.
**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 <https://code.claude.com/docs/en/sandbox-environments> and
<https://code.claude.com/docs/en/agent-sdk/secure-deployment>:
- "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,
<https://arxiv.org/abs/2506.08837> (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,
<https://genai.owasp.org/llmrisk/llm01-prompt-injection/>.
- **NIST AI 100-2 E2025** takes the same position: assume injection succeeds,
constrain by architecture. VERIFIED (metadata directly;
<https://csrc.nist.gov/pubs/ai/100/2/e2025/final>), 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,
<https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/>.
- **Published mitigation patterns that actually apply** (arXiv:2506.08837;
CaMeL <https://arxiv.org/abs/2503.18813>; dual-LLM
<https://simonwillison.net/2023/Apr/25/dual-llm-pattern/>):
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,
<https://code.claude.com/docs/en/agent-sdk/permissions>. 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,
<https://www.rfc-editor.org/rfc/rfc3834.txt>) 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,
<https://man.freebsd.org/cgi/man.cgi?query=vacation&sektion=1>.
- **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, <https://www.rfc-editor.org/rfc/rfc5230.txt>.
- **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 <https://en.wikipedia.org/wiki/Email_storm> 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, <https://healthchecks.io/docs/>. 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 <file>` 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/<basename>/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. <https://d-nb.info/1223023044/34>,
<https://pubmed.ncbi.nlm.nih.gov/11543300/>. 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,
<https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/>.
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,
<https://git-scm.com/docs/git-notes>, <https://git-scm.com/docs/githooks>.
- Taskwarrior's maintainers name exactly these risks — "race conditions,
**infinite loops** and runaway cascading effects" — and then ship only JSON
validation. VERIFIED, <https://taskwarrior.org/docs/hooks>.
## 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 | <https://code.claude.com/docs/en/hooks> | official | high | 1 |
| 3 | <https://code.claude.com/docs/en/routines> | official | high | 1 |
| 4 | <https://code.claude.com/docs/en/scheduled-tasks> | official | high | 1 |
| 5 | <https://code.claude.com/docs/en/desktop-scheduled-tasks> | official | high | 1 |
| 6 | <https://code.claude.com/docs/en/agent-view> | official | high | 1 |
| 7 | <https://code.claude.com/docs/en/headless> | official | high | 1, 4 |
| 8 | <https://code.claude.com/docs/en/statusline> | official | high | 3 |
| 9 | <https://code.claude.com/docs/en/plugins-reference> | official | high | 3 |
| 10 | <https://code.claude.com/docs/en/sandbox-environments> | official | high | 4 |
| 11 | <https://code.claude.com/docs/en/agent-sdk/secure-deployment> | official | high | 4 |
| 12 | <https://code.claude.com/docs/en/agent-sdk/permissions> | official | high | 4 |
| 13 | <https://code.claude.com/docs/en/sandboxing> | official | high | 4 |
| 14 | <https://genai.owasp.org/llmrisk/llm01-prompt-injection/> | official | high | 4 |
| 15 | <https://csrc.nist.gov/pubs/ai/100/2/e2025/final> | official | high | 4 |
| 16 | <https://arxiv.org/abs/2506.08837> | peer-reviewed | high | 4 |
| 17 | <https://arxiv.org/abs/2503.18813> (CaMeL) | peer-reviewed | high | 4 |
| 18 | <https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/> | community | medium | 4 |
| 19 | <https://simonwillison.net/2023/Apr/25/dual-llm-pattern/> | 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 | <https://www.securityweek.com/claude-code-gemini-cli-github-copilot-agents-vulnerable-to-prompt-injection-via-comments/amp> | community | medium | 4 |
| 26 | <https://www.rfc-editor.org/rfc/rfc3834.txt> | official | high | 5 |
| 27 | <https://www.rfc-editor.org/rfc/rfc5230.txt> | official | high | 5 |
| 28 | <https://datatracker.ietf.org/doc/html/rfc5321> §6.3 | official | high | 5 |
| 29 | <https://man.freebsd.org/cgi/man.cgi?query=vacation&sektion=1> | official | high | 5 |
| 30 | <https://man.freebsd.org/cgi/man.cgi?query=maildir&sektion=5> | official | high | 2 |
| 31 | <https://docs.mailman3.org/projects/mailman/en/latest/src/mailman/config/docs/config.html> | official | high | 5 |
| 32 | <https://support.zendesk.com/hc/en-us/articles/4408836366362-About-mail-loops-and-Zendesk-email> | official | high | 5 |
| 33 | <https://en.wikipedia.org/wiki/Email_storm> | 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 | <https://www.usenix.org/conference/soups2019/presentation/vance> | peer-reviewed | high | 3 |
| 39 | <http://library.usc.edu.ph/ACM/CHI%202017/1proc/p2215.pdf> | peer-reviewed | high | 3 |
| 40 | <https://sre.google/sre-book/monitoring-distributed-systems> | official | high | 3 |
| 41 | <https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/> | community | medium | External Knowledge |
| 42 | <https://d-nb.info/1223023044/34>, <https://pubmed.ncbi.nlm.nih.gov/11543300/> | peer-reviewed | high | External Knowledge |
| 43 | <https://git-scm.com/docs/git-notes>, <https://git-scm.com/docs/githooks> | official | high | External Knowledge |
| 44 | <https://taskwarrior.org/docs/hooks> | official | high | External Knowledge |
| 45 | <https://healthchecks.io/docs/> | official | medium | 6 |
| 46 | <https://apple.stackexchange.com/questions/394471>, <https://discussions.apple.com/thread/253354341> | community | medium | 6 |
| 47 | <https://blog.xargs.io/2012/05/27/todo-txt-count-in-rprompt-zsh>, <https://www.seanh.cc/2022/11/04/todo.txt> | 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.

View file

@ -0,0 +1,163 @@
---
type: feature-survey
created: 2026-07-25
question: "What features do tools in active use ship so a human does not have to actively monitor a queue?"
scope: complements 2026-07-25-cross-repo-delivery-research.md (which covered Unix/email prior art)
---
# Features that remove the need to watch
Companion to `2026-07-25-cross-repo-delivery-research.md`. That brief surveyed
Unix and email prior art (maildir, biff, RFC 3834, cron/launchd). It explicitly
reported the "what do comparable projects actually ship" question as **not
answered**. This closes that gap.
## The reframe that makes everything tractable
> With no daemon, no scheduler and no network, **nothing in this system can fire
> on its own** — the only moment code runs is when the developer opens some
> repository. So the discriminating question per feature is not "does it need a
> daemon?" but **"can it be computed at read time from timestamps and files
> already on disk?"**
Every aging and escalation tool surveyed (`actions/stale`, PagerDuty,
`probot/no-response`) depends on an external clock. **Their mechanics transfer;
their triggering does not.** A state machine evaluated at read time needs no
clock of its own — only a comparison against `now`.
## Candidate features
VERIFIED = the cited page was fetched and says this. Defaults are quoted only
where documented.
| Feature | Tool | Mechanism | Fit |
|---|---|---|---|
| **Dependency Dashboard** — one issue summarizing all pending work, body rewritten every run via `ensureIssue({title, reuseTitle, body})`, sectioned into Pending Approval / Awaiting Schedule / Rate-Limited / Errored / Open / … | Renovate | one always-current artifact instead of N notices | **direct** |
| **Dashboard as input device** — "click on a checkbox below" turns the summary into the control surface | Renovate | read and act in the same object | **direct** |
| **Severity bypasses the caps** — "Security update pull requests are not subject to this limit and do not count toward it" | Dependabot | a priority field exempts an item from batching | **direct** |
| **One continuously-updated comment** — found by a hidden header marker, not by search; modes `append`/`recreate`/`delete`/`hide`/`only_create` | sticky-pull-request-comment | identity-by-marker | **direct** |
| **Living release PR** — one PR updated as new changesets land | changesets/action | same pattern, different substrate | **direct** |
| **Read and done are orthogonal**`is:read` "doesn't include notifications marked as Done"; `PATCH` marks read, `DELETE` marks done. Two verbs, two axes | GitHub | seen ≠ resolved | **direct** |
| **Unread-only by default**`all` defaults to `false` | GitHub REST | the primitive behind any pending count | **direct** |
| **Typed `reason` on every item** — 14 values (`mention`, `review_requested`, `state_change`, …) | GitHub | triage keys off *why*, not just *what* | **direct** |
| **One command, all repos**`gh status` prints assigned issues, PRs, review requests, mentions and activity across every subscribed repo in one view | gh CLI | **the closest analogue to the actual complaint** | **direct** |
| **Auto-de-escalation**`remove-stale-when-updated` defaults to `true`; any activity clears the mark | actions/stale | mtime change recomputes at read time | **direct** |
| **Gap detection instead of guaranteed delivery** — monotonic event IDs; missed events "can be detected by noting a discontinuity in the event IDs" | Syncthing | proves at read time that nothing was silently dropped | **direct** |
| **Snooze that resurfaces itself** — 1/4/8/24h or custom, max 168h; only on *acknowledged* incidents; on expiry the incident "returns to a triggered state and notifies you again" | PagerDuty | store a resurface timestamp, compare at read time | **adapts** |
| **Grouping** — "All updates sharing the same `groupName` will be placed into the same branch/PR"; Dependabot `groups`: first matching rule wins | Renovate, Dependabot | N items become 1 | **adapts** |
| **Schedule gates the *action*, not the *run*** — `schedule` restricts "times… during which Renovate may create or update branches and PRs" | Renovate | a gate evaluated inside a run survives having no scheduler | **adapts** |
| **Concurrency cap**`prConcurrentLimit` default `10`; Dependabot raises max 5 and "no further pull requests are raised until some… are merged or closed" | Renovate, Dependabot | cap what is *displayed*, not what is *stored* | **adapts** |
| **Age gate before auto-action**`minimumReleaseAge` (default `null`); maintainers recommend `"14 days"` before automerging third-party deps | Renovate | age-gated automatic disposal | **adapts** |
| **Persisted cursor replayed on restart** — triggers "are saved and re-established across a Watchman process restart", re-evaluated from the last captured clock | Watchman | needs the daemon; the cursor idea does not | **needs daemon** |
| **Aging → stale → close**`days-before-stale: 60`, `days-before-close: 7` | actions/stale | state machine fits, triggering does not | **needs daemon** |
| Hourly rate caps; `operations-per-run: 30` | Renovate, actions/stale | need a clock / a rate limit that does not exist locally | **no fit** |
## Failure modes — the findings that change the design
**The living-summary pattern breaks on *identity*, not on content.** Renovate
discussion #12131: a repository got a *new* Dependency Dashboard roughly once an
hour. `ensureIssue` locates the existing dashboard by searching for issues
**authored by the bot account**; custom auth meant issues were created under a
different account, the lookup returned zero, and every run created a fresh
dashboard. **Design constraint: the summary artifact must live at a fixed,
deterministic path and must never be located by searching on a mutable
attribute.**
**Its second failure mode is latency, and it reads as breakage.** Discussion
#9905: users reported dashboard checkboxes "doing nothing". The checkbox is a
*request queued for the next run*, not an immediate action. An input surface on a
passively-regenerated artifact is inherently deferred — **and it must say so, or
deferral is read as failure.**
**Never auto-close on age.** This is the single most-criticized behavior in the
survey. The signal argument (fvsch.com/stale-bots, Feb 2023) is that auto-closing
destroys the queue's meaning: "Closed" stops distinguishing fixed from rejected
from arbitrarily hidden, and closed items drop out of default search, so users
"will not find that identical issue which was auto-closed" and file duplicates.
Sentiment is genuinely split — maintainers defend it on capacity grounds — but
capacity is not the constraint for a single operator. **Escalate presentation
only.**
**A silent cap makes a queue look shorter than it is.** `actions/stale`'s own
README warns that with `operations-per-run: 30` "you might end up with unprocessed
issues or pull requests after a stale action run". Any cap must be *stated* in the
summary, never silent.
**Maintainers' own defaults are not the shipped defaults.** Renovate recommends
`config:best-practices` over `config:recommended`, and the Dependency Dashboard
option itself defaults to `false` while the onboarding preset turns it on.
**Nobody has solved this for agents.** `avivsinai/agent-message-queue` (~76
stars) is a genuine close analogue — Maildir `tmp``new``cur` semantics, atomic
rename, no daemon by default — and its awareness story is still "poll, or run a
monitor, or use an experimental terminal-injection wake hack". It does **not**
solve the monitoring problem either. Claude Code's own agent messaging is
in-session, not cross-session. LangChain's Agent Inbox is server-backed. **No
surveyed project ships a daemon-free mechanism that makes an unopened
repository's backlog visible from elsewhere.** The strongest transferable idea
came from outside the space entirely: `gh status`.
## Ranked shortlist — cheapest first
Each passes the test: computable at read time, from what is already on disk, with
no daemon and no clock of its own.
1. **Surface every mailbox at every session start, not just the current
repository's.** (`gh status`.) The only item that attacks the stated complaint
head-on — an unopened repository is invisible precisely because injection is
repo-scoped. The enumerate-all logic already exists; what changes is *where its
output appears*. Must use a plain file count, **never** the consuming read
path, and must exclude `_broadcast`.
2. **One living summary at a fixed path, regenerated on every read.** Three
independent tools converge on this. Address it by deterministic path, never by
search.
3. **Seen and done as two independent states.** Without it the pending count
becomes a lie the moment something is read without being acted on — and the
count is the whole basis of item 1. Default view shows un-acted-on only.
4. **Explicit defer-until-timestamp that resurfaces itself.** The self-returning
version of "leaving one pending". Copy two constraints: cap the deferral, and
permit it **only on items already acknowledged** — which is why item 3 lands
first.
5. **Age computed at read time, escalating presentation, automatic
de-escalation.** An adaptation — no surveyed tool ships this daemon-free. Take
the state machine and both guardrails (any touch clears the escalation;
exemptions never age). Do not take auto-close.
**Explicitly not recommended:** hourly rate caps, scheduled digest windows, and
auto-close — the first two need a clock this system does not have, the third is
the most-criticized behavior in the survey.
## Sources
Renovate: [configuration options](https://docs.renovatebot.com/configuration-options/),
[schema](https://docs.renovatebot.com/renovate-schema.json),
[dependency-dashboard.ts](https://raw.githubusercontent.com/renovatebot/renovate/main/lib/workers/repository/dependency-dashboard.ts),
[presets](https://docs.renovatebot.com/presets-config/),
[best practices](https://docs.renovatebot.com/upgrade-best-practices/),
[#12131](https://github.com/renovatebot/renovate/discussions/12131),
[#9905](https://github.com/renovatebot/renovate/discussions/9905) ·
Dependabot: [options reference](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference),
[pull requests](https://docs.github.com/en/code-security/concepts/supply-chain-security/dependabot-pull-requests),
[auto-triage](https://docs.github.com/en/code-security/dependabot/dependabot-auto-triage-rules/about-dependabot-auto-triage-rules) ·
GitHub: [managing notifications](https://docs.github.com/en/subscriptions-and-notifications/how-tos/viewing-and-triaging-notifications/managing-notifications-from-your-inbox),
[REST notifications](https://docs.github.com/en/rest/activity/notifications),
[gh status](https://cli.github.com/manual/gh_status) ·
[actions/stale](https://raw.githubusercontent.com/actions/stale/main/README.md) ·
[fvsch: don't use stale bots](https://fvsch.com/stale-bots) ·
[PagerDuty snooze](https://support.pagerduty.com/main/docs/edit-incidents) ·
[Watchman trigger](https://facebook.github.io/watchman/docs/cmd/trigger.html) ·
[Syncthing events](https://docs.syncthing.net/dev/events.html) ·
[sticky-pull-request-comment](https://raw.githubusercontent.com/marocchino/sticky-pull-request-comment/main/README.md) ·
[changesets/action](https://raw.githubusercontent.com/changesets/action/main/README.md) ·
[agent-message-queue](https://github.com/avivsinai/agent-message-queue)
### Not verified — do not build on
`prHourlyLimit`'s default (two extractions of one page said `10`, no second
source); the `vulnerabilityAlerts` object's default; what Dependabot does when a
"dismiss until a patch is available" patch actually lands (absent from both pages
checked); **whether a GitHub notification marked Done returns to the inbox on new
thread activity** — two pages checked, neither states it, so do not assume
resurface-on-activity. `probot/no-response` is archived (2021-07-13) and
deprecated by its owner. `langchain-ai/agent-inbox` was search-level only, not
fetched.