Files
PaperClipAI/packages/adapters/claude-local
scotttongandClaude Fable 5.1 dffc2b3ca1 fix(claude-local): skip expired credentials file when reading the Claude token (#13505)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents on the Claude Local adapter run on a local Claude Code
subscription. A user connects that subscription from Agent → Harness /
Runtime → "Connect account".
> - The server verifies the login with `readClaudeToken` in
`packages/adapters/claude-local/src/server/quota.ts`. It reads
`~/.claude/.credentials.json` first and consults the macOS Keychain only
when no file is present.
> - On macOS the Claude CLI refreshes the Keychain item, not the file. A
leftover credentials file keeps an expired token forever, and the reader
ignores `claudeAiOauth.expiresAt`.
> - The stale file shadows the live Keychain login. The usage check
fails and the user sees "Could not verify the local subscription"
although `claude auth status` reports a valid login. Running `claude
auth login` again does not help.
> - This pull request skips a credentials file whose token has expired,
so the reader falls through to the Keychain or returns null.
> - The benefit is that a valid local Claude login connects on the first
try, and a dead token is never sent upstream.

## Linked Issues or Issue Description

No public issue exists for this bug. Description follows
`bug_report.yml`.

### What happened?

Agent → Harness / Runtime → "Connect account" → Claude (Subscription) →
Connect failed with:

> Could not verify the local subscription. Run claude auth login in a
terminal on the machine running Paperclip, then try Connect again.

`claude auth status` on the same machine reported `loggedIn: true`,
`authMethod: claude.ai`, `subscriptionType: max`. The Keychain item
`Claude Code-credentials` held a fresh token. `POST
/api/companies/:id/ai-connections/local/check` returned
`{"status":"sign_in_required"}`.

A leftover `~/.claude/.credentials.json` (written weeks earlier) held an
access token that expired the same day it was written. `readClaudeToken`
returned that token. `fetchClaudeQuota` got a non-OK response from
`/api/oauth/usage`, and the route threw the generic 422.

### Expected behavior

An expired credentials file must not block a valid login. The reader
skips the dead token and falls through to the Keychain. Connect
succeeds.

### Steps to reproduce

1. On macOS, sign in with `claude auth login` (credentials land in the
Keychain).
2. Place a `~/.claude/.credentials.json` with
`claudeAiOauth.accessToken` set and `claudeAiOauth.expiresAt` in the
past.
3. Open an agent → Harness / Runtime → Connect account → Claude
(Subscription) → Connect.
4. Before this change: the "Could not verify the local subscription"
error appears. After: the connection is created.

### Agent adapter(s) involved

Claude Code (`@paperclipai/adapter-claude-local`)

### Operating system

macOS (Keychain-backed credentials). On Linux the file is the live
store; an expired file token now returns null instead of a failing
request, so the user-facing message is unchanged.

## What Changed

- `packages/adapters/claude-local/src/server/quota.ts`:
`parseClaudeCredential` now returns the token plus `expiresAt` (epoch
ms) when the file records one. `readClaudeTokenFromFile` returns `null`
for a token whose `expiresAt` is in the past, so `readClaudeToken` moves
on to the next candidate (second file name, then Keychain when
`allowKeychain` is set). Files without an `expiresAt` keep the old
behavior. `parseClaudeCredentialToken` (used for the Keychain payload)
is unchanged in behavior.
- `packages/adapters/claude-local/src/server/quota-keychain.test.ts`:
three new cases — expired file falls through to Keychain; expired file
with no Keychain access returns `null`; a file with no expiry is still
accepted.

## Verification

- `pnpm --filter @paperclipai/adapter-claude-local exec vitest run
src/server/quota-keychain.test.ts` → 7 passed (4 existing + 3 new).
- `pnpm --filter @paperclipai/adapter-claude-local exec tsc --noEmit` →
clean.
- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/local-ai-credentials.test.ts` → 9 passed.
- Manual, on the affected machine: with the stale file in place, `POST
/api/companies/:id/ai-connections/local/check` (provider `anthropic`,
method `subscription`) returned `sign_in_required`; with the stale file
removed it returned `ready`. This change makes the first case behave
like the second without touching the file.

## Risks

- Low risk. The only behavior change is for a credentials file that
carries a numeric `expiresAt` in the past. Such a token is already
rejected upstream, so the change removes a guaranteed failure rather
than a working path.
- Clock skew: a machine clock that runs ahead of real time could treat a
token as expired slightly early. The fall-through then reads the
Keychain (macOS) or returns null, which triggers the same "sign in"
message the user already sees for an expired token.
- Keychain payloads are not expiry-checked in this PR. The CLI refreshes
that item itself, and `getQuotaWindows` already falls back to the CLI
`/usage` probe when the OAuth call fails.

## Model Used

- Claude — `claude-fable-5-1` (Claude Fable 5.1) via Claude Code, with
extended thinking and tool use (shell, file edit). Root cause found by
reproducing the server's read path against the local credential file and
Keychain.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 16:48:07 -07:00
..