Skip to content

fix(coord): the statusline installer wrote a config root no session reads, and five accounts shared one publish path - #650

Open
wshallwshall wants to merge 11 commits into
mainfrom
claude/admiring-proskuriakova-c34bbf
Open

fix(coord): the statusline installer wrote a config root no session reads, and five accounts shared one publish path#650
wshallwshall wants to merge 11 commits into
mainfrom
claude/admiring-proskuriakova-c34bbf

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Two defects in the usage tooling, the second found while fixing the first. Authored by the statusline seat; opened by the LANDER on their handover. Not armed — see the last section.

The install lied and the reader was right

install-usage-statusline.ps1 wrote ~/.claude/settings.json unconditionally and reported INSTALLED (user level -- every session on this machine). Claude Code reads settings from the root named by CLAUDE_CONFIG_DIR, which every launcher on this box pins, so the statusLine never fired — while usage.ps1 correctly reported the collector as not installed.

An install success followed by a reader saying it was never installed. Two instruments disagreeing, and the wrong one was louder and earlier.

The publish path was shared across five different accounts

A config root holds one credential set and therefore one Anthropic account. This box runs five roots with five different account emails and five separate 5h/7d pools — all publishing to one file. That is last-writer-wins across unrelated quotas.

The worse half: it disarms usage.ps1's staleness guard, because another account keeps the file warm. The guard looks present and is not.

Publish path is now <config root>/mefor-usage, derived by one shared function that the collector, the reader and the installer all call. Provenance travels per window, not per document, so a carry-forward cannot launder one account's percentages into another's.

Verification, reported by the author

  • 69 tests. ruff check, ruff format --check, mypy strict all clean.
  • Fifteen fixes each validated against their own reversion — delete the fix, the test goes red.
  • Two adversarial review rounds; the second, in isolated worktrees, found seven real defects, fixed in the last two commits.

Behaviour change to know about

A bare usage.ps1 now answers for this session's account rather than for the box. -AllRoots surveys every root. docs/WORKTREES.md is updated in the same branch.

After merge, the installer must be re-run per root, or with -AllRoots, from a plain terminal — every root currently carries a legacy command with no -StateDir.

Stated so the merge is not misread as fixing it

Nothing publishes on this box at present, and this branch is not the cause. latest.json sat frozen for 47 minutes across nine live sessions, every root correctly wired. The instrument was validated first — a fire carrying no rate_limits still rewrites the file — so this means not firing at all, not firing empty. Most likely the desktop client renders no status bar; unconfirmed. No seat's hook reads that file, so it costs nothing operationally today (Steward's measurement, three ways).

LANDER notes

Checked before opening: tip b35157d16 matches origin exactly, 10 ahead / 0 behind a490993b4, and the file set is disjoint from worktree_gate.ps1 and from messagefoundry/ — so no interaction with the held set (613, 615, 636, 640) or with b1-1067.

This PR is deliberately NOT armed. Review approvals required on this repo is 0, so arming merges unread. The owner's sign-off reached me relayed through the author's session rather than in my own, and opening a PR is reversible while arming is not. Arming is the owner's to trigger, or to tell me directly.

wshallwshall and others added 11 commits August 27, 2026 18:01
A box can run several Claude config roots at once -- ~/.claude for a bare
`claude`, and one ~/.claude-account-<N> per launcher, each pinned through
CLAUDE_CONFIG_DIR. Three scripts in scripts/coord need the same three answers
about that: which roots exist, which one am I in, and where does this root's
usage state live. Answering separately is what let install-usage-statusline.ps1
write ~/.claude/settings.json while every session on the box read a pinned root.

The predicate itself is not new -- it is carried from install-gate.ps1 and its
Python twin in tests/test_gate_installed_parity.py, along with both measured
incidents behind its anchors, rather than re-derived a third time.

