Skip to content

fix(quota): read Claude quota via claude /usage instead of keychain#111

Merged
hiskudin merged 1 commit into
mainfrom
fix/claude-cli-quota-probe
Jul 1, 2026
Merged

fix(quota): read Claude quota via claude /usage instead of keychain#111
hiskudin merged 1 commit into
mainfrom
fix/claude-cli-quota-probe

Conversation

@hiskudin

Copy link
Copy Markdown
Collaborator

Summary

  • Replaces the direct `/api/oauth/usage` HTTP probe (which reads the OAuth token from the macOS Keychain) with a shell-out to `claude --print --output-format json "/usage"`. The CLI is a client-side intercept (`num_turns: 0`, `duration_api_ms: 0`, `total_cost_usd: 0`) that hits the same backend but runs inside Claude Code, which has its own keychain ACL grant — so StackNudge's process never touches the keychain and the periodic 8h password prompt is gone.
  • Keeps the legacy API probe as a fallback for missing-CLI / spawn-failure / parse-failure paths only. Rate-limited soft-fails hold the prior snapshot rather than re-triggering the keychain path.
  • Pins the probe's cwd to `/.stack-nudge` and unlinks the session rollout file (`/.claude/projects//.jsonl`) that every `claude --print` writes, so a 60s polling cadence doesn't litter `claude --resume` with ~1.4k empty sessions/day. UUID-shape guard on the unlink prevents any path-traversal escape.
  • Settings → Usage now stays silent when the CLI probe is the source. The plaintext-file warning and the keychain-fallback explainer still surface in their respective modes (they have actionable info; the happy path doesn't).

Anthropic ToS (post Jan/Feb 2026) explicitly bans third-party OAuth tokens hitting `/api/oauth/usage`, so the CLI path is also the only policy-clean route long-term. Codex and Antigravity are unaffected (local rollout files and loopback RPC respectively — no keychain involved).

Closes the source of the recurring "stack-nudge wants to access Claude Code-credentials" prompt that anthropics/claude-code#22144 documents and Anthropic closed as not-planned.

Test plan

  • `./build.sh` — clean compile under Developer ID signing.
  • Cold-start verification: killed all `stack-nudge` processes, swapped binary into `~/Applications/StackNudge.app`, relaunched fresh. Debug instrumentation (since removed) confirmed CLI probe fires first, finds `/opt/homebrew/bin/claude`, returns 1682 bytes parsed as `.ok` in ~4s. `/usr/bin/log show --predicate 'processImagePath CONTAINS "stack-nudge"' --last 90s` showed zero outbound traffic to `api.anthropic.com` — API probe correctly suppressed.
  • No password prompts during launch or first poll.
  • Session-file cleanup: after probe, `~/.claude/projects/-Users-hiskud--stack-nudge/` is empty (the directory itself disappears once Claude has nothing to keep there).
  • Encoding fix verified: Claude maps both `/` and `.` to `-` in the projects subdirectory name, so `/Users/me/.stack-nudge` becomes `-Users-me--stack-nudge` (note the double dash). `probeSessionsDir` matches.
  • CI: 13 parser unit tests in `ClaudeCliQuotaProbeTests.swift` covering full-output, 0%-no-resets, malformed-line skip, unknown-bucket skip, envelope unwrapping, session-id extraction, path-traversal guard, and reset-time parsing with timezone. (Local `swift test` is blocked because this machine has Command Line Tools but not full Xcode — XCTest isn't on the toolchain.)

🤖 Generated with Claude Code

Today's QuotaProbe reads the OAuth token from the macOS Keychain and
calls /api/oauth/usage itself. Claude Code rotates that keychain item
every ~8h, which wipes the trusted-app ACL grant — so the next read
prompts the user for their password. anthropics/claude-code#22144
tracks the upstream ask, closed not-planned.

Shell out to `claude --print --output-format json "/usage"` instead.
That's a client-side intercept (zero model cost: num_turns=0,
duration_api_ms=0) that hits the same backend, but runs inside Claude
Code — which has its own keychain ACL grant. Our process never touches
the keychain, so the periodic prompt is gone.

CLI-first / API-fallback: the legacy probe is kept and invoked only on
hard-fail (`claude` not on PATH, spawn/timeout failure, unparseable
envelope). The CLI's own soft-fail (rate-limited cold cache, no bucket
lines) is treated as "leave the prior snapshot alone" so we don't
re-trigger the keychain path on transient noise.

Anthropic ToS (post Jan/Feb 2026) explicitly bans third-party OAuth
tokens hitting /api/oauth/usage — the CLI path is also the only
policy-clean route long-term.

Cleanup: every `claude --print` writes a session rollout to
~/.claude/projects/<cwd>/<uuid>.jsonl. At 60s polling that'd be ~1.4k
files/day, polluting `claude --resume`. cwd is pinned to ~/.stack-nudge
and the just-written file is unlinked after each probe (UUID-shape
guard prevents path traversal).

Settings → Usage now stays silent when the CLI probe is the source.
The plaintext-file warning and the keychain-fallback explainer remain
in their respective modes.

Codex and Antigravity are unaffected — local rollout files and loopback
RPC respectively, no keychain involved.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@StuBehan StuBehan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@hiskudin hiskudin merged commit d82ff77 into main Jul 1, 2026
6 checks passed
@hiskudin hiskudin deleted the fix/claude-cli-quota-probe branch July 1, 2026 09:05
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