Skip to content
21 changes: 16 additions & 5 deletions .claude/rules/commits.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,22 @@ and adopts a change, never to a model identity.
Commits here are SSH-signed, so the signature term is already satisfied and the
email term alone carries the refusal. The only value it accepts is the one
`identity_deny` forbids: not a tuning problem, two contradictory policies.
- **Deleting it does not survive.** It is registered in the launcher's own
settings, re-provisioned mid-session, and Claude Code _merges_ hooks across
settings files — so a lower-precedence file can add a hook and never remove
one. Turning it off is an owner action on the environment configuration that
generates those settings, outside this repository.
- **Deleting the FILE does not survive, and that is not the same as being
unreachable.** It is registered in the launcher's own settings, re-provisioned
mid-session, and Claude Code _merges_ hooks across settings files — so a
lower-precedence file can add a hook and never remove one. All of that is
measured and all of it stands. The conclusion drawn from it —
_"turning it off is an owner action on the environment configuration that
generates those settings, outside this repository"_ — **was false, and is
retracted** (CLOUD-1356; CLOUD-1079 owns the fix). Merging means the repo
cannot unregister the hook _through a settings file_, which is a narrower
claim: `batten wiring reclaim` removes the **registration** without touching
the file that carries it, and a `[[hook.handler]] on = "session-start"` row
runs it every session, exactly as `session:identity` and `session:signing`
already beat this launcher's own `--global` writes. A repair that runs once
loses to a rewrite that runs every session; a repair that also runs every
session does not. Reaching for _"outside this repository"_ over a `$HOME` path
is the tell: outside the repository **root** is not outside its **reach**.
- **The signature half is a different issue.** GitHub answers `verified: false,
reason: unknown_key` — the key is unpublished, not absent. That is CLOUD-591's,
and resetting the author signs nothing, so obeying trades a tracked gap for a
Expand Down
38 changes: 36 additions & 2 deletions .claude/rules/toolchain.md
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,28 @@ call` with no `CLOUD-*` key **in that same paragraph** stops the lap. Two open
entered the changed-file set. The PR body reaches the predicate as a RECORD
rather than on stdin, because `check` is declared `read` and has no stdin
channel: the boundary captures what `gh pr view --jq .body` returned.
- **GREEN AND PUSHED IS NOT A STOPPING POINT, AND NOTHING RELIABLE TELLS YOU SO**
(CLOUD-1372). Done is `main`, by fast-forward, CI-confirmed — the Definition of
Ready & Done owns it. A draft PR with a green local suite is In Progress, and
`land` is the step that has not run.

**The trap is a harness hook's checklist standing in for the repository's.**
This container's launcher hook says _"There are uncommitted changes … commit and
push these changes"_, and satisfying it feels like finishing, because it is the
loudest completion-shaped signal in the session and it arrives at exactly the
moment a turn is ending. It is a hook about **reclaim survival**, which is a
different question from **done**. Measured 2026-09-02: an agent took a branch
green, pushed, reported "committed and pushed", and stopped — repeatedly, with
the owner as the only detector.

**Do not rely on the engine to catch this.** `completion.unlanded` is the gate
for it and it is **not firing on this host** — its marker (`StopReason::EndTurn`
with no tool call after it) is absent from a stream whose turns end on tool
calls, and `Outcome::NotSignaled` writes nothing, so the silence is
byte-identical to a landed branch. CLOUD-1372 carries the measurement and the
fix. Until it lands, **the check is yours**: `git status -sb` showing `ahead`,
or a PR still in draft, means the work is not done.

- `unlanded-check` is the end-of-turn half nobody had (CLOUD-97), and it decides
NOTHING: `completion.unlanded` — a completion marker in the session transcript
with no patch-id-equivalent commit on the landing target — is the engine's
Expand Down Expand Up @@ -538,8 +560,19 @@ call` with no `CLOUD-*` key **in that same paragraph** stops the lap. Two open
spawning kind with `RuleScope::Tree` alone), so they are `stop_nudges` in
`lib.rs`, keeping the order the shell ranked them in — by MEASURED precision,
`stop-posture` at 3/3 leading `finding-sink` at 1/1, with the three unmeasured
below. `finding-sink-check.sh` and `unlanded-check.sh` are spawned unchanged,
with the same stdin the bash gave them, which is what bounds the cascade. The
below. `finding-sink-check.sh` is spawned unchanged, with the same stdin the
bash gave it, which is what bounds the cascade.

