Recover from an expired OAuth token instead of idling until someone runs Claude Code - #147
Conversation
…uns Claude Code The daemon is a pure free-ride: Claude Code owns the OAuth token and does all refreshing, because refreshing here would race its rotation and feed the OAuth endpoint's rate limit. The gap is what happens when the token expires while nobody is at the keyboard — every poll 401s, the device shows "No data", and it stays that way for as long as it takes for a human to run Claude Code. In one observed case that was 16 hours. This keeps the free-ride but closes the gap: when NO configured config dir has a usable token, the daemon runs one deliberately tiny headless Claude Code call (`claude -p ok`, cheapest model, 1 output token) so the CLI refreshes its own token as a side effect. The next poll finds it alive. The daemon still never mints or refreshes a token itself. Because this spends a small amount of the user's quota it is opt-in (`token_refresh = on`), fires only when everything is dead, is rate-limited to one attempt per 15 minutes, is bounded by a 120s timeout, and can never raise into the poll loop — a failure leaves the device idle exactly as before. A transient poll failure (network blip, 5xx) is explicitly NOT a trigger. The CLI is located via an optional `claude_cli` config override, then PATH, then the known install locations — service managers hand the daemon a minimal PATH that usually omits ~/.local/bin, which is where the installer puts it. All three daemons stay in lockstep. 21 new tests cover the default-off gate, the single spawn with the cheap model, rate limiting (including after a missing CLI), non-zero exit, spawn errors, the hung-subprocess kill path, CLI lookup precedence, and that the nudge fires only on all-dead and never on a transient failure or a healthy poll. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The problem is the usage window starts when you poll. People can be particular about when they start their usage windows so they might end for example right after lunch and allow them to have two full usage windows in one 9-5 work day. Could you perhaps modify this so that the daemon adds an option for starting a usage window? That would be a genuinely useful feature. Just polling to get fresh data doesn't really make sense in my opinion because: A) We can pretty much guarantee a usage of 0% for the current session, unless you're using your phone or another device B) We can cache the weekly limit so we can always display a 0% on the current session and keep the weekly limit displaying the last known value. Perhaps making the main loading bar boxes slightly opaque and adding your no data animation overlay on top of that could be a good indicator for stale data? The install.sh and install-mac.sh scripts could ask the user to set when they want usage windows started for them by the daemon perhaps? An option to keep a Claude usage window alive at all times could be useful for people though I admit so we could keep that option as well. But would be nice to give people the option to schedule a usage window like they schedule an alarm on their phones. What do you think? |
The gap
The daemon is a deliberate free-ride: Claude Code owns the OAuth token and does all refreshing, because refreshing here would race its rotation and feed the OAuth endpoint's rate limit (429). That's the right design — but it leaves one hole. When the token expires while nobody is at the keyboard, every poll 401s, the daemon correctly sends
{"ok":false}, and the device shows "No data" for as long as it takes a human to run Claude Code.I hit this on my own desk unit: 4,535 consecutive no-data beats, the longest stretch ~16 hours overnight. Nothing was broken — the refresh token was valid the whole time. The access token just needed someone to run the CLI.
What this does
Keeps the free-ride, closes the hole. When no configured config dir has a usable token, the daemon runs one deliberately tiny headless Claude Code call so the CLI refreshes its own token as a side effect:
The next poll finds a live token. The daemon still never mints or refreshes a token itself — it just nudges the process that owns it.
Guard rails
This spends a small but non-zero amount of the user's quota, so it is opt-in and hedged:
token_refresh = onin the config enables it.CLI lookup
claudeis resolved via an optionalclaude_cliconfig override →PATH→ known install locations. This matters: launchd/systemd hand the daemon a minimalPATHthat usually omits~/.local/bin, which is exactly where the official installer puts the binary. If it can't be found, the daemon logs the actionable message once rather than retrying.Scope
All three daemons stay in lockstep:
claude_usage_daemon.py,claude_usage_daemon_windows.py,claude-usage-daemon.sh.daemon/config.exampledocuments both new options.Verified
daemon/tests/test_token_selfheal.py), both Python daemons: default-off gate, single spawn with the cheap model, rate limiting (including after a missing CLI), non-zero exit, spawnOSError, the hung-subprocess kill path, CLI lookup precedence, and that the nudge fires only on all-dead — never on a transient failure or a healthy poll. Suite: 140 passed, 2 skipped. No real subprocesses are spawned in tests.bash -n).Independent of #143 — this is daemon-side only and touches no firmware.
🤖 Generated with Claude Code