One correction to that inheritance: the array return drops the comma. The comma
is right for install-gate.ps1's HashSet, where unrolling destroys the type, and
wrong for an array. Measured: with `return ,@(...)`, a caller writing
`@(Get-ClaudeConfigRoots ...)` gets ONE element that IS the array, whose string
form is every path joined by a space. That reached this branch as a single
-AllRoots target named "<root-1> <root-2>\settings.json", reported as
"Roots examined: 1", and it survived a smoke test because Split-Path happens to
accept arrays.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eads

It defaulted -SettingsPath to ~/.claude/settings.json and printed "statusLine
INSTALLED (user level -- every session on this machine)". Claude Code reads
settings from the root named by CLAUDE_CONFIG_DIR, and every launcher on the
box this was written for pins one, so the claim was false: the statusLine never
fired, nothing ever published, and usage.ps1 correctly reported the collector as
not installed. An install success followed by a reader saying it was never
installed -- two instruments disagreeing, the wrong one louder and earlier.

Five rules now decide the target set, first match wins: explicit -SettingsPath,
explicit -ConfigDir, -AllRoots, the CLAUDE_CONFIG_DIR pin, then ~/.claude.
-AllRoots is opt-in and covers account roots only, diverging from
install-gate.ps1 (which defaults to all) because a security gate fails by
under-reach while this writes into vendor-owned directories belonging to
different accounts.

The message is the other half of the fix, and it is why this was expensive to
find. "every session on this machine" is deleted rather than conditioned -- a
completeness claim is a liability, and naming each file written is shorter and
true. Every line is now paired with a check that backs it, WROTE and REWIRED are
distinguished, and no line says "publishes to" when all that happened is that a
settings key was written.

Four defects found while building it, each measured rather than reasoned:

- Explicitness must be captured at SCRIPT scope. A function's own
  $PSBoundParameters is EMPTY, so a resolver testing it there returns False for
  every caller -- and `-SettingsPath <fixture>` from the test suite would have
  been ignored in favour of the caller's live pinned root.
- Get-Settings closed over a script-scope $SettingsPath, which turns dangerous
  the moment that parameter is [string[]]: `Test-Path -LiteralPath @(a,b)`
  returns "True False", `-not` on it is False so the guard PASSES, and
  Get-Content -Raw over two existing files returns them CONCATENATED, which
  ConvertFrom-Json accepts as an array of two objects. It is now read per path.
- The ownership test had to stop being a substring match. The wired command now
  contains "\mefor-usage" inside its -StateDir argument, so `-like
  "*mefor-usage*"` would judge any foreign statusLine that merely mentions the
  publish path to be ours, and silently replace it in up to five roots at once.
  It is anchored on the first line, and three-way: a statusLine present with an
  empty command is NONE, not FOREIGN, which was a state with no exit.
- ConvertTo-Json -Depth 20 truncation is NOT caught by parsing the result back.
  Measured with a 24-level document: it warns, and the truncated text still
  parses cleanly with the deep node replaced by its type name. Both guards run.

-Status and -Uninstall follow the same five rules. -Status now reads each root's
publish path and collector back OUT of its wired command instead of recomputing
them, reports login markers, and runs before the git guard, because auditing is
not installing. -Uninstall going single-root under a pin was the worse lie of
the two: the operator believes they turned something off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
usage-collect.ps1's header asserted "the quota is ACCOUNT-WIDE, so any ONE
session's reading is the truth for all of them" and published to a single
user-level file. That premise is true per Anthropic ACCOUNT and false on a box
running several config roots: a config root holds one credential set and
therefore one account, and the box this was written for has five roots carrying
five different account emails and five separate 5h and 7d pools.

One shared file across them is last-writer-wins across unrelated quotas, and the
damage compounds. The percentage flaps. The carry-forward can leave five_hour
from one account beside seven_day from another in one document. And usage.ps1's
staleness guard never fires, because some other account keeps the file warm --
so the guard looks present and is disarmed, which is the worst of the three.

