You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Added
Codex, beside Claude Code. pitboard enroll codex/work records the Codex account signed
in now, reading its account, email and plan out of its own ID token with no network
call; pitboard enroll codex/work --sign-in runs codex login in a private CODEX_HOME so the account in use stays signed in; pitboard use codex/work switches
it; and pitboard shows each Codex account's five-hour and weekly limits from the same
usage read Codex itself makes, which spends no quota. Until a Codex account is enrolled,
pitboard reads nothing of Codex's and asks OpenAI nothing. Everything pitboard does for Codex
was read out of codex-cli 0.154.0 and is dated in a register of its own, checked against
every new build by the conformance run twice a week, and still green on 0.156.1.
Codex is not Claude Code, and pitboard says where they differ rather than hiding it:
A running codex never notices a switch, so a switch says to restart it instead of
counting down, and names how many sessions are still using the outgoing account.
codex login and codex logout revoke the stored refresh token, so a Codex park is
never a copy: the outgoing login is moved into the vault and read back before
anything replaces it. Signing out inside a session still running from before a
switch would revoke the login just parked, and the switch says so.
Codex's keychain stores are items Codex made for itself, which every read by another
program would put a permission prompt in front of, so pitboard handles Codex's
default store, the auth.json file, and refuses the others with a reason.
Codex takes no lock, so a session still running from before a switch can refresh its
token in the middle of one. The live login is read again just before it is replaced,
and nothing is written over a login that moved; a login that ends up naming two
accounts is refused rather than parked.
The menu bar app handles both tools. With accounts of both, the panel lists them under a
heading per tool, and the menu bar follows the signed-in account closest to running out.
When an account runs out, only another account of the same tool is offered. A Codex
switch has no countdown: the panel says running codex sessions keep the old account,
and how many pitboard found, and keeps saying so until that tool switches again rather
than until anything at all changes. "Add another account" asks which tool when more than
one is installed, and a Codex sign-in shows the address codex login printed. Cancelling
a sign-in no longer holds the app until the tool says something, which a Codex sign-in
never does before the browser is done.
Labels belong to a tool. work can be a Claude Code account and a Codex account at
once, codex/work says which, and a bare work still means what it always did as long
as it names one account; where it names two, pitboard lists both rather than picking.
A bare name for a new account means Claude Code, so every command written before there
was a second tool does what it did. New codes: provider_unknown, label_ambiguous, sign_in_not_isolated, live_store_unsupported, state_names_unknown_tool, recovery_elsewhere, sessions_still_running, codex_program_missing and codex_not_found.
No parked login goes unnamed. Every name pitboard is about to write a login into is
written down first, and the next command resolves any that nothing refers to: given back
to the account whose name it carries when that account holds nothing, deleted when nobody
wants it, and left alone when the store could not be read. Before this, a run killed
between writing a login and recording it left a live refresh token that no entry named,
never renewed, never deleted by pitboard uninstall, and on macOS not listable by any
tool a person has. pitboard doctor reports anything still outstanding.
pitboard adopt takes over a ~/.pitboard that another computer wrote. The machine
stamp is right and its reason is sound, but the refusal stopped every command including uninstall, so somebody whose home and keychain arrived through Migration Assistant met
a tool that would not do anything and an error telling them to enrol again with no
command that made it possible. Adopting keeps what is a fact about an account, the label,
the email, the uuids and the remembered numbers, and drops every parked login, because a
login belongs to the computer that signed in. It deliberately does not ask Anthropic
whether a parked token still works: finding out means exchanging it, and exchanging it is
the act that would rotate it past the other machine's copy.
pitboard repair asks the credential store itself what parked logins are on this
machine, rather than reading pitboard's own index, and accounts for every one it finds:
given back to the account whose name it carries, or deleted when no account here wants
it, or reported and left exactly where it is. Only a name this pitboard wrote down itself
is ever deleted: a keychain belongs to a whole login session while pitboard's records
belong to one PITBOARD_HOME, so a parked login it cannot account for is evidence of
another pitboard rather than of an orphan, and deleting it would end that account's
session for somebody who never ran the command. Giving one back is additive and safe on a
guess; deleting one is not, so on macOS a login given back that this pitboard never wrote
down is deleted only once pitboard has used it, by switching to it or renewing it, even
when the renewal is stopped before it records what it got back. Parking over it, forget
and uninstall leave it where it is, and uninstall says how many it left, as parks_left in --json. Elsewhere the vault is a directory inside pitboard's own, which
no other pitboard parks in, so whatever repair finds there is this one's. Measured
first: security dump-keychain without -d never prompts, takes 0.06 seconds, emits no
secret of any item, and does not slow later reads.
A crash matrix: every durable step of a switch, an enrolment, a renewal and a forget,
killed where it stands, recovered, and checked against what must be true afterwards
rather than against a particular outcome. Every case runs recovery twice, because a
recovery that only works once leaves a machine nobody can fix, and once with Anthropic
unreachable, because the answer then must be to change nothing. The two windows above
are what it found on its first run.
A failure that came from asking Anthropic now carries a cause beside its code, in both --json and the app's bindings: unreachable, rate_limited, server_error, answer_not_understood, login_refused or token_expired, each saying whether asking
again is worth anything. Before this, everything that was not a 401 arrived as identity_unverifiable and a sentence of prose, so nothing could tell being offline from
being rate limited from a login Anthropic had finished with. status tells the same three
apart too, where they used to share one code.
The app follows a switch made anywhere else on the machine. Three front ends ran on one
machine and none could tell when another had changed something, so a switch typed in a
terminal left the menu bar naming the account the person had just stopped using for as
long as five minutes, with a button offering a switch that had already happened. The app
now asks every couple of seconds when pitboard's account index last changed, which is one
stat of one file, and re-reads what it already knows when it moves: no network, no
keychain and nothing asked of Anthropic. Deliberately the index alone and not the whole
directory, because the status line writes usage readings after every message in every
open session.
The app has a window and a Settings scene. The panel is 400 points wide and everything
that needed more than that either expanded inside it or sent the person to a terminal,
and everything configurable lived in an ellipsis menu where opening at login sat between
hiding the checks and quitting. The window holds each account with what its limits have
been doing, the whole log of what pitboard has changed, and all of what it found about
this machine. Settings opens with Command-comma where people look for it, and reaches the
scheduled renewal the core could already do and the app could not.
A first run for somebody who installed only the app. The cask puts the command line on
the machine too, but an empty panel used to say "Run pitboard enroll <label>" and a
machine without Claude Code said one line of error, which between them sent every new
person to a terminal to find out whether the thing they had just installed worked. The
app now names the state it is in, no Claude Code, nobody signed in, signed in but
unnamed, or one account with nothing to switch to, and offers the one next step, each of
which it can do itself. Nothing is asked before the first read. The first launch ever
opens the window, because a status item is invisible to somebody who has just installed
it.
doctor reads the modes of everything on the disk that holds a login: the plaintext
credential file, pitboard's vault and every parked login in it, and fails when anyone
but the owner can read one. Where there is no keychain, a mode bit is the whole of that
protection, and a backup restore, a cp -r or a careless umask changes one quietly.
The app's bindings reach the rest of the core: the offline report, giving up on an
interrupted switch, the change log, renewing, and the renewal schedule. A read that cannot
reach Anthropic now falls back to the last numbers measured rather than an empty panel,
which said the accounts were gone. An interrupted switch that recovery cannot finish has
a way out in the panel rather than sending somebody to a terminal, which is the one place
a person who installed only the app has not got. And every warning is shown rather than
the first: a switch can warn about an overriding environment variable and a config that
did not update at once, and showing one of them is how somebody fixes the wrong thing.
pitboard keeps what each account's limits have been doing, and says how long each account
lasts. The single decision this tool exists to support is which account to use next, and
it answered with two instantaneous percentages and left the arithmetic to the person: 73%
of a weekly limit means nothing without knowing whether it was 40% this morning. It
already had the data and threw it away, one snapshot per account in a map every write
replaced. Each account now has a series, and from it the only number that answers the
question: until the tightest limit fills at the rate it has been filling, or until it
resets, whichever is sooner. It says nothing until there are three readings a quarter of
an hour apart, because a wrong runway tells somebody to switch when they need not, which
is worse than no runway. Sized before it was written: one reading is 319 bytes, a reading
that says nothing new is not kept, and anything older than a fortnight goes.
pitboard doctor says what is taking up the room in a login that will not fit, largest
first and named per MCP server. A login too big for security's standard input is almost
never the login: on one real machine the OAuth block was 506 bytes and eleven MCP server
tokens were 3679. Saying "8503 of 4032 bytes" left a person to guess which of those to
sign out of, and the answer was in the document pitboard had already read.
pitboard status names the credential slot it is speaking for, in --json always and in
the human report when it is not the default. CLAUDE_CONFIG_DIR selects a different
keychain item, so who is signed in is a fact about one slot and not about the machine;
the report named accounts without ever saying which slot it meant.
pitboard renew renews every parked login that is due and does nothing else, and pitboard schedule install hands that to the platform's own scheduler, a LaunchAgent on
macOS and a systemd user timer on Linux. Until now the only two things that renewed a
parked login were somebody typing pitboard and the menu bar app's poll, so the tool was
safe for a macOS user who leaves the app running and quietly unsafe for everyone else:
go away for the refresh window and every parked login is dead. It is opt-in and stays
opt-in, it runs one verb, it never switches and never asks for usage, and RunAtLoad is
off because installing it is not a reason to talk to Anthropic that second.
pitboard doctor says when an account has not been switched to for longer than a refresh
token's own life. pitboard renews a parked login for as long as its account is enrolled,
so one enrolled and forgotten keeps a live, continuously rotated token on the machine
indefinitely, and nothing said so. Nothing is dropped on a timer pitboard chose: the
threshold is the token's own lifetime and all the check does is say it.
pitboard owns how often it asks Anthropic anything. status asked about every enrolled
account plus the live login on every run with no memory of having just asked, and the
menu bar app asked the same questions every five minutes, on every wake and on every
panel open, from a process that knew nothing about the command line's; nothing honoured Retry-After, so a 429 became a stale row and the identical request went out on the next
tick. Two accounts and a running app is on the order of six hundred authenticated
requests a day nobody asked for, and it is the part of pitboard's behaviour that reads
least like a person switching between their own accounts.
An account is now asked about again once the tightest limit it describes could have moved
by a percentage point, which is three minutes for a five-hour window and derived from the
window rather than picked. A refusal is recorded and waited out, with Retry-After
believed over anything pitboard would choose, and the wait is shared by every front end
on the machine. pitboard status --fresh asks anyway; a wait Anthropic asked for is not
overridden. pitboard doctor says what is being held back and for how long.
A conformance checker, and a job that runs it. pitboard-conformance reads the literals
each of pitboard's facts about Claude Code is readable by out of a Claude Code build and
says which are still there; a scheduled workflow fetches the newest build twice a week
and runs it. Shallow on purpose, and it says so: a literal being present does not prove
the behaviour around it is unchanged, while a literal disappearing does prove something
moved. Measured across six builds before it was trusted at all: the probe set holds from
2.1.273 through 2.1.278 and correctly goes red on 2.1.124, which predates the credential
write lock, two of the five account-scoped keys, and the keychain error classification.
A park holds the account's whole slice of Claude Code's credential document rather than
its OAuth block alone: the four other keys a logout deletes go with it, and a switch back
puts them there. Before this, switching away deleted them and switching back could not
restore them, so an account came back to Claude Code with slightly less than it left.
Parks written by earlier versions still restore, and still behave exactly as they did.
Measured on one real account: the slice is 524 bytes against 506, which is nothing against
the 4032-byte ceiling. Whether restoring a device token spares a re-verification is not
measured and is not claimed anywhere.
pitboard reads what a session here would actually authenticate with, from files rather
than from three environment variables. Claude Code resolves authentication from layered
settings, and a managed policy or a line in a person's own settings.json can set an env block, an apiKeyHelper, or a third-party provider switch; under any of those a
session ignores the login pitboard moves and every switch is a no-op that reported
success. Worse, the app has no shell environment at all, so the one surface that could not
warn was the one most likely to be used on a machine that needed the warning. Managed
settings and the person's own are read; a project's are deliberately not, because an
answer true only in the directory pitboard happened to run in is worse than none. pitboard doctor says which it is, and a custom OAuth endpoint set in a file now refuses
a switch the way one set in the environment always did.
Every fact pitboard stands on about Claude Code is now a list rather than a comment:
what it is, where in Claude Code it was read, which build it was last verified against,
and what in this crate stops being true if it moves. pitboard doctor says which Claude
Code is installed here, read off disk and never by running it, beside the build those
facts came from. It states rather than warns: Claude Code ships several times a week, so
a mismatch is the ordinary state of the world within days and warning about it on every
machine would be noise. An assumption that has actually stopped holding is a different
thing and wants a probe, not a version number.
An interrupted switch is recovered without a network. Deciding what it did meant asking
Anthropic who owns the live login, so a switch interrupted on a plane, or while Anthropic
was having a bad morning, stopped every command that changes anything until it could be
asked. The record now carries a fingerprint of the refresh token on each side, which is
the same eight bytes of SHA-256 a park already records, so the common case is a
comparison. It narrows the dependency rather than removing it: Claude Code refreshing the
token inside those few seconds leaves a login matching neither side, and that is still a
question for Anthropic, and still changes nothing when Anthropic cannot be reached.
Fixed
The menu bar app finds a tool installed through a Node version manager or an npm prefix,
and can start its sign-in. An app opened from Finder has none of a shell's PATH, so it
looked for claude and codex only where their own installers put them, and did not
offer one installed through nvm, volta, fnm, asdf, mise, pnpm, bun or a custom npm
prefix. One it did find could not start if npm had installed it: an npm install is a
script run by node, which was not on the app's PATH either. The app now asks the
person's login shell for its PATH, off the main thread, which runs its startup files as
a terminal does, and looks there after PITBOARD_CLAUDE or PITBOARD_CODEX and before
the installers' places, passing over a folder macOS asks permission for, such as Documents
or iCloud Drive. A shell that does not answer within five seconds is stopped with
everything it started, and the app looks where it did before; it asks once more a minute
or more later, when the form for another account opens or a sign-in starts, because
startup files are slowest while the machine is still logging in.
Every sign-in, from the app and the command line, now starts the program by the path it
was found at: the first file on the PATH that can be run, in a directory named from the
root, so what is found is what starts. A program found where that PATH does not reach,
as the app finds one where its installer put it, has its own directory put first, which
is where npm puts the node that installed it; one found on the PATH runs with it as it
is, so it finds the node the terminal would.
Signing in again to the account in use puts its new login in use. pitboard enroll <label> --sign-in for the account signed in now parked the new login and left the tool
on the old one, which is the login somebody signs in again to replace, and the next
switch away parked the old login over the new one and deleted it. For Codex, whose
outgoing account is read from its ID token without asking OpenAI, the login kept could be
one whose refresh chain was already revoked. The new login now goes in place of the old
under the same lock, checks and read-back as a switch, nothing is parked, and --json
says in_use; a label this enrols for the first time says it was enrolled. Where pitboard
cannot tell that the login in use is that account's, the new one is parked as before,
because writing over a login nobody can name could lose it, and when that is the account
pitboard last saw in use it says so and why, with the new code sign_in_parked_not_in_use. A running codex keeps the old login and can write it back
when it refreshes, so the sessions are counted and warned about, with the new code sessions_keep_old_login. A new login that could not be written was not kept, and says to
sign in again, with the new code sign_in_not_kept. A new login pitboard could not
confirm is in use, where the old one may be gone too, is parked rather than lost, with the
new code sign_in_not_installed, and still says what writing and parking it warned about.
Where it was in use after all, that parked copy holds the refresh token the tool is using:
no renewal spends it, and the next change or renewal that can read the tool's login drops
it. The menu bar app says the new login is the one in use, keeps what that tool's last
switch said beside it, and shows what a sign-in of any account warned about.
A renewal killed after writing the fresh login and before recording it could lose the
login: the next change deleted the fresh copy as unrecorded and kept the old one, whose
refresh token the service had already spent. A copy pitboard wrote down itself now
replaces an older copy of a different chain. A renewal whose record cannot be saved keeps
what it wrote for the next run instead of deleting it.
An interrupted switch is recovered only where its tool's login was when it started. Read
from another CODEX_HOME or CLAUDE_CONFIG_DIR, recovery compared the switch with a
login that had nothing to do with it; it now stops with recovery_elsewhere.
A change refused over the name it was given still settles an interrupted switch, like
every other change. pitboard use nosuchlabel after an interrupted switch refused the
name and left the switch for a later run without a word about it; it now finishes or
undoes the switch, records that in pitboard log, and reports it beside the refusal, as
a warning in --json, and so does a name enroll --sign-in refuses before it opens a
browser, from the command line or the app. With nothing interrupted, a mistyped name
still takes no lock and changes nothing.
Labels written by 0.1.x that contain a slash can be switched to, forgotten, renamed and
signed in to again; they read as a tool prefix and were refused.
The floor between two questions about one account was the minute pitboard uses for a
window it cannot time, not the three minutes it promises. It worked a window's length out
from its kind and knew five_hour and seven_day, and Anthropic has been answering session, weekly_all and weekly_scoped. A window now carries its length where it is
known: stated outright by OpenAI, derived from the kind for Anthropic. In --json a
window gains length_seconds.
pitboard doctor --json is now what the bug template says it is. The template asks people
to paste it and promises labels, codes, paths and times with no tokens, no email addresses
and no account identifiers; it printed the signed-in email address and organisation uuid,
the login name, a value derived from the refresh token, and home paths carrying the
username, and the contract snapshot could not catch it because it redacted the whole
checks array. Identifiers are now salted digests, so two mentions of one account line up
inside a report and two reports do not line up with each other, and paths under the home
are shortened. pitboard doctor without --json is untouched: a person reading their own
machine should see their own account. The contract test pins the promise rather than a
snapshot of one machine.
Changing Claude Code's config no longer loses what Claude Code wrote meanwhile. It was
read, edited in memory, and a whole new file renamed over it, so anything written in
between was silently gone from the file that holds a person's project history and MCP
configuration. Measured on 22 September 2026 against a running session: it is rewritten
about every forty seconds and every rewrite changes something. Claude Code takes no lock
on it, so pitboard checks that the bytes it parsed are still the bytes on disk and starts
again from the new ones when they are not, and after four tries writes nothing rather than
writing over what Claude Code just put there. What pitboard removed from the file is
written into pitboard log rather than left to be inferred from a backup.
A renewed login's expiry is measured from Anthropic's clock rather than from this
machine's. The lifetimes a renewal answers with are relative, so what they are added to
decides when the login expires: on a machine running ahead, a freshly renewed park read
as already lapsed and every pitboard renewed it again, rotating the refresh chain on a
loop; on one running behind, a lapsed park looked restorable and a switch installed a
login that could not work. Measured first, which is why there is no skew estimate here:
this machine sat within 0.75 seconds of Anthropic across eight requests, and the Date
header has a granularity of one second, so the whole spread was noise. Anchoring is the
correction; estimating would have been machinery with nothing to correct.
"Nothing is signed in" is no longer said when something is. If Claude Code's config names
somebody as signed in and no store pitboard reads holds that login, pitboard is looking in
the wrong place, and writing a login there would put it where nobody reads it. That is now
its own refusal and its own failing check, with the code live_credential_elsewhere,
rather than advice to sign in again. It is the failure that would follow Claude Code
moving where it keeps a login, and the one the Linux platform has been most at risk of.
A switch asks about the login going in, not only about the one coming out. It used to ask
Anthropic twice about the login it was throwing away and never once about the login it was
installing, so an account whose refresh chain had been revoked or signed out elsewhere
installed cleanly, read back cleanly and reported a switch; the person found out the next
time they ran claude, by which point the login they had left was parked and the one they
had arrived at did not work. A park Anthropic refuses is dropped and the switch does not
happen; one filed under the wrong account is refused by name; one whose access token has
lapsed is renewed and the renewed login is what gets installed. Costs one round trip,
asked before Claude Code's write lock is taken, so it does not hold up its writes.
A switch no longer reports success it did not have. Claude Code's /logout deletes the
credential with no write lock held once it has given up waiting, which is the one write
pitboard cannot exclude; landing just after the install, it left the incoming account
signed out while pitboard use printed "Switched to work" and exited 0. The slot is now
read back before the incoming copy is discarded, so a switch that did not hold leaves
both logins parked and says what happened, instead of leaving neither and saying nothing.
The new code is switch_did_not_hold.
The credential write lock now notices when it stops being pitboard's. A machine that
sleeps mid-switch lets the lock age past its staleness window, and Claude Code reclaims
it and writes underneath a switch that believes it still holds it. The heartbeat compares
the directory's mtime against what it last stored and stops, marking the lock lost, and
releasing it then leaves the directory alone rather than taking away a lock that now
belongs to somebody else. Claude Code treats the same event as a warning and keeps
writing, so pitboard cannot expect the other side to stop. Measured first: APFS returns a
mtime 18 to 60 nanoseconds from the one it was given, so a check against the value asked
for would abandon every switch.
A locked keychain no longer reads as a lost login. On a machine whose keychain is locked
the write fails, the read-back that decides whether anything changed fails too, and that
second failure was taken to mean the slot had changed: pitboard attempted a rollback,
that failed as well, and the person was told their login could not be put back and they
should sign in again. Nothing had been written and it had never moved. The read-back now
has three answers rather than two, and not knowing is one of them: nothing further is
written, every copy is kept, and the record of intent stays so a later run with a store
that answers finishes or undoes the switch. The new code is switch_unverified.
Changed
pitboard's account list is at schema 4: every account says which tool it is for, and
which account is signed in is kept per tool. A schema 3 file is brought forward on its
first read with nothing moved and nothing in the keychain or the vault touched. An older
pitboard refuses the new file and says to upgrade whichever of the command line and the
app is behind, rather than calling it corrupt.
Messages name the tool they are about. An error that used to say "Claude Code",
"Anthropic" or claude whatever the account now names that account's tool, its service
and the command that signs in to it. Codes are unchanged. A few Claude Code messages
gained the tool's name where a second tool made them ambiguous: "nothing is signed in
right now" reads "nothing is signed in to Claude Code right now". A name a message tells
somebody to type is qualified, claude/work, where another tool has an account of the
same name and a bare one would be refused as ambiguous.
A Claude Code credential with no account in it, which is what /logout leaves beside
the machine's MCP tokens, is nobody signed in. use and enroll answered live_credential_shape_unexpected with exit 3 and now answer live_credential_absent
with exit 1, or live_credential_elsewhere where Claude Code's config still names
somebody, and doctor warns rather than fails.
In --json, use gains provider and an adoption object saying whether sessions
already running follow on their own within a number of seconds or need restarting; adoption_ceiling_seconds is null for a tool that needs restarting. Enrolments and
renewals gain provider. All additive.
In --json, every status account gains provider and qualified, its name with the
tool spelled out (codex/work), null for a login nothing has enrolled. Two stale codes
are new: login_unreadable, for a tool's login that is there and could not be read, and login_unusable, for one that was read and is no account pitboard can park or switch,
such as an API key. Where no record pins such a login on an account, it gets a row of
its own with no label, email or account id, and only for a tool with accounts enrolled. doctor gains environment.codex (home, present, backend, login_present, version) and a section about Codex whose codes all start codex_: codex_backend, codex_auth_file, codex_login, codex_version, codex_running, and for a Codex
account codex_parked_login and codex_dormant_account. What somebody chose is not a
fault: a keychain store fails only where it puts enrolled Codex accounts out of reach,
an API key login is a warning only where there are Codex accounts to switch to, and
either is otherwise stated without a warning. All additive: Claude Code's rows, checks
and codes are what they were.
CLAUDE_CODE_CUSTOM_OAUTH_URL refuses changes to Claude Code accounts, and no longer
stops a change to a Codex one.
pitboard-core is reorganised around a provider boundary, and much of what it exposed
moved or changed shape: errors that name a tool carry it, switch::settle and sign_in take the tool, renewals are keyed by account, and Claude Code's own document
rules live under provider::claude. A breaking change for anyone building on the crate,
declared as such; nothing changes for the command line's contract beyond the additions
above.
pitboard-core says what it supports. The interface other programs may build on is service::Pitboard, context::Context and what they return; the rest is reachable for
this repository's own front ends and may change in any release. The enums a caller reads
codes out of are now #[non_exhaustive], so adding a code is not a breaking change for a
consumer, which is what the command line's JSON contract has always promised. Writing
pitboard's index is no longer reachable from outside the crate: every change goes through switch, which records its intent first. Marking those enums and withdrawing state::save
are themselves breaking changes for anyone who built on 0.2.0, so this is the release that
makes them, while the crate is young enough for that to cost nothing. Nothing changes for
anyone using the command line or the app.
Claude Code's supervisor daemon is named as what it is, a second writer of the login that
runs on a schedule of its own. It takes the same write lock and re-reads the credential
inside it, so it cannot put an older account back over a switch. doctor reports it.
The storage v5 question is settled rather than open. The successor backend replaces the
fallback half of Claude Code's chain and only for a caller that hands a backend in, so an
ordinary claude still reads the keychain first. doctor now warns only for the
combination that can mislead, the flag on and the login in the fallback.
Linux is decided rather than assumed. Claude Code has exactly two guarded credential
stores, the macOS keychain and the Windows credential manager behind a feature flag;
searched whole, the shipping build carries no libsecret, no org.freedesktop.secrets, no
gnome-keyring and no Secret Service. So on Linux its login is a plaintext file at mode
0600 and pitboard's parked copies are files beside it, which is what pitboard already
did. There is no keyring backend to add. The facts pitboard stands on can now rest on an
absence: each one may name literals whose arrival would disprove it, and the conformance
run fails when one turns up, because a fact resting on something not existing is wrong
the moment it does and nothing disappearing would ever say so.
README and SECURITY.md say what a downloader can actually check: the archives, the
source tarball Homebrew builds from, SHA256SUMS, a bill of materials beside each
artefact and appcast.xml are all attested, and gh attestation verify checks any of
them against the workflow and commit that produced it. They named one archive.
A failed read in the app shows that failure's own warnings rather than the last
successful read's. A fresh network error used to sit above warnings about things that
may have been fixed since.
Text in the app grows with the person's own. Every column was a width fixed at the
default size, so anyone with larger text got a limit name running into its bar and a
reset time clipped off the right. The checks were read out without saying whether they
passed, an account row was six separate stops for a screen reader rather than one, and
the status item announced itself as "speedometer".
A login too large for security's standard input is now written the only other way security offers, as an argument, which is what Claude Code does for the same login on
every token refresh. The switch says so, and doctor shows the size. PITBOARD_NO_ARGV=1
refuses instead. Measured first: writing the item in process would have made every later
read by security take about a second instead of 0.01, for good.
Internal
The release publishes the Homebrew tap itself, from the checksums it has already computed
for SHA256SUMS, and then installs the formula and the cask from the public tap on a
clean runner and fails if what it serves is not the version just released. The tap used
to be written by a workflow of its own inside the tap repository, waking every six hours
and taking the checksum of whatever it downloaded, so brew install could be a version
behind for most of a day with the release green and nothing anywhere saying so. The
formula now builds from a source tarball the release publishes and attests, rather than
from the archive GitHub generates for a tag, whose bytes GitHub has changed before now. packaging/update-tap.sh is gone with the second download it did.
There is no CARGO_REGISTRY_TOKEN any more. crates.io issues the publish job a token
from its GitHub identity and revokes it when the job ends, so there is no standing
credential to leak, and the exchange runs on a pre-release tag too, where a registration
that does not match is found out before a release reaches the one step nobody can undo.
That step now waits in a GitHub environment with required reviewers.
Every artefact carries a CycloneDX bill of materials, generated from the lockfile per
target, published with the release and attested like the artefact it describes. So are SHA256SUMS and appcast.xml, which are made in the same job and published in the same
release as the files they describe: on their own they said a download had arrived whole
and nothing about who put it there.
The release checks the published feed against the public key in the published app bundle,
which is the key an installed copy checks it against. It used to check it with the
private key that signed it, in the job that signed it, so a wrong key verified against
itself. Nothing in that job reads a secret now. A release whose update key differs from
the last one's is refused unless a repository variable says that is what it means to do.
CONTRIBUTING.md has a procedure for replacing the Sparkle update key or the Developer
ID certificate, and .github/workflows/rotation.yml runs the awkward half of it every
month against keys it makes on the runner, a feed on 127.0.0.1 and bundles under an invalid. identifier. It names no repository secret, so it cannot reach the real key,
and CI checks that it still names none. Running it found the trap: generate_appcast
will not sign a bundle carrying a key other than the one it is handed, and says so by
writing the feed with no signature and exiting 0.