fix(claude): read credentials from ~/.claude/.credentials.json
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:
@@ -1,8 +1,8 @@
|
||||
"""Host Claude auth helpers.
|
||||
|
||||
Reads the host's Claude Code auth state and returns only the
|
||||
session key needed by egress. Does not expose refresh tokens
|
||||
or raw auth payloads.
|
||||
Reads the host's Claude Code credentials from ~/.claude/.credentials.json
|
||||
and returns only the access token needed by egress. Does not expose
|
||||
refresh tokens or raw auth payloads.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -19,8 +19,8 @@ def claude_auth_path(host_env: dict[str, str] | None = None) -> Path:
|
||||
env = os.environ if host_env is None else host_env
|
||||
home = env.get("HOME")
|
||||
if home:
|
||||
return Path(home) / ".claude.json"
|
||||
return Path.home() / ".claude.json"
|
||||
return Path(home) / ".claude" / ".credentials.json"
|
||||
return Path.home() / ".claude" / ".credentials.json"
|
||||
|
||||
|
||||
def claude_host_access_token(
|
||||
@@ -42,32 +42,33 @@ def claude_host_access_token(
|
||||
if not isinstance(raw, dict):
|
||||
die(f"claude host credentials: {path} must contain a JSON object")
|
||||
|
||||
oauth = raw.get("oauthAccount")
|
||||
oauth = raw.get("claudeAiOauth")
|
||||
if not isinstance(oauth, dict):
|
||||
die(
|
||||
f"claude host credentials: {path} is missing oauthAccount. "
|
||||
f"claude host credentials: {path} is missing claudeAiOauth. "
|
||||
"Run `claude login` on the host or disable "
|
||||
"agent_provider.forward_host_credentials."
|
||||
)
|
||||
|
||||
session_key = oauth.get("sessionKey")
|
||||
if not isinstance(session_key, str) or not session_key:
|
||||
access_token = oauth.get("accessToken")
|
||||
if not isinstance(access_token, str) or not access_token:
|
||||
die(
|
||||
f"claude host credentials: {path} oauthAccount.sessionKey is missing "
|
||||
f"claude host credentials: {path} claudeAiOauth.accessToken is missing "
|
||||
"or empty. Run `claude login` on the host and restart the bottle."
|
||||
)
|
||||
|
||||
# expiresAt is stored in milliseconds
|
||||
expires_at = oauth.get("expiresAt")
|
||||
if isinstance(expires_at, (int, float)):
|
||||
check_now = now or datetime.now(timezone.utc)
|
||||
exp_dt = datetime.fromtimestamp(float(expires_at), timezone.utc)
|
||||
exp_dt = datetime.fromtimestamp(float(expires_at) / 1000.0, timezone.utc)
|
||||
if exp_dt <= check_now:
|
||||
die(
|
||||
"claude host credentials: host Claude session token is expired. "
|
||||
"claude host credentials: host Claude access token is expired. "
|
||||
"Run `claude login` on the host and restart the bottle."
|
||||
)
|
||||
|
||||
return session_key
|
||||
return access_token
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
||||
Reference in New Issue
Block a user