Measured before changing anything: latest.json and history.jsonl were absent in
all six roots, so nothing had ever published and none of this had fired yet. The
installer's new -AllRoots is what would have armed it, which is why the two ship
together rather than in sequence. No migration and no fallback: there is no file
to move, and a fallback reading the old path would serve the unpinned root's
document to a pinned session -- the exact bleed being removed.

The publish path is now <config root>/mefor-usage, derived by one function that
the collector, the reader and the installer all call. The filesystem is the
partition key, so two roots cannot collide however CLAUDE_CONFIG_DIR is spelled.
~/.claude/mefor-usage keeps its existing contents untouched; only latest.json
and history.jsonl are scoped.

The published document now records where it came from, in three fields, because
one would detect nothing. config_root is a label derived from the write path, so
it agrees with that path by construction. config_root_env is the ambient pin
read live, and it is what usage.ps1 gates on: a document whose config_root_env
names a root other than the one it sits under is refused, not reported. Whether
CLAUDE_CONFIG_DIR reaches a statusLine child process is UNMEASURED, so a
document with no stamp is read and labelled UNVERIFIED rather than refused --
absence of provenance and wrong provenance are different facts.

usage.ps1's "no data" message replaces one sentence with five diagnosed states.
The old one said "not installed or has not run yet" and printed the bare
installer command with no root, so following its advice re-ran the invocation
that produced the false INSTALLED claim. The state that matters most is
WIRED_ELSEWHERE: without it the reader tells the operator to restart and wait,
forever, while nothing anywhere says the two halves disagree.

Two guards that are easy to leave out. New-Item -ItemType Directory -Force
creates every missing ancestor, so an unvalidated pin would have a live session
manufacture a config root nothing can launch from; the collector now creates the
leaf only, against a parent that already exists. And usage.ps1 runs under
SilentlyContinue, which turns a failed dot-source into a confidently wrong
answer rather than an error, so it refuses on the library, not on the path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Thirty-five tests, driven as real subprocesses against a fixture home shaped
like the real box -- including all three kinds of look-alike directory, because
each defeats a different plausible predicate. .claude-account-2.lock carries a
settings.json and no .claude.json, so "has settings" adopts it. .claude-desktop-N
carries a .claude.json and nothing launches from it, so "has .claude.json"
adopts four directories no session can boot from.

Two tests exist because a fixture can lie. The pin is POPPED from the child
environment rather than merely overwritten: this suite runs inside a Claude Code
session, which on the box in question is itself pinned, so a child would inherit
CLAUDE_CONFIG_DIR and every "no pin" test would quietly exercise the pinned path
and pass. And -HomeDir is passed rather than USERPROFILE overridden, because
measured, [Environment]::GetFolderPath('UserProfile') ignores that override --
so a script resolving home itself could not be redirected, and -AllRoots would
enumerate and WIRE the real account roots. One test asserts both properties
directly, since everything else here depends on them.

The anchor test runs the whole chain with nothing recomputed by the test:
install into a pinned root, execute the string the installer actually wired
exactly as Claude Code would, read it back as a session in that root would, and
confirm a second root still sees nothing. A hand-written stamp would have let it
pass whether or not the collector produces one.

One existing test changes and gets stronger. It used to rewrite the wired
command to inject -StateDir, because the command carried no publish path; now
that the installer bakes a per-root path in, that injection binds -StateDir
twice and pwsh refuses it. Deleting the rewrite is what lets the test run the
production string verbatim, which its own docstring always claimed it did.

Stated rather than implied: the -Force on the directory glob is structurally
untestable here. Its absence is invisible on Windows and collapses the set to
nothing on Linux, and this module is skipif'd to nt, so only a CI ubuntu leg
could ever observe it. This file has none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…h one

The "one publisher, N readers" paragraph asserted the publish path is user-level
"because the data is a property of the account, not of a checkout". Half right:
it is a property of the ACCOUNT, and a config root is what selects one. On a box
running several roots the user-level path merges five accounts into one file.

Also pins the predicate against its other three copies rather than migrating
them. config-roots.ps1 exists so install-usage-statusline.ps1 would not write a
fourth, but it could not simply absorb the rest: install-gate.ps1 pairs its copy
with a deliberately WIDER audit population that must not be selected by the
predicate it checks, and tests/test_gate_installed_parity.py holds that copy in
parity with a Python reader. Folding them in is its own migration with its own
test surface, so the rule is asserted as one BEHAVIOUR across all four instead --
an edit to any of them turns the test red rather than going silent.

The one place they genuinely differ is named rather than left latent:
install-coordination.ps1 uses -match, which is case-insensitive, while
[regex]::IsMatch is not, so a `.Claude-Account-2` would be accepted by one and
rejected by the other. No such directory exists here, and the installer's -Status
audit reports any ~/.claude* directory with a settings.json that the predicate
rejected, which is what keeps an under-reach loud.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ALL THREE ARE THE SAME SHAPE AS THE BUG THIS BRANCH EXISTS TO FIX -- an
instrument reporting something it had not checked -- which is why they are worth
their own commit rather than a squash.

1. -Uninstall counted a root as removed OUTSIDE the ShouldProcess guard, so
   `-Uninstall -AllRoots -WhatIf` printed "removed: 5" and exited 0 having
   removed nothing. An operator dry-running before committing reads that as "the
   collector is off" while every root keeps publishing, and a coordinator
   branching on the documented exit codes gets 0, which the header defines as
   every root reaching the desired state. Worse than the install claim this
   branch deletes, because a person believes they turned something OFF. The
   counter moves inside the guard and a WOULD REMOVE line and column appear
   beside it, mirroring the install path.

2. The provenance stamp described the WRITER of the document, not the ORIGIN of
   the numbers in it, and the carry-forward is exactly the hop between the two.
   Root A publishes into root B's directory; B's next session fires before its
   first API response, carries A's percentages forward and rebuilds published_by
   with B. A document-level check then compares B against B, passes, and reports
   A's headroom as B's. The window kept its older captured_at and would have been
   called stale eventually -- never FOREIGN. So the origin now travels WITH the
   window, exactly as captured_at already does: a freshly observed window is
   stamped, a carried one keeps the stamp it arrived with, and usage.ps1 applies
   the refusal per window as well as per document.

3. The WIRED_ELSEWHERE diagnosis was computed only inside the no-data branch,
   which made it unreachable in the case where it misleads most. A root re-wired
   to publish into a sibling still holds its own older latest.json, so the reader
   served that stale percentage as current for twenty minutes and then said "no
   live session is publishing" -- also false; the session is publishing, just
   elsewhere. The diagnosis now runs on every invocation and a mis-wire prints
   ABOVE the numbers, because saying it afterwards is too late to stop the wrong
   decision.

Also from the same review: Get-ClaudeConfigRoots was a name already taken in
scripts/coord by session-registry.ps1, which eleven scripts dot-source and which
answers a different question (roots that have RUN a session, by a sessions/
subdirectory) with a different parameter set. Sharing the name would make the
winner depend on dot-source order, and in one direction silently: that function
resolves home from $env:USERPROFILE itself, so a caller expecting the -HomeDir
seam would enumerate the real home. Renamed to Get-LaunchableConfigRoots.

And a "skipped" tally column that nothing could increment: the catch reports a
failed backup as FAILED, so the column was structurally always zero, which reads
as "nothing was skipped" -- a claim about the run rather than a fact about the
code.

Each fix is pinned by a test validated against its own reversion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The same defect class as the rest of this branch, found by the same review.

- The install path printed a "backup <path>" line for every root, including ones
  that had no settings.json to copy. An -AllRoots run over five roots would send
  an operator looking for five backups and finding two. Write-SettingsFile now
  reports whether it took one and the caller prints accordingly, naming the
  no-backup case explicitly rather than staying silent about it.

