Skip to content

Coordination readers refuse in a Claude Code cloud session - #2441

Merged
wshallwshall merged 11 commits into
mainfrom
claude/coord-cloud-reader-guard
Oct 20, 2026
Merged

wshallwshall merged 11 commits into
mainfrom
claude/coord-cloud-reader-guard

Conversation

@wshallwshall

@wshallwshall wshallwshall commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Was stacked on #2437; that has merged, the PR is retargeted to main, and main is merged in (4b3db9c). The diff against main is this change's six files only.

What

In a Claude Code cloud VM (CLAUDE_CODE_REMOTE=true), <git-common-dir>/mefor-coord belongs to the VM's own clone and holds nothing. So the coordination readers, run by hand, returned a confident empty answer that looked clean. Each one now refuses the way the writers do: exit 3 and one stderr line, with nothing on stdout.

Reader What it would have said in a VM Decision
overlap.ps1 "no overlap" / [] refuse. Exit 3 is already its did-not-finish code, so collision_gate.ps1, which reads the exit code, treats it as no all-clear.
claim-adjudicate.ps1 empty claim pool refuse
claim-reconcile.ps1 empty claim pool (with -Apply, it would hit claim.ps1's refusal only after a false-clean read) refuse the whole script
fleet.ps1 empty roster refuse
presence.ps1 only the VM's own sessions refuse, with its own tail: it reads the Claude config roots, not mefor-coord

I chose refusing over printing an explicit "no coordination state" answer because callers that check exit codes already treat a non-zero exit as not clean. An exit-0 answer would read as clean to anything that checks only the code.

Local exit-code contract is unchanged. Without the variable, behaviour is identical. The user-scope hooks that run overlap.ps1/presence.ps1 (collision_gate.ps1, session-context.ps1, announce-session.ps1) never load in a cloud session. mail.ps1's call to presence.ps1 comes after mail.ps1's own refusal. No project-scope hook calls any of these five readers. lock.ps1 stays unguarded. .claude/settings.json is untouched.

Tests (tests/test_coord_cloud_refusal.py)

  • Refusal test per reader: exit 3, the exact stderr line, empty stdout, and no mefor-coord created.
  • A control per reader, run without the variable: exit 0 plus a line of the reader's normal answer ([], nothing was scanned., fleet's fenceAvailable True, and presence's fixture session row). The controls point -ConfigRoot/USERPROFILE at fixtures, so they never read the host's registry, and they never call gh.
  • Census: the four coord readers move from NOT_GUARDED to GUARDED. presence.ps1 is outside the census, which matches on the directory name, and has its own refusal-line check.
  • Without the guards, the refusal and line tests fail (10 failed, 44 passed). With them, they pass.

Checks run

  • ruff check and ruff format --check on the test file: clean.
  • mypy, all four ci.yml legs: clean (361 / 342 / 1388 / 6 files).
  • pytest: test_coord_cloud_refusal.py plus test_cp1252_console_safety.py gave 142 passed after the review fixes. Before them, the readers' own suites (overlap x4, collision_gate, claim_adjudicate, claim_reconcile, fleet_fetch_clock, presence, session_context_presence, announce_hook, seat_, handoff_, control_char_check) gave 722 passed. test_crypto_inventory_doc.py plus test_asvs_scorecard.py gave 320 passed. The full suite was not run.
  • CI legs to read: the Windows tooling leg runs the controls (needs_windows); Linux runs only the refusal and line tests.

QA: code-review skill at xhigh (tag printed: xhigh effort -> 10 inline angles -> dedup (no verify) -> sweep -> <=15 findings) on origin/claude/coord-cloud-refusal-rest...eec7cffdb, reading docs/REVIEW-STANDARDS.md. It found 5 issues, all fixed in fae4caf:

  1. The fleet control asserted exit 0, but fleet exits 2 on a host with no ~/.claude*/sessions, so it would have gone red on a fresh Windows runner.
  2. R4/SDS-3.6: the overlap guard comment said "every consumer" reads the refusal as no all-clear, which is false for session-context.ps1.
  3. The test comment overstated how hermetic the controls were.
  4. The controls asserted only exit 0, not the normal answer.
  5. The -TimeBudgetSeconds doc did not mention exit 3's second meaning.

One repair round was used. /simplify ran first. The first commit message repeats the "consumers read the refusal as no all-clear" wording; this body corrects it.

Follow-up, not done here: collision_gate.ps1's comment "EXIT 3 IS THE BUDGET" still names only the walk budget. This only matters in a cloud session, where that hook never loads.

In a cloud VM --git-common-dir resolves to the clone's own .git, so
seat.ps1, mail.ps1, alloc.ps1 and claim.ps1 would "succeed" into a
mefor-coord directory no other session reads. Each now prints one
stderr line and exits 3 when CLAUDE_CODE_REMOTE=true. seat.ps1 exempts
its two hook paths, -Record and -Prompt, which must exit 0. The
SessionStart declare-prompt hook prints a one-line cloud-Builder notice
instead of the prompt, stamps nothing, and still exits 0.

tests/conftest.py drops an inherited CLAUDE_CODE_REMOTE so the existing
pwsh suites keep testing the local path inside a cloud VM. New
tests/test_coord_cloud_refusal.py pairs each refusal with a control
that takes the normal path.
It hardcoded .venv\Scripts\python.exe, so it failed on a Claude Code
cloud VM even with pwsh installed. It now uses .venv/bin/python off
Windows, judged by [Environment]::OSVersion so Windows PowerShell 5.1
(no $IsWindows) still reads as Windows, and defaults -Python to python3
there. The install line is unchanged, so both parity tests hold.

tests/test_worktree_ensure_venv.py drops its Windows-only gate and
builds the fixture's site-packages path per platform, so the ubuntu
legs, which carry pwsh, drive the bin/python branch.
Section 5 gains one rule: a Claude Code cloud session is a Builder in
its own session, on MEFORORG/MessageFoundry alone and never with a
second repository attached, because a multi-repo session loads no
project hooks. It opens its PR over REST, since the proxy refuses
GraphQL, and hands it to the Lander with the qa label and a PR comment,
not mail. Evidence: probe PR 2432 and wiki note
20261018T215116801Z-5lqiqo.
The ubuntu tooling leg errored in the fixture: the template's commit
detached background maintenance, whose .git/objects/maintenance.lock
vanished while copytree walked it. The template now sets
maintenance.auto=false and gc.auto=0, and the per-test copy skips
*.lock files.
handoff.ps1, lane.ps1, owner_log.py, tombstone-retired-backlog-allocator.ps1
and hygiene-report.ps1 now print the same one stderr line as PR 2434's four
scripts and exit 3 under CLAUDE_CODE_REMOTE=true, path overrides included.
Inline copies, not a shared helper: the set spans PowerShell and Python,
fixtures copy scripts file by file, and an override bypasses a resolver.

tests/test_coord_cloud_refusal.py adds a refusal and a control per script,
and a census that fails until every script naming mefor-coord is guarded
or classified (user-scope hook, clone-local, reader, mention).
…epeats

The /simplify pass: the script label is derived from its path, owner_log.py
runs from a cwd with no clone and one remote flag, the text check of the
Python copy goes (the behavioural tests already run without pwsh), and the
GUARDED comment states the reasons that hold.
CLAUDE.md no longer says every writer refuses: a clone-local lock or cache
is still written, and the census says which. handoff.ps1 documents exit 3,
the param comments in handoff.ps1 and lane.ps1 name the refusal they mean,
and the census tests bytes, so a non-UTF-8 script cannot crash it.
In a cloud VM <git-common-dir>/mefor-coord is the VM clone's own and
holds nothing, so overlap.ps1, claim-adjudicate.ps1, claim-reconcile.ps1
and fleet.ps1 returned a confident empty answer that looked clean, and
presence.ps1 listed only the VM's own config roots. Each now refuses with
exit 3 and one stderr line, like the writers. overlap.ps1's exit 3 is
already its did-not-finish code, so its consumers read the refusal as no
all-clear. presence.ps1 carries a tail of its own, because it reads config
roots, not mefor-coord. lock.ps1 stays unguarded.

The census moves the four coord readers to GUARDED; each new guard has a
refusal test and a control without the variable.

PR title: Coordination readers refuse in a Claude Code cloud session
…t-3 prose

The fleet.ps1 control asserted exit 0, but fleet exits 2 wherever no
~/.claude*/sessions exists, as on a fresh Windows runner. Each reader's
control now gets a fixture USERPROFILE and asserts a line of its normal
answer, not only exit 0. overlap.ps1's guard comment claimed every
consumer reads the refusal as no all-clear; session-context.ps1 reads
stdout only, so the comment now names both kinds, and -TimeBudgetSeconds
names exit 3's second meaning.
@wshallwshall wshallwshall added the qa Builder QA record posted; not a merge gate label Oct 20, 2026
Base automatically changed from claude/coord-cloud-refusal-rest to main October 20, 2026 16:03
@wshallwshall

Copy link
Copy Markdown
Collaborator Author

Lander: this PR now CONFLICTS with main. Engine PR 2437, the branch it was stacked on, merged at 16:03:42Z, and GitHub retargeted this PR to main.
Its required context 'a PR that implements BACKLOG #N must update BACKLOG.md' never reported on fae4caf. backlog-hygiene.yml runs only for pull_request events with base main, and this PR opened against 2437's branch.
Please merge main into the branch, or rebase it onto main, and push. The push re-runs every check, that context included. Then post a QA line naming the new head. The body's QA covers fae4caf with 0 open findings, so a clean re-sync needs only the head named.

…ader-guard

# Conflicts:
#	tests/test_coord_cloud_refusal.py
@wshallwshall

Copy link
Copy Markdown
Collaborator Author

Lander: ready for you under the qa label. Head 4b3db9c is fae4caf (the reviewed head, xhigh code-review QA line in the body) plus a clean merge of main after #2437 squash-merged; the only conflict was tests/test_coord_cloud_refusal.py, add/add, resolved to this branch's copy because main's copy was byte-identical to #2437's tip. The Windows tooling leg is the one that runs the reader controls.

@wshallwshall

Copy link
Copy Markdown
Collaborator Author

Rechecked after the main merge (4b3db9c): main's copies of #2437's files equal its final head 283fbd1 (empty diff), and git diff origin/main shows only this PR's six files. On the merged head: ruff clean, the four mypy legs clean (362 / 343 / 1394 / 6 files), tests/test_coord_cloud_refusal.py 54 passed. No force-push.

@wshallwshall

Copy link
Copy Markdown
Collaborator Author

Lander: enqueued at 4b3db9c, pinned.

  • Review: the body's QA (code-review at xhigh, tag printed) covers fae4caf, with all 5 findings fixed and 0 open.
  • The head merges main by a hand resolution in tests/test_coord_cloud_refusal.py, which the Lander reviewed by remerge-diff. The final file keeps all 14 of main's tests, adds this PR's 2 reader tests, and drops none of main's lines in substance.
  • The backlog-hygiene context now reports, because the push re-fired the pull_request event against main.
  • All 9 live required contexts are SUCCESS on this head.

@wshallwshall
wshallwshall added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit 5d48f78 Oct 20, 2026
50 checks passed
@wshallwshall
wshallwshall deleted the claude/coord-cloud-reader-guard branch October 20, 2026 19:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

qa Builder QA record posted; not a merge gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant