Skip to content

fix(limits): restore Claude Code usage tracking - #25

Merged
howdeploy merged 3 commits into
howdeploy:mainfrom
StefanSH:fix/claude-code-limit-tracking
Sep 5, 2026
Merged

fix(limits): restore Claude Code usage tracking#25
howdeploy merged 3 commits into
howdeploy:mainfrom
StefanSH:fix/claude-code-limit-tracking

Conversation

@StefanSH

@StefanSH StefanSH commented Aug 30, 2026

Copy link
Copy Markdown

Summary

Restores Claude Code usage-limit tracking while keeping credential selection aligned with the Claude Code process launched by CanvasTTY. The HOME limit row displays both used percentage and reset countdown.

Changes

  • On macOS, read Claude credentials from the Keychain service selected by the effective secure-storage root.
  • Honor CLAUDE_SECURESTORAGE_CONFIG_DIR, including its explicit empty-string default-store override; otherwise use the effective CLAUDE_CONFIG_DIR or default ~/.claude root.
  • Use Claude Code-credentials for the default root and Claude Code-credentials-${sha256(NFC(root)).slice(0, 8)} for non-default roots.
  • Query the Keychain item for the current OS account.
  • Preserve error semantics: missing item is not-authenticated, killed lookup is timeout, and malformed JSON or other Keychain failures are protocol-error.
  • Keep .credentials.json behavior unchanged on Linux and Windows.
  • Keep the HOME contract at one selected row per provider; the separate Fable-row change is not part of this PR.

Coverage

  • Default macOS profile.
  • Custom CLAUDE_CONFIG_DIR.
  • Custom and explicitly empty CLAUDE_SECURESTORAGE_CONFIG_DIR.
  • Current-account Keychain query.
  • Missing-item, timeout, malformed JSON, and other Keychain failures.
  • Linux and Windows file-based credential lookup.

Verification

  • All 543 tests pass.
  • TypeScript typecheck passes.
  • Production build passes.
  • Repository secret audit passes.
  • git diff --check passes.

@howdeploy howdeploy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for restoring Claude usage tracking for the default macOS profile. Before merge, please align the credential lookup with the credential store that the launched Claude Code process actually uses, so the fix stays correct across macOS profiles and does not regress Linux or Windows.

  1. The Keychain service cannot always be hard-coded to Claude Code-credentials. Current Claude Code uses CLAUDE_SECURESTORAGE_CONFIG_DIR when it is defined, otherwise the effective CLAUDE_CONFIG_DIR / default config root. On macOS, a non-default root selects a service with a hashed suffix (Claude Code-credentials-${sha256(NFC(root)).slice(0, 8)}), and Claude also queries the current account (security ... -a <username>). CanvasTTY preserves these environment variables for the launched CLI, but this PR always reads the default service. With multiple profiles it can therefore show limits for the wrong account or report a false signed-out state. Please derive the same service/account from the effective credential root, or fail closed instead of falling back to another profile.

Please add coverage for:

  • the default macOS profile;
  • custom CLAUDE_CONFIG_DIR;
  • CLAUDE_SECURESTORAGE_CONFIG_DIR, including the explicit empty-string default-store override;
  • unchanged file-based behavior on Linux and Windows.
  1. Please preserve Keychain timeout/error semantics. At the moment every non-parser execFile failure, including a killed timeout, becomes not-authenticated. A timeout should remain timeout; malformed stored JSON should be a protocol error; only a missing credential item should mean not authenticated.

  2. Commit e2aa440 adds a separate Fable row, but this feature is not documented in the PR body and changes the current HOME contract from one selected provider row to potentially two Claude rows. Please either split that feature into a separate PR, or update the UI contract and add coverage for the default six-provider plus Fable layout after product confirmation.

The current head passes all 537 tests, typecheck, build, secret audit, and git diff --check. I could not independently run the real macOS Keychain path from the Linux review host.

@StefanSH
StefanSH force-pushed the fix/claude-code-limit-tracking branch from e2aa440 to 6113048 Compare August 30, 2026 17:42
@StefanSH

Copy link
Copy Markdown
Author

Thanks for the detailed review — addressed all three points.

  1. macOS credential selection now matches Claude Code: CLAUDE_SECURESTORAGE_CONFIG_DIR takes precedence (with an explicit empty string selecting the default store), otherwise the effective CLAUDE_CONFIG_DIR / default root is used. Non-default roots use the NFC-normalized SHA-256 service suffix, and the Keychain query includes the current account via -a.
  2. Keychain failures now retain their meaning: exit code 44 is not-authenticated, a killed/ETIMEDOUT lookup is timeout, malformed JSON is protocol-error, and other execution failures also fail closed as protocol-error.
  3. I removed e2aa440 from this PR's branch history, so the separate Fable row is no longer part of the PR and HOME remains one selected row per provider.

Coverage was added first in fb1bc53, with the implementation in 6113048, for the default macOS profile, custom config roots, secure-storage overrides including empty string, account selection, error semantics, and unchanged Linux/Windows file lookup.

Validation after the changes: 543/543 tests, typecheck, production build, secret audit, and git diff --check all pass. I also launched the Electron dev app successfully on macOS; it started cleanly and remained running without startup errors.

@howdeploy

Copy link
Copy Markdown
Owner

Thanks for the update. I re-reviewed head 6113048, and the earlier issues are improved, but credential selection still diverges from the Claude Code 2.1.234 process launched by CanvasTTY in three cases:

  1. The file-backed path ignores CLAUDE_SECURESTORAGE_CONFIG_DIR. readClaudeCredentials returns configRoot/.credentials.json before resolving the secure-storage override on every non-macOS platform. Current Claude Code resolves .credentials.json under the effective secure-storage root: CLAUDE_SECURESTORAGE_CONFIG_DIR when it is defined, with an empty string selecting the default ~/.claude, otherwise the effective config root. With CLAUDE_CONFIG_DIR=A and CLAUDE_SECURESTORAGE_CONFIG_DIR=B, Claude reads B/.credentials.json while this PR reads A/.credentials.json, so HOME can show another profile or a false signed-out state. The new Linux/Windows test currently locks in the opposite expectation by calling the override ignored. As a black-box check, credentials placed only under CLAUDE_CONFIG_DIR plus an empty directory selected through CLAUDE_SECURESTORAGE_CONFIG_DIR produced loggedIn: false from claude auth status.

  2. The macOS default-service decision must use override presence, not path equality. Claude omits the hash only when neither config variable selects a custom store, or when CLAUDE_SECURESTORAGE_CONFIG_DIR is explicitly empty. Any explicit non-empty root is hashed even if its text equals ~/.claude. For example, with CLAUDE_CONFIG_DIR=/test-home/.claude, Claude selects Claude Code-credentials-e3dba04f, while this PR selects Claude Code-credentials because secureStorageRoot === defaultConfigRoot.

  3. The Keychain account still differs from Claude. Claude selects process.env.USER || userInfo().username, validates it, and falls back to claude-code-user; this PR always uses userInfo().username. CanvasTTY passes USER to the launched CLI, so an inherited USER=profile-user makes Claude query profile-user while the limits adapter queries the OS username. The macOS tests all inject account, so the production selection path is not covered.

Please derive the same effective secure-storage root for both file and Keychain backends, preserve Claude’s presence-based suffix semantics, and mirror its account selection with coverage for these cases.

Independent validation on this head: all 543 tests pass, typecheck passes, production build passes, and the secret audit passes. I could not execute the native macOS Keychain path from the Linux review host.

@howdeploy

Copy link
Copy Markdown
Owner

Thanks for the update. I rechecked head 6113048. The general direction is good, the Fable row is gone, and the Keychain error mapping is much better. The remaining blocker is that CanvasTTY still does not resolve Claude's credential store exactly like the Claude Code process it launches.

Please use the following as the acceptance contract for the next revision.

1. Resolve the effective credential store once, before branching by platform

Please introduce one small resolver that returns both:

  • the effective credential root; and
  • whether macOS should use the unsuffixed default Keychain service.

Both the file-backed path and the macOS Keychain path must consume that same result. The current early non-macOS return at LimitsService.ts:329-330 happens before CLAUDE_SECURESTORAGE_CONFIG_DIR is resolved, which is why Linux and Windows diverge from Claude.

The required precedence is:

  1. If CLAUDE_SECURESTORAGE_CONFIG_DIR is present and non-empty, use it as the credential root. It is a custom store, even when its string value equals the default ~/.claude path.
  2. If CLAUDE_SECURESTORAGE_CONFIG_DIR is present but empty, explicitly select the default ~/.claude credential root and the unsuffixed default macOS service.
  3. Otherwise, use the effective CLAUDE_CONFIG_DIR / injected options.configRoot when it is non-empty. An explicitly selected non-empty config root is a custom store, even when its string value equals ~/.claude.
  4. If neither variable selects a non-empty root, use the default ~/.claude root and the unsuffixed default macOS service.

The important part is that “default service” is selection metadata, not resolvedRoot === defaultConfigRoot. Path equality loses the distinction between an implicit default and an explicit non-empty custom selection.

Expected behavior:

Environment File-backed credential path macOS Keychain service
no overrides ~/.claude/.credentials.json Claude Code-credentials
CLAUDE_CONFIG_DIR=A A/.credentials.json Claude Code-credentials-<hash(A)>
CLAUDE_CONFIG_DIR=~/.claude explicitly ~/.claude/.credentials.json Claude Code-credentials-<hash(~/.claude)>
CLAUDE_CONFIG_DIR=A, CLAUDE_SECURESTORAGE_CONFIG_DIR=B B/.credentials.json Claude Code-credentials-<hash(B)>
CLAUDE_CONFIG_DIR=A, CLAUDE_SECURESTORAGE_CONFIG_DIR="" ~/.claude/.credentials.json Claude Code-credentials
CLAUDE_SECURESTORAGE_CONFIG_DIR=~/.claude explicitly ~/.claude/.credentials.json Claude Code-credentials-<hash(~/.claude)>

Keep the existing NFC normalization before SHA-256 hashing.

2. Fix Linux and Windows file lookup

After resolving the store, non-macOS platforms must read:

<effective credential root>/.credentials.json

Please replace the current Linux/Windows test named keep using config-root credential files. Its CLAUDE_SECURESTORAGE_CONFIG_DIR: "/ignored/on/non-macos" expectation is the opposite of Claude Code 2.1.234 behavior.

The replacement test should create two different roots with two different OAuth tokens, set CLAUDE_CONFIG_DIR=A and CLAUDE_SECURESTORAGE_CONFIG_DIR=B, and assert that the request receives the token from B. Please also cover the explicit empty secure-storage value selecting the default root.

3. Match macOS service selection by presence semantics

Please do not decide the suffix with:

secureStorageRoot === defaultConfigRoot

Instead, use the resolver's explicit usesDefaultService/equivalent flag. A non-empty override must be hashed even if its normalized path text equals the default path. This needs tests for both:

  • explicit CLAUDE_CONFIG_DIR=<default root>; and
  • explicit CLAUDE_SECURESTORAGE_CONFIG_DIR=<default root>.

Both must query the hashed service. Only the implicit default or an explicitly empty secure-storage override may query Claude Code-credentials without a suffix.

4. Match Claude's Keychain account selection

The production path must mirror Claude's account selection:

process.env.USER || userInfo().username

with the same validation/fallback behavior to claude-code-user. options.account can remain as the deterministic test override, but tests that always inject it do not exercise production behavior.

Please add coverage for at least:

  • USER=profile-user taking precedence over the OS username;
  • absent/empty USER falling back to userInfo().username; and
  • an unavailable/invalid resolved username falling back to claude-code-user.

This matters because CanvasTTY passes USER into the launched Claude process. The limits adapter and Claude must query the same Keychain item.

What can remain unchanged

  • The OAuth usage request and headers.
  • The current Keychain error semantics (44 -> not-authenticated, timeout -> timeout, malformed JSON/other failures -> protocol-error).
  • The HOME percentage/reset UI changes.
  • The removal of the unrelated Fable row.

Ready-to-merge criteria

  • One shared credential-store resolver drives both file and Keychain backends.
  • The scenario table above is covered by tests.
  • The real production account-selection path is covered without injecting options.account in every case.
  • Existing behavior for error mapping remains covered.
  • npm test, npm run typecheck, npm run build, npm run audit:secrets, and git diff --check pass.

Once those cases are fixed, this PR should be in good shape for another review.

@howdeploy

Copy link
Copy Markdown
Owner

Thanks for pushing this forward — the direction is right (Keychain-backed credentials on macOS + percent alongside the reset countdown), and most of the implementation and test coverage is already in good shape. Before this can merge, the remaining three gaps from the last review still need to be closed:

  1. Honor CLAUDE_SECURESTORAGE_CONFIG_DIR on Linux/Windows too. readClaudeCredentials() currently returns early for non-darwin platforms before resolving the secure-storage root, so with CLAUDE_CONFIG_DIR=A + CLAUDE_SECURESTORAGE_CONFIG_DIR=B we read A/.credentials.json while real Claude Code reads B/.credentials.json — a wrong profile or a false signed-out. Note that the new test keep using config-root credential files currently locks in this incorrect behavior; please fix the test together with the resolver.
  2. Default Keychain service selection must be presence-based, not path-equality-based. secureStorageRoot === defaultConfigRoot loses the difference between "nothing was set" and "CLAUDE_CONFIG_DIR explicitly set to the default path" — Claude Code hashes the -${sha256(NFC(root)).slice(0,8)} suffix in the latter case, and we must match that.
  3. Keychain account must match what Claude uses: process.env.USER || userInfo().username with the claude-code-user fallback. CanvasTTY passes USER through to the CLI, so today the adapter and the CLI can query different keychain items when USER differs. Please cover this production path without injecting options.account in the test.

Acceptance contract from the previous review still stands: one shared resolver producing the effective root + a "use default service" flag before platform branching; the 6-scenario env → path/service table in tests; the fixed linux/win32 test; account selection covered on the real path.

Also, since there are no CI checks on this branch: please confirm npm test, npm run typecheck, and npm run build pass locally on the head commit.

@howdeploy
howdeploy merged commit 7c6035d into howdeploy:main Sep 5, 2026
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.

2 participants