- -Status reported an unparseable settings.json as "carries no statusLine of
  ours". A corrupt file is not a clean one: it may carry a working statusLine the
  audit cannot see, so that wording steers an operator AWAY from a stray
  publisher. Both the per-root report and the independent audit now say the file
  could not be parsed and that ownership is therefore unknown.

- usage.ps1's staleness message asserted "no live session is publishing", which
  nothing in the script checks and which is often false -- a session may be
  publishing perfectly well, into another root. That is now the mis-wire warning's
  job, printed above the numbers; the staleness line states the age and stops.

- The -AllRoots survey heading read "Every config root on this box" while
  enumerating by an anchored, case-sensitive name shape with no second opinion
  beside it. A root spelled `.Claude-Account-7` and burning quota would be absent
  from a list an operator reads as complete. The heading now names its rule and
  its bound, and a second pass selected by a DIFFERENT rule (any ~/.claude*
  directory carrying a settings.json) names anything the survey did not cover --
  so the survey can no longer confirm only its own predicate.

- A CLAUDE_CONFIG_DIR naming a directory that does not exist was diagnosed as
  "this root has no settings.json", whose remedy is an installer invocation the
  installer REFUSES, because it will not create a config root. The operator
  follows the advice, gets a refusal, and nothing has named the actual fault.
  NO_SUCH_ROOT is now its own state and its remedy is to fix the variable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deleting the staleness line's "-- no live session is publishing" was right: nothing
in the script checked it, and it is false whenever a session is publishing fine into
another root. But the WARNING that replaced it fires on a MIS-WIRE, and a healthy
root reports WIRED_HERE, so the commonest real failure fell through to a bare
"reading is N min old" -- the line a reader skims past.

That is not hypothetical. Measured on this box tonight: every root wired, every
collector present, and latest.json frozen for 47 minutes across nine live sessions.
The instrument was validated first -- a statusLine fire carrying NO rate_limits still
rewrites the file, because the carry-forward stamps a fresh captured_at -- so a frozen
mtime means not firing at all rather than firing empty.

So wired-and-silent gets its own arm, and every clause in it is backed by a check
already run: "wired, and the collector it names exists" is what WIRED_HERE tested
(ownership, an extracted -StateDir matching this directory, Test-Path on the
collector); "nothing fresh" is the window states. The two possible CAUSES -- no
session started here since wiring, or a statusLine that never runs -- are offered as
alternatives rather than asserted, because nothing available can tell them apart, and
the reader is pointed at the one instrument that can: the status bar itself.

The properties the staleness guard already provided are asserted alongside it in the
same test, because this arm sits next to them: the number still shows, its age still
shows, and it is still never projected from.

Found by the Steward seat, whose stated concern is that a quiet plausible stale
reading is more dangerous than a loud failure -- and which pointed out that the case I
had just measured was precisely the one my replacement did not cover.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every one is the same shape as the bug this branch exists to fix: a guard added
in one place and not its twin, or a line asserting a check that never ran.

1. THE SURVEY NEVER APPLIED THE PER-WINDOW REFUSAL. Get-RootSummary checked only
   the DOCUMENT stamp, which a carry-forward passes -- the document is written by
   this root and sits under this root while the numbers inside came from another.
   Measured: in one run the single-root reader refused both windows with
   "refusing to report another account's headroom" and the survey twelve lines
   lower printed those exact percentages as the other root's own. The survey is
   the ONLY place accounts are compared, so it is where the refusal matters most
   and it was the one place it did not run. The test is now one function,
   Test-WindowFromRoot, called by both paths.

   NOT by calling Get-WindowReport per row, which was the obvious fix and is
   wrong: Get-Rate closes over the reader's own $histPath, so every other root's
   row would compute its burn rate from this root's history.

2. THE SURVEY AGED ROWS BY THE DOCUMENT STAMP, which the collector rewrites on
   every fire including a pure carry-forward. A six-hour-old reading rendered as
   "[seen 0 min ago]" -- a stale number wearing a fresh timestamp, the exact shape
   honesty rule 1 forbids. Rows are aged by the older of their two windows, and a
   row past -MaxAgeMinutes renders STALE.

