Two users a week apart have now reported the same red card text, from different root causes, and the text cannot tell them apart. Verified on main (9a15a14d).
Claude credentials could not be loaded. is what every ClaudeLoginResolution::Terminal produces. Only Absent and ExplicitLogout reach the setup-token fallback (agent_usage.rs:1885-1892), so a Terminal verdict also silently skips the tokenbar-claude-oauth-token route the in-app prompt and README tell people to use — which is what makes the ambiguity actionable rather than cosmetic.
The diagnostic strings already exist and are thrown away
Each lower layer produces a distinguishable message. All of them are discarded at the resolution boundary by Err(_).
| Produced at |
Existing string |
Discarded at |
agent_usage.rs:2755 |
read Claude Keychain credentials: {spawn error} |
:2553 |
agent_usage.rs:2760 |
Claude Keychain credentials could not be read. — any security exit other than 44, including a denied ACL |
:2553 |
agent_usage.rs:2764 |
...are not UTF-8 JSON. |
:2553 |
agent_usage.rs:2767 |
...are empty. |
:2553 |
agent_usage.rs:2535 |
Claude credentials file could not be read. |
:2558 |
agent_usage.rs:2683 / :2685 |
decode Claude OAuth credentials: {serde error} |
:2591 |
agent_usage.rs:2693 |
...have no access token. |
:2591 |
agent_usage.rs:2726 |
Claude credential location cannot be scoped safely. |
:2591 |
I confirmed locally that security find-generic-password exits 44 when an item is absent and 0 on success, so keychain_item_not_found (:2735) routes absence to Ok(None) correctly. Every other failure — a denied ACL, a locked keychain, errSecInteractionNotAllowed — lands in the same bucket as corrupt JSON.
The card is already able to show it
No new UI is needed. detailText returns snapshot.error verbatim when present (AgentLimitsCard.swift:438-443), and the card renders it in red with lineLimit(2) plus the full string in .help() (:338-343). A longer message degrades gracefully into the tooltip.
Proposal
- Give
Terminal a payload (agent_usage.rs:1022) and stop discarding the string at the four Err(_) arms.
- Include the
security exit code in the Keychain read-failure message (:2756-2761). One number separates a denied ACL from errSecInteractionNotAllowed from a spawn failure, without a round trip through an issue thread.
- Say in the text that the setup-token fallback was skipped deliberately, and what to do next.
CLAUDE_UNCONFIGURED_ERROR (:2517) is the existing model for a message that names an action.
Two adjacent defects to fix in the same pass:
:1902-1907 — when the login is Absent and the setup-token Keychain read fails, it emits the same CLAUDE_CREDENTIALS_LOAD_ERROR. Different failure, identical text.
:2528 — Err(_) => Terminal for the environment loader is unreachable. load_claude_credentials_from_environment (:2635-2676) contains no Err( at all.
Deliberately not proposed
No Terminal sub-case should be reclassified. Each was analysed separately; every one is damage or unprovable rather than absence, and the cross-account argument from #94 holds for all of them. {"claudeAiOauth":{}} is the most tempting and fails the same way the fall-through in #221 did: nobody can prove the shape is not a logout remnant, and there is no reported case of it. After #221 the classification split is correct — what remains is purely observability.
No Keychain-to-file fall-through on a read failure. Same defect #221's security review rejected as P2: the file may predate a logout the Keychain already recorded.
Do not migrate the credential read to the Security framework. Keychain ACL is evaluated against the process talking to securityd, which for a shell-out is /usr/bin/security — stably signed by Apple — not TokenBar. A user's one-time "Always Allow" therefore survives TokenBar updates. Because TokenBar ships ad-hoc signed (docs/knowledge/release.md, "Code signing and local secret storage"), its own designated requirement changes on every update, so calling the framework directly would make the requesting identity TokenBar itself and re-break access on each release. The deferred cleanup about security shell-outs is motivated by argv exposure, which applies to the write path; the read path passes -w for output, not a secret in argv.
Context
Raised by @coshsh1991 in #219 as the third suggestion — "at minimum, surface a distinct error so users know the setup-token path is being skipped" — which v1.13.3 did not address; only option (a) shipped. @qodeboy then hit the red text on v1.13.3 with a different cause, reporting no Keychain permission prompt, which is consistent with a read failure rather than the shape #221 fixed.
Two users a week apart have now reported the same red card text, from different root causes, and the text cannot tell them apart. Verified on
main(9a15a14d).Claude credentials could not be loaded.is what everyClaudeLoginResolution::Terminalproduces. OnlyAbsentandExplicitLogoutreach the setup-token fallback (agent_usage.rs:1885-1892), so aTerminalverdict also silently skips thetokenbar-claude-oauth-tokenroute the in-app prompt and README tell people to use — which is what makes the ambiguity actionable rather than cosmetic.The diagnostic strings already exist and are thrown away
Each lower layer produces a distinguishable message. All of them are discarded at the resolution boundary by
Err(_).agent_usage.rs:2755read Claude Keychain credentials: {spawn error}:2553agent_usage.rs:2760Claude Keychain credentials could not be read.— anysecurityexit other than 44, including a denied ACL:2553agent_usage.rs:2764...are not UTF-8 JSON.:2553agent_usage.rs:2767...are empty.:2553agent_usage.rs:2535Claude credentials file could not be read.:2558agent_usage.rs:2683/:2685decode Claude OAuth credentials: {serde error}:2591agent_usage.rs:2693...have no access token.:2591agent_usage.rs:2726Claude credential location cannot be scoped safely.:2591I confirmed locally that
security find-generic-passwordexits 44 when an item is absent and 0 on success, sokeychain_item_not_found(:2735) routes absence toOk(None)correctly. Every other failure — a denied ACL, a locked keychain,errSecInteractionNotAllowed— lands in the same bucket as corrupt JSON.The card is already able to show it
No new UI is needed.
detailTextreturnssnapshot.errorverbatim when present (AgentLimitsCard.swift:438-443), and the card renders it in red withlineLimit(2)plus the full string in.help()(:338-343). A longer message degrades gracefully into the tooltip.Proposal
Terminala payload (agent_usage.rs:1022) and stop discarding the string at the fourErr(_)arms.securityexit code in the Keychain read-failure message (:2756-2761). One number separates a denied ACL fromerrSecInteractionNotAllowedfrom a spawn failure, without a round trip through an issue thread.CLAUDE_UNCONFIGURED_ERROR(:2517) is the existing model for a message that names an action.Two adjacent defects to fix in the same pass:
:1902-1907— when the login isAbsentand the setup-token Keychain read fails, it emits the sameCLAUDE_CREDENTIALS_LOAD_ERROR. Different failure, identical text.:2528—Err(_) => Terminalfor the environment loader is unreachable.load_claude_credentials_from_environment(:2635-2676) contains noErr(at all.Deliberately not proposed
No
Terminalsub-case should be reclassified. Each was analysed separately; every one is damage or unprovable rather than absence, and the cross-account argument from #94 holds for all of them.{"claudeAiOauth":{}}is the most tempting and fails the same way the fall-through in #221 did: nobody can prove the shape is not a logout remnant, and there is no reported case of it. After #221 the classification split is correct — what remains is purely observability.No Keychain-to-file fall-through on a read failure. Same defect #221's security review rejected as P2: the file may predate a logout the Keychain already recorded.
Do not migrate the credential read to the Security framework. Keychain ACL is evaluated against the process talking to
securityd, which for a shell-out is/usr/bin/security— stably signed by Apple — not TokenBar. A user's one-time "Always Allow" therefore survives TokenBar updates. Because TokenBar ships ad-hoc signed (docs/knowledge/release.md, "Code signing and local secret storage"), its own designated requirement changes on every update, so calling the framework directly would make the requesting identity TokenBar itself and re-break access on each release. The deferred cleanup aboutsecurityshell-outs is motivated by argv exposure, which applies to the write path; the read path passes-wfor output, not a secret in argv.Context
Raised by @coshsh1991 in #219 as the third suggestion — "at minimum, surface a distinct error so users know the setup-token path is being skipped" — which v1.13.3 did not address; only option (a) shipped. @qodeboy then hit the red text on v1.13.3 with a different cause, reporting no Keychain permission prompt, which is consistent with a read failure rather than the shape #221 fixed.