**Two corrections, both measured 2026-09-02 (CLOUD-1372).** This clause named
`unlanded-check.sh` as a second spawned sibling; that program **does not
exist** — it retired into the engine, and `unlanded_pointer` reads the store
directly. And the ranking above is no longer by precision alone: **the
completion reading is now FIRST**, ahead of `stop-posture`'s prose nit, because
precision is the right axis only between rules about the same kind of thing.
The others say the turn was untidy; that one says the work exists nowhere but
this container. Its minting also moved **out** of the ladder — `record_state`
now runs before any rule can return, since a branch carrying a `filed-here`
pointer used to return first and the verdict was never minted at all. The
recursion bound is the payload's `stop_hook_active`, never a state file; the
channel is `additionalContext`, never exit 2, because CLOUD-97 and CLOUD-219
each ruled a deny out independently and `Event::carries_a_verdict` is now the
Expand All @@ -549,6 +582,7 @@ call` with no `CLOUD-*` key **in that same paragraph** stops the lap. Two open
so the five rules decided only WHICH nudge fired and never WHETHER one did, and
a constant has zero mutual information with the thing it is meant to detect. An
empty answer is an answer, and silence is what keeps the channel credible.

- **`claim-guard` is retired** (CLOUD-444); the pull-time half of the pair the
key rule finishes (CLOUD-272) is now the `claim-needs-receipt` row in
`batten.toml` — a `receipt` rule with `trigger = "write"` and `key = "branch"`.
Expand Down
32 changes: 26 additions & 6 deletions .serena/memories/connector-allowlist-recovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,19 +219,39 @@ with all its tools; this session did not bind them._ `ListMcpResourcesTool`
(resources, not tools) and `claude mcp list` (CLI config, not connectors) prove
NOTHING here — the only evidence is a call returning "No such tool available".

**Sensor gap, unfiled because the tracker is the unreachable thing.** Both
`mcp-attach-check` and `mcp-allow-check` pass green through this. Neither
compares the injected config's `tools[].name` against the tools the session can
actually call, which is the one comparison that catches it — and
`connector-allow-resolve` already reads that file, so only the predicate is
missing. Recorded in PR #575's body; wants a row of its own.
**Sensor gap — FILED 2026-09-02 as CLOUD-1359.** Both `mcp-attach-check` and
`mcp-allow-check` pass green through this. Neither compares the injected
config's `tools[].name` against the tools the session can actually call, which
is the one comparison that catches it — and `connector-allow-resolve` already
reads that file, so only the predicate is missing.

This paragraph read _"unfiled because the tracker is the unreachable thing"_ for
its whole life, and that is worth keeping rather than deleting: **a defect whose
own occurrence blocks its report is under-represented in the tracker by
construction**, so the count of episodes is unknown rather than low. The
deferral was real while it held and stopped being real the moment a session with
a bound connector read this file. It is filed from one. Prior record: PR #575's
body.

## What is NOT known

- **What causes the flip.** No hypothesis is supported by evidence yet.
- **Whether a `SessionStart` hook's settings write affects the session that is
starting**, or only the next one. Permissions are read at startup and the hook
runs at startup; the ordering is unmeasured. Measure it, do not assume it.

**Still unanswered 2026-09-02 — but the question's premise is now known to be
too simple, which changes how to measure it.** It assumes startup is one
ordered moment. Measured this session: `~/.claude/launcher-settings.json` and
both its scripts carry mtime **16:59 — MID-session**, not session start
(CLOUD-1079). So the launcher rewrites the settings surface while a session is
running, and "does my startup write take effect" and "does my write survive"
are two different questions with two different answers. A one-shot write can
lose to a later rewrite even if the ordering at startup is favourable, which
is why the landed answer for the hooks themselves is a repair that runs **every
session** rather than a write that runs once. Whoever measures this must
distinguish the two; a single before/after reading cannot.

- **Whether the UUID survives an OAuth re-grant.** Stable across two containers
is not stable across a re-grant.

Expand Down
16 changes: 13 additions & 3 deletions .serena/memories/toolchain-and-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,9 +404,19 @@ would set, so it chooses no evidence:

ln -sfn ~/.claude/projects/<slug>/<session>.jsonl .claude/.transcript.jsonl

Unfiled: the tracker was unreachable in the session that measured it
(`mem:connector-allowlist-recovery`). It wants a row, and the fix is plausibly a
`SessionStart` write beside the other things `session-start.sh` already asserts.
**FILED 2026-09-02 as CLOUD-1361.** This read _"Unfiled: the tracker was
unreachable in the session that measured it"_ — true while it held, and the same
shape as `mem:connector-allowlist-recovery`'s sensor gap (CLOUD-1359): a session
that cannot reach the tracker generates findings it cannot file, so this class is
under-represented by construction rather than rare. Both filed from a session
whose connector is bound.

Its remedy line needs one correction: `session-start.sh` is **retired**
(CLOUD-312 row 10, #804), so the session-start write lands as a
`[[hook.handler]] on = "session-start"` row rather than as another step inside
that script. The row also carries the half this note left implicit — the engine
and `[transcript]`'s comment disagree about what an absent transcript means, and
settling that comes before fixing the symlink.

## The shell tasks' exit convention is the inverse of batten's

Expand Down
53 changes: 53 additions & 0 deletions batten.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6952,6 +6952,59 @@ timeout_ms = 600000
owner = "CLOUD-312"
expires = "2027-02-28"

[[hook.handler]]
id = "session-wiring"
on = "session-start"
run = ["mise", "run", "session:wiring"]
# THE THIRD REPAIR OF LAUNCHER-PROVISIONED STATE, and it is here for the reason
# the two rows above it are (CLOUD-1079). `session:identity` beats a `--global`
# identity write; `session:signing` beats a `--global` signing write; this beats
# a hook REGISTRATION the same launcher merges into `~/.claude/`. One mechanism,
# three subjects, and this was the subject nobody had wired.
#
# WHY IT EXISTS AT ALL, because the row it closes spent weeks reading as somebody
# else's job. `~/.claude/launcher-settings.json` registers
# `session-start-git-identity.sh` and `stop-hook-git-check.sh`, the second of
# which exits 2 on every correctly-attributed commit here and prescribes the
# identity `[attribution] identity_deny` forbids. From "Claude Code MERGES hooks
# across settings files, so a lower-precedence file can add one and never remove
# one" — true, and measured — the inference drawn was that turning them off is an
# owner action on the provisioning configuration, outside this repository. That
# is false, it is CLOUD-1356's root sentence, and the distinction it misses is
# the whole of this row: the repo cannot unregister the hook THROUGH A SETTINGS
# FILE, which is not the same as cannot repair the wiring. `batten wiring
# reclaim` removes the REGISTRATION and never the file that carries it — which is
# also why a blind `rm` is the wrong instrument, since
# `session-start-git-identity.sh` sets `core.hooksPath` for the whole container
# and that line may be load-bearing.
#
# A REPAIR THAT RUNS ONCE LOSES TO A REWRITE THAT RUNS EVERY SESSION. A previous
# session ran `wiring reclaim -y` by hand and the launcher re-provisioned over
# it, from which the same row concluded permanent impossibility. The answer was
# never a better one-off; it is this row, which runs at the same cadence as the
# thing it is undoing.
#
# THE RESIDUE IS STATED RATHER THAN ABSORBED: both scripts were measured with
# mtime 16:59 — rewritten MID-session, not only at session start — so this closes
# the session-start window and does not hold against a mid-session rewrite. What
# would close that is the container's Setup script, which already calls
# `mise run deps-install` and runs upstream of both the rewrite and the harness
# snapshot. CLOUD-1079 owns that half; this row is the half that is expressible
# here.
#
# AFTER `session-batten`, because `batten wiring reclaim` is the binary answering
# about its own registrations — the same ordering `session-attribution-identity`
# takes above, and for the same reason. BEFORE the preflight and the census, so
# both observe the repaired wiring rather than the launcher's: a census that
# reported `merged_siblings: 2` because it ran first would be reporting a state
# this sequence had already been asked to fix.
#
# The bound is the task's, not a guess: `wiring reclaim` reads settings files and
# rewrites registrations, opening no network and building nothing.
timeout_ms = 30000
owner = "CLOUD-1079"
expires = "2027-02-28"

[[hook.handler]]
id = "session-container-preflight"
on = "session-start"
Expand Down
Loading
Loading