3. THE VERDICT IGNORED THE WIRING DIAGNOSIS. A root the script itself called
   mis-wired printed the WARNING and then exited 0 with state=OK, beside
   statusline_state=WIRED_ELSEWHERE in the same document. Two instruments
   disagreeing inside one script. The state list is hoisted so the prose and the
   verdict cannot drift, and an untrusted diagnosis makes the reading UNKNOWN. An
   explicit -StateDir is exempt: there the operator named the directory.

4. THE WIRED-AND-SILENT WARNING USED -or WHERE IT NEEDED -and, so it fired on a
   document published 0.15 seconds earlier: a fresh five_hour beside a seven_day
   that had simply never been published leaves one window UNKNOWN, and the arm
   claimed "nothing fresh has published here" over a reading taken moments ago.

5. THAT SAME WARNING ASSERTED "the collector it names exists" WHEN NONE WAS
   NAMED. A wired command carrying a -StateDir but no recognisable collector
   assignment yields a null collector path, which skips the Test-Path guard
   entirely -- so the clause asserted a check that never ran. It is now
   conditional on one having been named.

6. test_opus_is_not_claimed_as_a_blind_spot COULD NOT FAIL FOR ITS OWN DEFECT.
   Written as a single disjunction, re-inserting the exact false sentence while
   leaving the "NOT gaps" text in place kept the expression true and the suite
   green. Split into two independent assertions.

7. TWO INSTALLS IN THE SAME SECOND DESTROYED THE ONLY PRE-INSTALL BACKUP. The
   stamp has one-second resolution; measured, run 2 overwrote run 1's original
   with run 1's post-install content. A backup holding post-install content is
   worse than none, because the operator is told one exists. The backup now picks
   a free name and the output reports where the copy actually landed.

Two findings were REFUTED by their verifiers and are not acted on: that -AllRoots
writes the ambient CLAUDE_CONFIG_DIR root despite -HomeDir, and that a live root
points at a temp checkout.

One test fixture changed rather than the code: a root holding a fresh document
while carrying no statusLine is a state that cannot occur, since something must
have been wired to publish there, and with the diagnosis now reaching the verdict
that contradiction made the test assert OK over a root the script calls unwired.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The wiring diagnosis now reaches the verdict, and the assertion pinning that went
into the no-data test next door -- which exits 20 because there is nothing to
read, whatever the diagnosis does. It could not have failed.

Moved to the test whose fixture can actually distinguish the two: a mis-wired root
holding a FRESH reading. Without the fix that is a clean OK and exit 0, printed in
the same run as a WARNING and statusline_state=WIRED_ELSEWHERE.

Found by reverting the fix and watching the test stay green, which is the only
reason it surfaced. The first fail-first pass reported it green too and I read
that as "the test does not pin it" -- but three of the five reversions had
silently not applied at all, because inline `python -c` inside a double-quoted
bash string ate the dollar signs before python saw them. A reversion that does not
apply is indistinguishable from a test that does not pin, and it is the more
flattering of the two readings. The reversion helper now fails loudly when its
target string is absent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by the owner running -AllRoots against their real roots. One block read:

    replaced a mefor-usage statusLine that published somewhere else
    was: ...\.claude-account-5\mefor-usage
    now: ...\.claude-account-5\mefor-usage

The publish path had not moved at all. The COLLECTOR had. A sentence
contradicting the two lines printed under it is exactly the defect this branch
exists to remove, shipped inside the fix for it.

The cause is that REWIRED is entered whenever the command is not byte-identical,
which is a WEAKER fact than any single reason for it, and the message named one
reason unconditionally. The reason is now DERIVED from what actually differs --
no -StateDir at all, a different publish path, a different collector, or
something else -- and when the collector is what changed, both paths are printed
so the operator can see which one moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant