feat: authenticate Claude Code with a long-lived token #2

Merged
dusan merged 1 commit from feat/claude-long-lived-token into main 2026-08-12 21:49:09 +00:00
Owner

Reading Claude Code's .credentials.json works, but inherits that login's lifetime: the stored access token expires within hours, only the CLI refreshes it, and this extension deliberately will not — a second writer could rotate the refresh token out from under the harness. A user who has not opened Claude Code since yesterday therefore gets "Login expired" on a subscription that is perfectly fine.

claude setup-token mints an sk-ant-oat01- OAuth token for the same account and audience as the one being scraped, so it authenticates the existing /api/oauth/usage request unchanged. A profile can now opt into using one, kept in the login keyring rather than in dconf, where a long-lived credential would sit in plaintext in every backup and in any dconf dump pasted into a bug report.

Design

  • Per-profile opt-in. A new authMode rides inside the existing provider-profiles JSON, so there is no gschema change and an older version's value parses unchanged. The default stays harness, so existing installs come up exactly as before.
  • Token mode never falls back to the file. A silent fallback would hide a revoked token behind a stale read and leave the user unable to tell which credential produced the numbers on screen.
  • detect() returns true unconditionally in token mode. Half the point of the feature: a token-backed profile needs no CLI installed at all. It also stops "hide missing providers" from making the profile vanish between choosing token mode and pasting a token.
  • No new UsageRemedy values. not-signed-in and reauthenticate already mean the right things; the dropdown has the profile in hand, so it picks the wording ("No token saved" / "Token rejected") and the claude setup-token command. A keyring that cannot be read stays an error rather than a remedy — no amount of pasting fixes it.
  • A tokenRevision counter is what tells the poller a snapshot was taken with a credential that has since been replaced, since the token itself is never in settings.
  • libsecret is imported dynamically, so an install without the typelib loses this one feature instead of the whole extension.

The preferences verify a pasted token against the live endpoint before storing it, and only an outright rejection blocks the save — refusing a good token because the user was offline would be the worse failure. The value is write-only in the interface: cleared from the entry once stored, never read back into a widget, and deleted on forget, on switching away from token mode, and on removing the profile.

The extension never runs claude setup-token itself — it is an interactive browser OAuth flow, and spawning a terminal from a shell extension is a review liability. The command is offered as a copy button, the same pattern the "Login expired" page already uses.

Verified against the live endpoint

Path Result
Token from CLAUDE_CODE_OAUTH_TOKEN 3 usage windows returned
Token from the keyring 3 usage windows returned
No token stored remedy: not-signed-in → "No token saved"
Invalid token remedy: reauthenticate → "Token rejected"
Harness mode (unchanged path) 3 usage windows returned

libsecret store → lookup → clear → confirm-gone was exercised directly, and every Adw/Gtk API the preferences use was runtime-checked (PasswordEntryRow with apply button, set_subtitle_lines, ComboRow.set_selected, clipboard.set(string) round-tripping real text). just check clean, 81/81 tests pass, just pack produces a zip containing the new modules.

Not verified

The preferences window itself never rendered. It needs just install + just prefs, and the standalone route is closed because the prefs resource requires the Shew typelib. Widget APIs are individually runtime-checked; the assembled page is not.

Note

--profile was renamed to --profile-id in the probe tool: gjs claims --profile for its own profiler, swallows the flag, and drops a capture file in the cwd.

Reading Claude Code's `.credentials.json` works, but inherits that login's lifetime: the stored access token expires within hours, only the CLI refreshes it, and this extension deliberately will not — a second writer could rotate the refresh token out from under the harness. A user who has not opened Claude Code since yesterday therefore gets "Login expired" on a subscription that is perfectly fine. `claude setup-token` mints an `sk-ant-oat01-` OAuth token for the same account and audience as the one being scraped, so it authenticates the existing `/api/oauth/usage` request unchanged. A profile can now opt into using one, kept in the login keyring rather than in dconf, where a long-lived credential would sit in plaintext in every backup and in any `dconf dump` pasted into a bug report. ## Design - **Per-profile opt-in.** A new `authMode` rides inside the existing `provider-profiles` JSON, so there is no gschema change and an older version's value parses unchanged. The default stays `harness`, so existing installs come up exactly as before. - **Token mode never falls back to the file.** A silent fallback would hide a revoked token behind a stale read and leave the user unable to tell which credential produced the numbers on screen. - **`detect()` returns true unconditionally in token mode.** Half the point of the feature: a token-backed profile needs no CLI installed at all. It also stops "hide missing providers" from making the profile vanish between choosing token mode and pasting a token. - **No new `UsageRemedy` values.** `not-signed-in` and `reauthenticate` already mean the right things; the dropdown has the profile in hand, so it picks the wording ("No token saved" / "Token rejected") and the `claude setup-token` command. A keyring that cannot be read stays an *error* rather than a remedy — no amount of pasting fixes it. - **A `tokenRevision` counter** is what tells the poller a snapshot was taken with a credential that has since been replaced, since the token itself is never in settings. - **libsecret is imported dynamically**, so an install without the typelib loses this one feature instead of the whole extension. The preferences verify a pasted token against the live endpoint before storing it, and only an outright rejection blocks the save — refusing a good token because the user was offline would be the worse failure. The value is write-only in the interface: cleared from the entry once stored, never read back into a widget, and deleted on forget, on switching away from token mode, and on removing the profile. The extension never runs `claude setup-token` itself — it is an interactive browser OAuth flow, and spawning a terminal from a shell extension is a review liability. The command is offered as a copy button, the same pattern the "Login expired" page already uses. ## Verified against the live endpoint | Path | Result | |---|---| | Token from `CLAUDE_CODE_OAUTH_TOKEN` | 3 usage windows returned | | Token from the keyring | 3 usage windows returned | | No token stored | `remedy: not-signed-in` → "No token saved" | | Invalid token | `remedy: reauthenticate` → "Token rejected" | | Harness mode (unchanged path) | 3 usage windows returned | libsecret store → lookup → clear → confirm-gone was exercised directly, and every Adw/Gtk API the preferences use was runtime-checked (`PasswordEntryRow` with apply button, `set_subtitle_lines`, `ComboRow.set_selected`, `clipboard.set(string)` round-tripping real text). `just check` clean, 81/81 tests pass, `just pack` produces a zip containing the new modules. ## Not verified The preferences window itself never rendered. It needs `just install` + `just prefs`, and the standalone route is closed because the prefs resource requires the `Shew` typelib. Widget APIs are individually runtime-checked; the assembled page is not. ## Note `--profile` was renamed to `--profile-id` in the probe tool: gjs claims `--profile` for its own profiler, swallows the flag, and drops a capture file in the cwd.
Reading Claude Code's own .credentials.json works, but inherits that
login's lifetime: the stored access token expires within hours, only the
CLI refreshes it, and this extension deliberately will not — a second
writer could rotate the refresh token out from under the harness. A user
who has not opened Claude Code since yesterday therefore gets "Login
expired" on a subscription that is perfectly fine.

`claude setup-token` mints an sk-ant-oat01- OAuth token for the same
account and audience as the one being scraped, so it authenticates the
existing /api/oauth/usage request unchanged. A profile can now opt into
using one, kept in the login keyring rather than in dconf, where a
long-lived credential would sit in plaintext in every backup and in any
dconf dump pasted into a bug report.

Token mode never falls back to reading the file: a silent fallback would
hide a revoked token behind a stale read and leave the user unable to tell
which credential produced the numbers on screen. It also reports the
profile as available unconditionally, which is half the point — a
token-backed profile needs no CLI installed at all.

No new remedy values: 'not-signed-in' and 'reauthenticate' already mean
the right things, and the dropdown has the profile in hand, so it picks
the wording ("No token saved" / "Token rejected") and the command. A
keyring that cannot be read stays an error rather than a remedy, since no
amount of pasting fixes it.

libsecret is imported dynamically, so an install without the typelib loses
this one feature instead of the whole extension.

The preferences verify a pasted token against the live endpoint before
storing it, and only an outright rejection blocks the save — refusing a
good token because the user was offline would be the worse failure. The
value is write-only in the interface: cleared from the entry once stored,
never read back into a widget, and deleted on forget, on switching away
from token mode, and on removing the profile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dusan merged commit 113b4bc4ed into main 2026-08-12 21:49:09 +00:00
dusan deleted branch feat/claude-long-lived-token 2026-08-12 21:49:09 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
dusan/gnome-shell-extension-clankers!2
No description provided.