fix(claude): read credentials from ~/.claude/.credentials.json
lint / lint (push) Successful in 2m10s
test / unit (pull_request) Successful in 53s
test / integration (pull_request) Successful in 16s
test / coverage (pull_request) Successful in 1m8s

The actual OAuth token is in ~/.claude/.credentials.json under
claudeAiOauth.accessToken, not in ~/.claude.json.
~/.claude.json holds only UI state and profile metadata (oauthAccount
has no token fields). expiresAt in the credentials file is milliseconds,
not seconds.

Discovered after testing against Claude Code 2.1.198.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 21:33:04 +00:00
parent f0d27863c2
commit bdd352570b
4 changed files with 84 additions and 44 deletions
@@ -68,15 +68,32 @@ Rejects in manifest validation when:
### Host auth extraction (`contrib/claude/claude_auth.py`)
Claude Code stores its OAuth credentials in `~/.claude/.credentials.json`
(not in `~/.claude.json`, which contains only UI state and profile
metadata). The relevant fields are:
```json
{
"claudeAiOauth": {
"accessToken": "sk-ant-oat01-...",
"refreshToken": "sk-ant-ort01-...",
"expiresAt": 1748276587173,
"scopes": ["user:inference", "user:profile"]
}
}
```
Note: `expiresAt` is in **milliseconds** (not seconds).
At prepare/launch time, when `forward_host_credentials: true`:
1. Resolve `~/.claude.json` (falling back to `$HOME/.claude.json`).
1. Resolve `~/.claude/.credentials.json` (via `$HOME`).
2. Parse the JSON object.
3. Require an `oauthAccount` dict.
4. Require a non-empty `oauthAccount.sessionKey` string.
5. If `oauthAccount.expiresAt` is present as a number, require it to be
in the future.
6. Return only the session key to the launch path.
3. Require a `claudeAiOauth` dict.
4. Require a non-empty `claudeAiOauth.accessToken` string.
5. If `claudeAiOauth.expiresAt` is present as a number, divide by 1000
and require the result to be in the future.
6. Return only the access token to the launch path.
Errors name the missing or invalid condition and point the operator at
`claude login`, without printing token values.