Tips and tricks
Check that a tool measured the question you asked before trusting its result. These lessons came from lost work, inactive controls, and misleading measurements during KORUS development.
Each entry records the incident or measurement behind it. The examples concern concurrent sessions; they may be less useful before you have run them.
Find the entry that matches your problem. Most commands require pwsh 7.3+, were tested on Windows,
and assume Claude Code for Desktop.
KORUS was built and measured with Claude Code for Desktop. A command-line-only or editor-extension setup is outside that tested scope; see Limits.
- PowerShell 7 + Windows-first. Most of this repo is
pwsh. The Python gates – the git-hook checkers underscripts/hooks/and the leak gate atscripts/security/scan_forbidden.py– are stdlib-only and portable; nearly everything else assumespwsh 7.3+. - Announcing needs Claude Desktop.
scripts/hooks/announce-session.ps1resolves peers and asks the model to send, throughccd_session_mgmt– an MCP server absent on a plain CLI install. Without it the hook fires, finds peers, then names tools the model lacks. - Claude Code’s session record schema is a vendor contract. The whole liveness fence rests on
<config-root>/sessions/<pid>.jsonand itspid/startedAt/sessionId/cwd/entrypointfields. It can change under you without notice. list_sessionscannot see every session kind. It enumerates only sessions the desktop app spawned. An editor-extension session is never registered there, so sharing the config root does not help.scripts/coord/presence.ps1reads the file registry, which covers every launch surface.
1. Read this first: Every failure here is green
Several failures produce the same visible result as a working control:
- A wired hook resolving nothing exits 0, like a healthy one with nothing to report.
- A missing gate script exits non-zero-but-not-2, and the tool call runs anyway.
- An empty peer list means “nobody is here” and “I could not look”.
- A scanner that loaded zero rules exits 0, like one that found none.
In a fresh clone, audit the controls before installing or trusting them.
pwsh -NoProfile -File bin/ccx-doctor.ps1 -Repo <the-repo-you-governed>
The audit prints what it scanned and performs four checks:
- it hashes every installed copy against this checkout’s source;
- it reads every config root’s live matchers;
- it fires each control and requires a deny, pairing each attack with a negative control the guard must allow;
- it names its own blind spots, on every run, pass or fail.
Read repo examined before the verdict. Without -Repo, the command audits the repository
containing your current directory.
Running in the tooling checkout can produce a plausible report about the wrong repository.
The report uses these result tags:
| Tag | Meaning | Exit |
|---|---|---|
OK |
Proven: installed, wired, and it refused what it must refuse | 0 |
RED |
Proven broken: stale, unloadable, allowed a deny case, or denied an allow case | 1 |
OFF |
Implemented, but nothing invokes it – zero enforcement | 1 (unless opt-in by design) |
?? |
Could not be determined | 2 |
-SkipAttacks marks every attack ?? and returns exit 2. Skipped attacks cannot establish a pass.
2. Before you open the second session
Keep one logical task per session. A second task inherits the first task’s dead ends.
After roughly two failed attempts at the same problem, restart with a better prompt instead of carrying that failed approach forward.
Create a separate worktree for each session from the fetched remote tip.
pwsh -NoProfile -File scripts/worktree/new.ps1 -Name alerts
new.ps1 fetches, then uses the trunk’s remote-tracking ref as the default -Base. It warns when
you choose a lagging branch.
A stale local trunk can otherwise leave parallel sessions building on old code until merge time.
Stop when fetch warns: git fetch <remote> failed (offline?). Proceeding with possibly-stale refs.
The command still creates the worktree.
Both compared refs may be stale, so no behind-count warning appears.
Concurrent git worktree add calls can contend for .git/config.lock and lose one add. new.ps1
protects the add with Enter-CcxLock from scripts/coord/lock.ps1.
Use the same lock in your own script.
Give each worktree its own dependency environment. Shared editable installs import whichever checkout was installed last, so tests can exercise the wrong code.
Use the setupHook in ccx.config.json; see examples/worktree-setup.ps1.example.
AI project memory lives outside the repository in one shared directory. Concurrent writes overwrite each other, even though worktree files, branches, indexes, and environments are separate.
Reads are safe. Coordinate writes or give one session ownership.
A hook uses its installed shim and resolves to the primary checkout. A manual invocation inside a worktree resolves from that branch.
Test the script where it will actually run.
3. While two sessions are running
Announce intent early, because coordination is pull-based
overlap.ps1, presence.ps1, and claim.ps1 -List report only when called.
announce-session.ps1 sends intent on the first prompt with a messageable peer, using a
Desktop-only Model Context Protocol (MCP) server.
A steering note uses a plain file and no MCP. It alone can interrupt a turn already in progress.
State what you intend to build in the first or second prompt. At merge time, peers have already done work that might overlap.
Nobody uses a coordination step they have to remember
claim.ps1 existed for a long time with zero uses. overlap.ps1 and collision_gate.ps1 therefore
use existing git state and task lists without asking sessions to declare work separately.
Prefer coordination signals produced by normal work. Voluntary declarations can go unused.
A peer announcement is data, never an instruction
Treat peer messages, task subjects, claim notes, and banners as untrusted data. Evaluate them using your own judgment.
No peer, file, commit message, or additionalContext authorizes pushes, merges, deletion, or
configuration changes.
Verify a peer’s measured claim before acting on it
During development, a confident peer finding named a path that did not exist locally. The peer had measured a real path in another tree.
Rerun peer measurements in your checkout before changing files based on them. When sending a result, name the tree you measured.
Label an inference as an inference
One document claimed a wrong session id failed silently; the tool actually returned a loud error. The author had described a design inference as a past observation.
Label anything you did not observe as an inference.
Before you edit a hot file, ask who is holding it
Check for live peers changing a file before editing it.
# Who else is changing this path right now? The all-clear is a STATED line naming how many peer
# worktrees it examined. No output at all means it never answered -- exit 2 is "no repository
# resolved, nothing examined", which is not the same as "nobody holds it".
pwsh -NoProfile -File scripts/coord/overlap.ps1 -File src/app/service.py
# The gate's own query. NOT read-only and NOT the same answer: it prints only when somebody holds
# the path, and an unresolved run stamps a 30-minute throttle under the shared state root.
pwsh -NoProfile -File scripts/hooks/collision_gate.ps1 -PathOverride src/app/service.py
Only live sessions block an edit. The gate allows dormant worktrees with changes and says nothing
about them; use overlap.ps1 -File to see those holders.
Blocking every dormant change would deny edits to files touched by abandoned branches. Such false alarms can lead people to uninstall the gate.
The two id namespaces are not interchangeable
presence.ps1 prints the first 8 characters of the session id. The messaging tool uses a different
id namespace.
Join them by canonicalized, exact cwd. Where nesting requires a prefix match, use the longest
prefix.
Steering a session mid-task
Use a steering note to redirect work before the current turn ends.
Wire steer-inject.ps1 before starting the session. It is opt-in per worktree, and no installer
adds it.
New wiring reaches only later sessions; see Steering.
Run from a second terminal in the target worktree. ccx-steer.ps1 takes no session argument and
writes to the terminal’s current repository:
pwsh -NoProfile -File bin/ccx-steer.ps1 "stop after the current file; the API shape changed"
If wiring predates the session, the note arrives at its next tool-call boundary. The sender gets no delivery report, so an unwired target looks the same from that terminal.
scripts/hooks/steer-inject.ps1 uses a PreToolUse matcher of *. That starts pwsh before every
tool call.
Measured cost was ~366 ms per call, including ~267 ms of bare startup. Enable it only in worktrees that need it.
Kill switches must be files
Use a file to disable a hook in a running session: <git-common-dir>/ccx-coord/announce/OFF.
CCX_ANNOUNCE_DISABLE affects sessions not yet started.
Deleting ~/.claude/hooks/ccx-gate.repos.txt disables the worktree gate.
Commit at logical stops; keep pushes gated
Commit at tested, logical stops so rescue has usable history. Ask before pushes, pull requests, and merges; auto-merge can put a pull request on trunk.
scripts/hooks/push_guard.py blocks direct pushes to protectedRefs locally. It installs per clone
and remains advisory because --no-verify skips it.
4. When you write a guardrail
A guardrail’s failure posture says what happens when the check breaks. Fail-open lets work continue; fail-closed stops it.
State the posture and reason at the top of the file. collision_gate.ps1 fails open to avoid
blocking work; worktree_gate.ps1 fails open with a notice.
claim_check.py fails closed in two places because a false clean result is unrecoverable.
Error paths in collision_gate.ps1 once returned exit 0 without output, just like an all-clear.
Failed resolution now reports through additionalContext; it still allows the work.
In presence.ps1, @() | ConvertTo-Json -AsArray sends no objects, so the converter never runs.
The branch printed nothing where another printed [].
Emit an empty array explicitly. Put failure-to-inspect notices on stderr.
Always include the hookSpecificOutput wrapper. The client silently ignores a bare
permissionDecision, leaving every tool call permitted.
Gate the destination path. Over 30 days, 29% of Edit/Write calls from primary-seated sessions targeted sibling worktrees through absolute paths.
A working-directory rule would wrongly deny those valid writes.
The same 29% also escapes an occupancy check based only on recorded working directories. Such checks cannot see sessions writing into another tree.
prune-merged.ps1 therefore requires two independent occupancy signals. Recent git-metadata
modification time supplies the stronger signal.
False denials encourage bypasses. block-blanket-git-stage.ps1 deliberately allows --amend but
denies a single-dash flag cluster containing a.
Tool-name lists miss every unlisted route in both matcher and rule body. A dispatch ban covering three tool names left four other routes probe-verified ALLOW.
Document the list and its limits.
A command-string gate cannot see commands inside an agent-written script. PreToolUse sees the
invocation arguments.
The hooks from scripts/coord/install-git-hooks.ps1 inspect the tree at commit time. Shared
.git/hooks covers every worktree and write route.
Protect hook configuration separately. Linked worktrees share config as well as objects, so one change to hook resolution can disable commit-time checks everywhere.
A verb gate without config coverage leaves that route open.
Use Resolve-CcxGitTarget from scripts/hooks/_gittarget.ps1 and Split-CcxCommand from
scripts/hooks/_command.ps1.
Separate parsers for -C, cd, and working directory can each assume another rule covers the same
case.
Keep one implementation of each safety check. Both read-only presence.ps1 and destructive
prune-merged.ps1 use Get-WorktreeOccupancy from scripts/coord/occupancy.ps1.
Use one allowlist path for every consumer. With two paths, uninstalling the gate once left its
backup check able to run git checkout on the primary.
Give installers the same session refusal, discovery, -Status, and -Uninstall behavior. Test that
they reach the same directories.
install-gate.ps1 -NoDispatchGate disables an implemented rule. The installer warns, and -Status
reports its tools as UNWIRED so the disabled check remains visible.
Allocate claims, numbers, and locks by exclusive file creation. A failed create establishes that another process owns it.
A read-modify-write operation on one shared list silently lost 4 of 8 concurrent writes here.
Do not expire locks on timers. Expiry can admit a second process while the first still holds the critical section.
lock.ps1 retries without stealing. A timeout fails loudly and names the holder.
Liveness may only VETO, never PERMIT. With no heartbeat, the system cannot prove a session is gone.
DEAD, STALE, or absent means no veto, not permission.
Log every denial. One gate once printed decisions and exited 0 without retaining a log, counter, or audit file. Nobody could answer:
- how many drift events did this prevent?
- is the false-positive rate one a day, or one in a thousand?
- did that fix change anything?
Without those records, severity rankings were opinions. worktree_gate.ps1 now records timestamp,
rule, tool, working directory, and rule-composed detail.
It excludes raw commands and file contents so secrets in arguments cannot enter the plaintext log.
Test denial text as well as decisions. One denial listed the forbidden primary first among suggested worktrees, displacing a valid suggestion beyond the display cap.
A string-to-object comparison made its filter always pass, and no test checked the message.
overlap.ps1 and collision_gate.ps1 share a named row contract recorded in both files. Adding
fields is allowed.
Renaming a field or changing its meaning requires updating both notes in one commit. Otherwise, the meaning can change without an error.
Use one numbering scheme for a rule set. If prose and code assign different meanings to “rule 3”, a later edit can update one and silently leave the other.
5. When you measure whether it works
Each measurement trap below occurred while building this repository, despite a check reporting success.
Before trusting a result:
Name the question. Name what the tool actually returns. Check that they are the same sentence.
$? after a pipe reads the last command
A leak scan refused the tree, but its wrapper printed exit=0. $? held tail’s status from the
last pipeline command.
# WRONG - $? is tail's
scan ./tree | tail -5
echo "exit=$?"
# Right - read the producer's status, or do not pipe
scan ./tree > scan.out; rc=$?
tail -5 scan.out
echo "exit=$rc"
# Or, in bash:
scan ./tree | tail -5
echo "exit=${PIPESTATUS[0]}"
PowerShell’s $? and $LASTEXITCODE report different results; only native commands set
$LASTEXITCODE.
Several scripts set $PSNativeCommandUseErrorActionPreference = $false to keep non-zero git
results inspectable instead of throwing.
printf mangles Windows paths
A fixture was meant to contain C:\Users\<name>\.... printf treated \U as a Unicode escape and
changed it.
The scanner could not find the intended violation because the file never contained it.
# WRONG - \U, \n, \t all get interpreted
printf 'C:\Users\someone\project\file.txt' > fixture.txt
# Right - quoted heredoc, then print the file back and LOOK at it
cat <<'EOF' > fixture.txt
C:\Users\someone\project\file.txt
EOF
cat fixture.txt
Always read the fixture back before trusting a test that depends on its exact bytes.
A scanner handed a file argument can be a silent no-op
A scanner once accepted a file argument, dropped it, and exited 0. A mixed file-and-directory run also passed because its refusal required every input to be dropped.
The scanner now reads file arguments and returns 2 when an argument yields zero files. Check its input counts even when another input scanned successfully.
A green gate is evidence only if you proved it can see that class
Plant a known violation and require failure before trusting a pass. A clean result alone cannot prove the detector ran.
Check that detector and rule counts are non-zero. ccx-doctor.ps1 also pairs attacks with ordinary
inputs that must pass.
Those controls distinguish working enforcement from a script that refuses everything.
A broken probe can also misreport a working gate. Establishing what a hook actually does records that case and its four resulting rules.
On 2026-08-11, a grep for three tokens returned zero on a ref without a classifier. The same search also returned zero on a ref that had to match.
The conclusion was true, but the check supplied no evidence. Always run the search against a known positive before trusting a zero.
A control can fire and still miss the subject
A positive control proves the instrument works. It does not prove the instrument was pointed at what you asked about.
On 2026-09-17, three analysis passes reported a file absent from a project because they searched a primary checkout sitting hundreds of commits behind its trunk.
Their control was a different file, old enough to exist in both trees. It fired, so the search looked healthy while answering about the wrong tree.
Reproduced in this repository the same day, from a primary checkout 48 commits behind origin/main:
git ls-files docs/WORKTREES.md # 1, their control: in both trees
git ls-files .github/workflows/required-workflow-state.yml # 0, the subject: a false absence
git show 'origin/main:.github/workflows/required-workflow-state.yml' | Measure-Object -Line # 158
Control on something that exists only at the ref you mean. A control both trees share cannot tell a stale subject from a true zero.
Worktrees carries the reading rule that avoids this.
Arming a control in place can contaminate what you measure next
Assert on the result’s content, not on its exit code. A merge that keeps your change and a merge that silently drops it both exit 0.
Reported by a peer session on 2026-09-17, against its own work. It checked a patch against a scratch
copy with git apply --check. That flag writes nothing, so the copy never received the patch.
To arm its detector it then mutated a line in that same copy, and the patch was refused. The control fired, correctly. But the merge probe it built from that directory carried the mutation and none of the change.
git merge-tree returned exit 0, and exit 0 was true. It answered whether the merge had a textual
conflict, while the question asked was whether the merged file held the fix.
Two verbs to distrust together: a check-only flag that leaves no artifact, and an in-place edit made to arm a control. Plant the control in a throwaway copy, never in the one the real measurement reads.
A file’s arrival is a change surface, and a search for consumers misses it
Search for what reads your file’s name as well as for what reads its content. A check that globs a directory sees a new file the moment it exists.
Reported by a peer session on 2026-09-17, on a pull request in another repository. Three CI legs went red. Two sessions independently searched for consumers of the edited script, and both found the same four test files. Both readings were correct.
The failing check never reads that script. test_tooling_partition.py globs tests/test_*.py and
fails on any file missing from tests/tooling_manifest.txt. It saw the branch’s new test file as a
name in that glob.
Those searches drew their population from what the change touched. The failing consumer cares that the file exists, which nothing in the edit’s content can answer.
A control can fire and still miss the subject is the wrong subject, an instrument pointed at the wrong tree. This one is the wrong population: the aim was right, and the answer sat outside the set searched.
The same split sits in this repository. tests/test_prose_rules_hold.py excludes the frozen
.old.md archives, because nobody may edit them. tests/test_internal_links_resolve.py reads
git ls-files *.md, which returns all 50 of them.
Measured at 339b9ff:
(git ls-files '*.old.md').Count # 50, the archives no editing sweep opens
(git ls-files '*.md').Count # 163, the link scan's corpus
# 38 anchored links from those archives into current pages
(git grep -hoE '\]\([A-Za-z0-9_./-]+\.md#[a-z0-9-]+\)' -- '*.old.md' |
Select-String -Pattern '\.old\.md' -NotMatch).Count
Renaming one of those 38 headings fails a check over a file the sweep had ruled out of scope.
The repair is not to fix the link. Each archive is sha256-pinned, so editing one reds
test_page_revisions.py instead. Both routes are closed, and the rename is what has to give.
That gap is now guarded. Ask before you rename, and the answer names the archives and the options:
python scripts/quality/frozen_citations.py docs/LIMITS.md "What actually switches each control on"
tests/test_a_heading_a_frozen_archive_cites_cannot_be_renamed.py pins the set and reddens with the
same message when one of those headings goes.
Run the check’s own classifier against your ref and against the base ref, which is the armed control.
Here that returned ['test_coord_occupancy_unplaceable.py'] at the branch and [] at the base, with
pytest never run.
The string you ran and the string you published can be different commands
Paste the exact published text into the target shell and run it there. A tool layer that re-quotes your input hands the shell a different command from the one your page will show a reader.
Two sessions hit this on 2026-09-17, from opposite ends of one line. The subject was the ASCII gate
command, which the root CLAUDE.md publishes under Pure ASCII, everywhere and owns.
A peer verified it from a Bash tool with the dollar sign backslash-escaped. What bash executed was behaviourally single-quoted and graded every case correctly. What the peer published was double-quoted.
A second session then tested that published string, escaped the dollar sign the same way, read the same correct grades, and nearly called it sound. It had re-run the first session’s correction rather than its command.
Measured at 538ea4b on pwsh 7.6.6, run from bash over three trees the gate grades 0, 1 and 2:
# <tree> is docs, then .claude/skills, then no-such-dir-xyzzy
# Escaped, which is what the tool layer executed -> 0, 1, 2
pwsh -NoProfile -Command "& ./scripts/quality/check-ascii.ps1 -Path <tree>; exit \$LASTEXITCODE"
# Unescaped, which is what the page published -> 0, 0, 0
pwsh -NoProfile -Command "& ./scripts/quality/check-ascii.ps1 -Path <tree>; exit $LASTEXITCODE"
Bash expands the unset variable to nothing, so the inner exit carries no argument and returns 0. A
violation and a scan that read no files both report clean.
One case is not enough. Over docs both forms return 0, and the broken form returns 0 for a
violation and for a scan that read no files as well. Run every case the command distinguishes; a
control that fires proves the instrument works, not that you ran the published text.
The ; exit $LASTEXITCODE clause is load-bearing, and trimming it as noise is the likeliest way to
arrive here. Over the same three trees:
| The published line | docs |
.claude/skills |
no-such-dir-xyzzy |
|---|---|---|---|
| clause expanded away | 0 | 0 | 0 |
| clause removed | 0 | 1 | 1 |
| clause intact | 0 | 1 | 2 |
The middle row is the one to watch. It passes two cases and fails only the case a reader is least likely to run. That failure grades a violation and a scan that read nothing alike, which is what the gate reserves 2 to prevent.
A relay lost a clause the third time, and neither session was wrong. A brief carrying the peer’s line
to the second session dropped > $null 2>&1 from inside the quoted string, so the two measured
strings one clause apart.
Measured the same way from pwsh, after a success and after cmd /c exit 7: with the clause, 1 and 1.
Without it, the 0 and 7 that CLAUDE.md publishes.
The caller expands $null as well, so the inner shell gets a redirection operator with no target. It
refuses to parse and never runs the gate, so that 1 is not a verdict about any tree.
A control can fire and still miss the subject is an instrument pointed at the wrong tree. Here the instrument, the subject and the population were all right. The artifact under test was not: one string was measured and another was shipped.
A gate cannot see a policy judgment
A scanner cannot judge whether ordinary prose belongs in a repository. Assign that decision to a human reading the diff; do not assume a pattern can make it.
Build artifacts contaminate a scan
A username scan failed on clean source because __pycache__/*.pyc embedded source paths.
.gitignore covers __pycache__/ and *.py[cod], but a disk scan still sees them.
Use git ls-files when checking what will be published. For a disk inventory, include build
artifacts and say that the scan covers them.
An over-matching glob turns a passing check into a fake failure
An overly broad glob sent Markdown to a PowerShell parser. Its syntax error was valid for PowerShell, but the file was valid Markdown.
Twenty minutes went into investigating the wrong file.
Print the input file list before checking it. That makes a wrong scan scope visible.
A missing tool returns nothing, and nothing reads as zero
Two “0 hits” reports came from a missing bc command. Empty output was compared as zero.
Check that required tools exist. Treat an empty result as an error, not zero.
Test the installed copy, not the repository copy
A gate had 85 green tests against its repository copy. Enforcement used a stale installed copy, so the tests proved nothing about the live gate.
Feed crafted JSON to the installed file the client invokes. install-git-hooks.ps1 -Status compares
hashes with the receipt and current sources.
Installing from a stale checkout can downgrade the gate for every worktree.
Merging a hook does not install one
Merging changes neither ~/.claude/settings.json nor ~/.claude/hooks/. Gitignored project
settings also fail to reach new worktrees.
Measured here, over half the worktrees lacked project settings. One live session had zero coordination context.
Check an installation receipt and a freshly resolved target. A settings entry alone only claims the hook exists.
install-coordination.ps1 -Status reads its receipt and resolves every shim target again.
An installer with no row for a hook is a hook that runs nowhere
On 2026-08-11, a documented watcher passed client checks but had no row in
install-coordination.ps1. It had never been enabled anywhere.
Source, tests, and docs all showed the hook existed. None recorded an installation.
Put at least one signal outside the component being audited
A hook printed status and exited 0 for weeks while resolving nothing. Its receipts lived inside a script the shim could not find.
Every check depended on reaching that missing script.
Put a signal where it can still report if the control fails to load.
Prove each fix catches its own regression
Mutate each fix and require its test to fail. A test that passes both versions does not detect the repaired defect.
Re-measure a premise before you defend it
A deny rule rested on three claims. Remeasuring confirmed the first and found the second already covered elsewhere.
The third, which justified a hard denial, was one undocumented observation that did not reproduce.
Mark figures you cite without remeasuring them. Present-tense wording can make an old measurement look fresh.
A rebase can change what a check is measuring
A branch opened against 58 tracked scripts and a page naming 47 of them. Two pull requests landed under it before it merged.
One added scripts/quality/frozen_citations.py. The subject the branch’s own check reads grew by
one while it sat open, so the tree it was written against was not the tree it would run on.
Re-derived after that rebase: 59 tracked, 59 rowed. The check calls git ls-files per run, so it
demanded a row for the arrival rather than passing at 58.
Two numbers agreeing does not prove the instrument saw the new member. A cached subject list returns 59 and 59 as well. Deleting that file’s row from a copy of the page made the check name that exact file, which is what separated the two cases.
A suite count moved the same way and was restated rather than replaced: 634 at 9379109, 637 after
one rebase, 652 after the next. Each was a true reading of a ref the branch no longer pushed.
Re-derive after a rebase. Arm the check on the file that arrived, not on the ones you wrote.
Measured at a22cfba.
Reconcile the parts against the total the tool already printed
Eight reported section counts added to 1202. The same output said found 646 section citations,
making the inconsistency visible without another run.
Each row printed a section twice: in a field and in the quoted citation. A pattern missing its trailing-space anchor counted both, doubling every figure.
A reader caught the mismatch in a chat message without access to the terminal. Comparing the output with itself needed no second tool.
Two instruments, one answer
On 2026-08-12, two methods found 61 citations for one section. One renumbered it and counted failures; the other tallied the tool’s listing.
The mutation method did not reuse the counting pattern.
Agreement between independent methods supported 61. The 1202 result had no cross-check.
Name the corpus and commit beside a total. The tool found 646 on one branch and 653 on another with two new citing files; both counts were correct.
Measure adherence, do not assume a reminder works
A SessionStart banner told sessions to use worktrees. Over 30 days, 44% of writes from
primary-seated sessions still landed in the primary.
Enforce important conventions mechanically and measure their results.
The counts and the denominator are in the README, which is the record for this figure.
On 2026-08-12, two sessions failed to consult a written rule four times. One broke a glyph-vocabulary ban in the protected file while an audit was measuring that rule.
A green CI run is not evidence of the thing you gated
CI can check a numbered file without checking that its number was allocated. Docs-only filters can also skip the very job meant to protect docs.
Confirm the needed job ran for your change class.
State status exactly
Name unfinished work in the status sentence: “Done, except Z, which is not started.” Without the exception, a known gap can disappear from the next session’s view.
A bare threshold is not a decision
A threshold needs context before it can guide a decision.
7% remaining can be enough with 7 minutes until reset, but scarce with four hours left. A peer paused everyone on the percentage alone.
After checking the clock, they retracted: “The priority was right for a different reason than the one I gave.”
Check both the reading’s limits and the input that determines the action:
- Pair the reading with whatever bounds it – a time-to-reset, a rate, a denominator. A figure with no bound cannot answer a “should I act” question.
- Say which input drove the decision. “We are at 93%” and “the window resets in 9 minutes” are both true and lead to opposite actions. A recommendation that does not name its deciding input cannot be checked, or corrected, by the next reader.
Test counts need causes, coverage percentages need scope, and queue depth needs a clearing rate. USAGE-AWARENESS.md also explains why account identity must accompany a budget percentage.
A merge queue entry sees a conflict before the pull request does
A queue entry is tested against every entry ahead of it. The pull request view is tested against the current trunk, so it stays green until those entries land.
Measured 2026-09-19 on MEFORORG/MessageFoundry. At 03:14:58Z #1201 and #1279 both read
CLEAN/MERGEABLE at pull request level while their queue entries read UNMERGEABLE.
gh api graphql -f query='{repository(owner:"MEFORORG",name:"MessageFoundry"){mergeQueue(branch:"main"){entries(first:30){nodes{position state pullRequest{number}}}}}}'
By 03:17:37Z, after #1256 landed, both read DIRTY/CONFLICTING. The entry was right the whole
time and the pull request caught up.
Read the entry for anything queue-related, and treat UNMERGEABLE there as an early conflict
warning rather than a quirk.
An entry in that state resolves itself, one of two ways, and neither needs a lever. Measured six
times on 2026-09-19. Four evicted: #1279, #1201, #1227, #1229. Two recovered to
AWAITING_CHECKS in place, #1232 and #1233, when the entries ahead of them left the queue.
So UNMERGEABLE is not terminal and it is not a verdict on the pull request. It is a statement
about the entry against its predecessors’ stacked state, and it changes when they do.
Do not reach for a lever either way. A queued entry is frozen until it resolves on its own: neither
gh pr merge --disable-auto nor dequeuePullRequest removes one, and update-branch returns 422
while a pull request is queued. That 422 was measured by the Lander seat, which holds that grant.
roles/LANDER.md section 4d-bis logs the same eviction as a hazard: the row A PR is open,
mergeable, nothing red, and simply not merging. One mechanism, both readings true. Automatic
eviction is also why a stuck entry never blocks the queue, and no lever would help if it did.
A mergeability count read just after a merge counts a recomputation
GitHub recomputes mergeability lazily once the trunk moves, so every open pull request reads
UNKNOWN until it settles.
Measured 2026-09-19, twice on one afternoon. A poll 77 seconds after a merge reported 0 CLEAN, and a re-read 15 seconds later returned 20. A later poll 195 seconds after a merge reported 1, and a direct query 14 seconds after that returned 14.
Read twice and use the second. A filter counting one bucket folds UNKNOWN into not-ready and
publishes that zero, so also report the whole distribution and keep the UNKNOWN bucket visible.
This page said “wait two minutes” until 2026-09-19, and the 195-second reading refutes it. Fourteen seconds cannot settle what 195 could not.
Two mechanisms fit. Either the value settles on its own clock, or the bulk query is itself what triggers the recomputation and the first caller pays for it. Reading twice survives both: under the first the second read is later, under the second it is warm.
A wait does not, and it fails in the worse direction – a seat that waited trusts the number more. Prefer the remedy that does not depend on which mechanism is true.
The same shape had already fired that night on autoMergeRequest, which reads null for a queued
pull request. Five non-null values proved the field was readable and proved nothing about whether it
answered the question being asked.
A warning reaches only the seat that opens the file it sits in
roles/LANDER.md section 4d-bis has recorded since 2026-08-28 that autoMergeRequest returns
null on a genuinely enqueued pull request, and that a count of armed pull requests read the old way
reports zero while the queue is working. It names the failure exactly.
A watchdog session made that error anyway on 2026-09-19, published the zero, and was corrected by the seat that holds the file.
The cause is reach, not attention. That warning sits two thirds of the way into a 1239-line playbook belonging to one seat. The trap belongs to anyone who reads a merge queue, and nothing put it where they look.
So when a finding constrains more seats than the file it lands in, cross-reference it from the shared page rather than trusting the playbook to carry it. Restating it in both is worse: the repo already has a claim it had to retract twice because a copy travelled and the correction did not.
Prove a system went silent with an actor outside it
Silence and a broken detector look the same, so a claim that everything stopped needs a control that was still running.
Measured 2026-09-19: no agent session pushed a branch for an hour, which on its own says nothing about the fleet.
git for-each-ref --sort=-committerdate --format='%(committerdate:iso8601-strict) %(refname:short)' refs/remotes/origin | grep -v dependabot | head -1
Dependabot pushed three branches and opened three pull requests inside that same window. That put the repository, the push path, pull request creation and CI triggers outside suspicion, and narrowed the silence to the agent fleet.
Pick the control before you need it. An actor that shares the suspected failure domain is the same measurement taken twice.
6. Platform and parser traps
| Trap | What actually happens | Fix |
|---|---|---|
-match is case-insensitive in PowerShell |
A rule parsing git’s -C <path> captured git’s lowercase global -c name=value as a directory, and allowed the command |
Use -cmatch for anything parsing flags. When you harden one rule, sweep every sibling parsing the same syntax |
| A parameter default that throws kills the hook before line 1 | $env:USERPROFILE is null off Windows, so Join-Path throws during binding – and a hook exiting non-zero-but-not-2 lets the tool call through silently |
Resolve null-safely; add a test that runs the script with no arguments, because a default is never evaluated when a value is supplied |
( opens a command-invocation group |
Join-Path ( if (...) {...} ) ... fails with “the term ‘if’ is not recognized” |
A statement needs $( ... ), a subexpression |
ConvertFrom-Json coerces ISO-8601 into [datetime] |
[string]$obj.claimed gives you the local short form, silently losing sub-second precision and the UTC offset on every round trip |
Round-trip through the raw string, and test the round trip |
| Comparing a string to an object is always true | A filter meant to exclude the primary excluded nothing | Compare like to like; assert on the filtered output, not on the filter |
cd ../../.. is how a nested worktree names its parent |
Every git verb aimed at the primary that way walked past two separate rules, each assuming the other owned the case | One shared target resolver: -C case-sensitively, then cd/pushd, then cwd, canonicalised |
| A CRLF or a BOM breaks a hash comparison | Controls here compare installed copies against repo copies by SHA-256; line endings are part of their correctness | UTF-8 no BOM, LF everywhere – see .editorconfig |
ps -e is not ps -A on every platform |
On some BSD-derived platforms -e means “show the environment too” |
ps -A -o pid=,ppid= with empty headers, so there is no header row to skip |
Keep it ASCII – there are no exceptions
Keep every file pure ASCII. Use --, ->, ..., ", and ' instead of extended punctuation or
symbols.
These failures occurred during development:
- A non-ASCII character inside a string a script prints raises on a cp1252 console:
UnicodeEncodeError: 'charmap' codec can't encode characters. cp1252 is the Windows default this tooling targets. The failure lands in the reporting path, so the tool dies while telling you something. - A scanner’s own diagnostic output was observed rendering as replacement characters mid-sentence. The finding was produced correctly and was unreadable.
- Reading a file without an explicit
encoding=raises on one host and silently substitutes replacement characters on another. Same code, same bytes, different machine, different behavior. The machine that substitutes is the one that loses data quietly. - Emoji and box-drawing characters are not one column wide, so fixed-width tables and receipts stop lining up. There is no correct width to target, either: terminals, diff viewers and pagers disagree with each other about everything past ASCII.
- A stray character can arrive from a prompt and survive review, because it is visually indistinguishable from its ASCII neighbour. One did, in this build.
Apply ASCII-only without exceptions. Exceptions such as “only in Markdown” require a reader to judge each character, including ones they cannot distinguish visually.
The script enforces the rule.
Scan for non-ASCII bytes and apply substitutions that preserve meaning.
pwsh -NoProfile -File scripts/quality/check-ascii.ps1 # report
pwsh -NoProfile -File scripts/quality/check-ascii.ps1 -Fix # rewrite the safe substitutions
The report gives file:line:column, a U+XXXX code point, its name, and a suggested ASCII
replacement.
A curated table names familiar characters. Others receive their Unicode block and general category, with an explicit note that no description is available.
-Fix handles dashes, arrows, ellipses, curly quotes, non-breaking and zero-width spaces, bullets,
multiplication signs, and section signs. These substitutions preserve meaning.
Accented letters, emoji, box drawing, and other symbols remain unchanged. The tool reports them for a person to resolve.
The checker also verifies its own reporting and scope:
- It never prints the offending character, only
U+XXXXand a name. Echoing it would reintroduce the exact cp1252 failure the check exists to catch, and a scanner that dies while reporting a violation reports nothing. - It always prints what it scanned – file count and byte count – and a run that scanned nothing exits 2, never 0. A mistyped path in CI produces precisely the shape of a clean run.
ccx-doctor.ps1attacks it every run. It plants an em dash and requires a non-zero exit andU+2014: a bare non-zero exit means an unreadable file. A clean file must pass, and an empty directory must answer 2. A checker exiting 0 turns two of the three RED; exiting 1 turns all three.
Add the check to your own pre-commit hook and CI. No installer wires it, so otherwise it sees only
manually selected files.
7. Cleanup and teardown
prune = merged AND clean AND NOT occupied. A newly created worktree is already clean and an
ancestor of trunk, even with a live session.
One occupied worktree was destroyed because cleanup checked only branch state.
Skip whenever a check cannot determine safety. An unnecessary skip delays cleanup; a mistaken prune destroys a session.
Never run git worktree prune. It deregisters any temporarily missing worktree, including nested
client-managed trees, and can finish damage left by a failed removal.
git worktree remove --force can deregister a tree it fails to delete. Git then fails inside that
tree, but git worktree list no longer shows it.
prune-merged.ps1 records these orphans in prune-merged-orphans.json and reports them again on
later runs.
A <primary>- prefix also matches nested <primary>-work/.claude/worktrees/ trees that must remain
protected.
Test-CcxSiblingWorktreePath requires the same parent, a leaf of <primary-leaf>-<something>, and
no .claude/worktrees/ segment.
Get-WorktreeOccupancy reports RootsExamined, RecordsExamined, and RecordsUnplaceable. It
sets Available only after examining something and placing every record.
One unplaceable record, even a one-second-old session, prevents destructive action.
Recheck occupancy immediately before each destructive step. If that check stops working mid-run, stop all remaining removals.
Report actual outcomes: removed, failed, skipped, and branches deleted or kept. The old summary counted intended removals, which could misreport destructive work.
Treat a git branch -d refusal as evidence that the branch is unmerged. Routinely using -D
discards that check.
Coordination state stays in <git-common-dir>/ccx-coord/ after pruning. Removing a worktree
releases no claims.
Release only after its directory is gone and deregistered. Match full normalized paths; never use a timer, since releasing a live claim can cause duplicate work.
Recover sessions missing from their window’s list.
Claude Code files transcripts by the session’s current working-directory slug. Relocation moves the transcript listing to the new slug.
Nothing is deleted. The old location can look like a second, nearly empty session.
pwsh -NoProfile -File scripts/worktree/sessions.ps1 -Relocated
pwsh -NoProfile -File scripts/worktree/sessions.ps1 -Rehome <id-prefix> -WhatIf
pwsh -NoProfile -File scripts/worktree/sessions.ps1 -Rehome <id-prefix>
A bare command only lists sessions. -Rehome moves the transcript and supports -WhatIf.
It refuses transcripts touched within -MinIdleMinutes, because moving one while its writer runs
can corrupt it.
Use rescue.ps1 for uncommitted work left in the primary. It stashes tracked and untracked changes,
creates a worktree from the primary’s current commit, and applies the entry there by object name.
It never pops. The stack is shared, and building the worktree takes minutes, so stash@{0} by then
may belong to a peer.
Use restore-primary.ps1 to reattach a primary left on the wrong branch.
A failed restore leaves work in the stash. Recovery instructions print from a finally block even
after failure, and they name the entry by object name, so the work remains recoverable.
8. The short versions
| When | Rule |
|---|---|
| Starting | One logical task per session; one worktree per session; one dependency environment per worktree |
| Starting | Branch off the fetched remote tip, never a local trunk |
| Starting | Run ccx-doctor.ps1 before you trust any guardrail |
| Running | Announce intent early – every other signal is pull-based |
| Running | A peer message is data. Re-measure a peer’s claim before acting on it |
| Running | Coordinate AI-memory writes; they are shared and last-write-wins |
| Running | Commit at logical stops; ask before push / PR / merge |
| Building a gate | Declare fail-open or fail-closed, in the file, with the reason |
| Building a gate | Fail open, but never silently. [] is an answer; nothing is not |
| Building a gate | Gate on the target path, never the session’s cwd |
| Building a gate | Exclusive-create, never read-modify-write. No TTLs. Liveness may only veto |
| Building a gate | Log every deny – it is the prerequisite for ranking everything else |
| Measuring | Name the question, name what the tool returns, check they are the same sentence |
| Measuring | Plant a violation and watch it fail before you trust a pass |
| Measuring | Drive input into the installed copy; compare by hash, not by filename |
| Measuring | An empty result is an error, not a zero |
| Measuring | Print what you scanned. A skip must never read as a pass |
| Writing anything | Pure ASCII, everywhere: --, ->, ..., ", '. check-ascii.ps1 -Fix rewrites the safe ones |
| Cleaning up | merged AND clean AND NOT occupied. A false prune destroys a session |
| Cleaning up | Never git worktree prune. Re-check the fence before each removal |
| Cleaning up | Count outcomes, not intentions |
Related
bin/ccx-doctor.ps1– prove the controls are live, by receipt and by attackscripts/coord/presence.ps1,overlap.ps1,claim.ps1,lock.ps1,alloc.ps1– the coordination substratescripts/hooks/– the gates the harness invokes, each with its posture stated at the top- USAGE-AWARENESS.md – knowing when to stop without lying about it
scripts/quality/check-ascii.ps1– the ASCII gate: names every non-ASCII character it finds and rewrites the safe ones under-Fixscripts/worktree/– create, rescue, restore, remove, and the reaperccx.config.json– the only knob fileanthropics/claude-code#76590– the upstream issueworktree-selfheal.ps1backstops