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
+11 -9
View File
@@ -294,12 +294,13 @@ class TestAgentProviderRuntime(unittest.TestCase):
self.assertEqual({}, plan.provisioned_env)
def test_claude_forward_host_credentials_populates_egress_route(self):
session_key = "sk-ant-oat01-test-key"
access_token = "sk-ant-oat01-test-key"
with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp:
home = Path(tmp) / "host-claude"
home.mkdir()
(home / ".claude.json").write_text(json.dumps({
"oauthAccount": {"sessionKey": session_key},
cred_dir = home / ".claude"
cred_dir.mkdir(parents=True)
(cred_dir / ".credentials.json").write_text(json.dumps({
"claudeAiOauth": {"accessToken": access_token},
}))
plan = build_agent_provision_plan(
template="claude",
@@ -319,12 +320,13 @@ class TestAgentProviderRuntime(unittest.TestCase):
self.assertEqual(frozenset({"CLAUDE_CODE_OAUTH_TOKEN"}), plan.hidden_env_names)
def test_claude_forward_host_credentials_populates_provisioned_env(self):
session_key = "sk-ant-oat01-test-key"
access_token = "sk-ant-oat01-test-key"
with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp:
home = Path(tmp) / "host-claude"
home.mkdir()
(home / ".claude.json").write_text(json.dumps({
"oauthAccount": {"sessionKey": session_key},
cred_dir = home / ".claude"
cred_dir.mkdir(parents=True)
(cred_dir / ".credentials.json").write_text(json.dumps({
"claudeAiOauth": {"accessToken": access_token},
}))
plan = build_agent_provision_plan(
template="claude",
@@ -336,7 +338,7 @@ class TestAgentProviderRuntime(unittest.TestCase):
host_env={"HOME": str(home)},
)
self.assertEqual(
{CLAUDE_HOST_CREDENTIAL_TOKEN_REF: session_key},
{CLAUDE_HOST_CREDENTIAL_TOKEN_REF: access_token},
plan.provisioned_env,
)
+36 -16
View File
@@ -19,7 +19,9 @@ class TestClaudeHostAccessToken(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory(prefix="bb-claude-auth.")
self.home = Path(self.tmp.name)
self.auth_path = self.home / ".claude.json"
self.cred_dir = self.home / ".claude"
self.cred_dir.mkdir()
self.auth_path = self.cred_dir / ".credentials.json"
def tearDown(self):
self.tmp.cleanup()
@@ -33,36 +35,40 @@ class TestClaudeHostAccessToken(unittest.TestCase):
claude_auth_path({"HOME": str(self.home)}),
)
def test_returns_session_key(self):
def test_returns_access_token(self):
key = "sk-ant-oat01-real-key"
self._write({"oauthAccount": {"sessionKey": key}})
self._write({"claudeAiOauth": {"accessToken": key}})
out = claude_host_access_token({"HOME": str(self.home)})
self.assertEqual(key, out)
def test_missing_auth_file_dies(self):
with self.assertRaises(Die):
claude_host_access_token({"HOME": str(self.home)})
# Use a home with no .credentials.json
empty_home = self.home / "empty"
empty_home.mkdir()
claude_host_access_token({"HOME": str(empty_home)})
def test_missing_oauth_account_dies(self):
def test_missing_claude_ai_oauth_dies(self):
self._write({"hasCompletedOnboarding": True})
with self.assertRaises(Die):
claude_host_access_token({"HOME": str(self.home)})
def test_missing_session_key_dies(self):
self._write({"oauthAccount": {"expiresAt": 2000000000}})
def test_missing_access_token_dies(self):
self._write({"claudeAiOauth": {"expiresAt": 2000000000000}})
with self.assertRaises(Die):
claude_host_access_token({"HOME": str(self.home)})
def test_empty_session_key_dies(self):
self._write({"oauthAccount": {"sessionKey": ""}})
def test_empty_access_token_dies(self):
self._write({"claudeAiOauth": {"accessToken": ""}})
with self.assertRaises(Die):
claude_host_access_token({"HOME": str(self.home)})
def test_expired_token_dies(self):
# expiresAt is in milliseconds; 1000000 ms = year 1970
self._write({
"oauthAccount": {
"sessionKey": "sk-ant-oat01-x",
"expiresAt": 1000,
"claudeAiOauth": {
"accessToken": "sk-ant-oat01-x",
"expiresAt": 1000000,
},
})
with self.assertRaises(Die):
@@ -73,10 +79,11 @@ class TestClaudeHostAccessToken(unittest.TestCase):
def test_future_expiry_is_accepted(self):
key = "sk-ant-oat01-y"
# 2000000000000 ms = ~year 2033
self._write({
"oauthAccount": {
"sessionKey": key,
"expiresAt": 2000000000,
"claudeAiOauth": {
"accessToken": key,
"expiresAt": 2000000000000,
},
})
out = claude_host_access_token(
@@ -87,7 +94,7 @@ class TestClaudeHostAccessToken(unittest.TestCase):
def test_absent_expiry_field_is_accepted(self):
key = "sk-ant-oat01-z"
self._write({"oauthAccount": {"sessionKey": key}})
self._write({"claudeAiOauth": {"accessToken": key}})
out = claude_host_access_token({"HOME": str(self.home)})
self.assertEqual(key, out)
@@ -101,6 +108,19 @@ class TestClaudeHostAccessToken(unittest.TestCase):
with self.assertRaises(Die):
claude_host_access_token({"HOME": str(self.home)})
def test_extra_fields_in_credentials_are_ignored(self):
key = "sk-ant-oat01-real"
self._write({
"claudeAiOauth": {
"accessToken": key,
"refreshToken": "sk-ant-ort01-secret",
"scopes": ["user:inference", "user:profile"],
"expiresAt": 2000000000000,
},
})
out = claude_host_access_token({"HOME": str(self.home)})
self.assertEqual(key, out)
if __name__ == "__main__":
unittest.main()