Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: enhance credential loading with hex-encoded JSON support
- Added a new function to parse credentials from hex-encoded JSON format, improving compatibility with macOS keychain items.
- Updated the credential loading logic to utilize the new parsing function for both file and keychain sources.
- Added a test case to verify the correct handling of hex-encoded JSON credentials.
  • Loading branch information
robinebers committed Feb 3, 2026
commit 3f5d017ef91d1995e46ff32d2c0fd0038447974c
32 changes: 30 additions & 2 deletions plugins/claude/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,39 @@
const SCOPES = "user:profile user:inference user:sessions:claude_code user:mcp_servers"
const REFRESH_BUFFER_MS = 5 * 60 * 1000 // refresh 5 minutes before expiration

function tryParseCredentialJSON(text) {
if (!text) return null
const trimmed = String(text).trim()
if (!trimmed) return null
try {
return JSON.parse(trimmed)
} catch {}

// Some macOS keychain items are returned by `security ... -w` as hex-encoded UTF-8 bytes.
// Example prefix: "7b0a" ( "{\\n" ).
// Support both plain hex and "0x..." forms.
let hex = trimmed
if (hex.startsWith("0x") || hex.startsWith("0X")) hex = hex.slice(2)
if (!hex || hex.length % 2 !== 0) return null
if (!/^[0-9a-fA-F]+$/.test(hex)) return null
try {
let decoded = ""
for (let i = 0; i < hex.length; i += 2) {
decoded += String.fromCharCode(parseInt(hex.slice(i, i + 2), 16))
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
}
Comment thread
cursor[bot] marked this conversation as resolved.
return JSON.parse(decoded)
} catch {}

return null
}

function loadCredentials(ctx) {
// Try file first
if (ctx.host.fs.exists(CRED_FILE)) {
try {
const text = ctx.host.fs.readText(CRED_FILE)
const parsed = JSON.parse(text)
const parsed = tryParseCredentialJSON(text)
if (!parsed) return null
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
const oauth = parsed.claudeAiOauth
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow keychain fallback when file parse fails

If the credentials file exists but contains malformed JSON (e.g., partial write), tryParseCredentialJSON returns null and this code returns immediately, so the keychain fallback is never attempted. Before this change, a parse error in the file path would drop into the keychain branch, so users with a corrupt file but valid keychain entry now get a hard "Not logged in" error. Consider only returning null when a valid parsed file is present but missing tokens, and otherwise continue to keychain.

Useful? React with 👍 / 👎.

if (oauth && oauth.accessToken) {
return { oauth, source: "file", fullData: parsed }
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
Expand All @@ -25,7 +52,8 @@
try {
const keychainValue = ctx.host.keychain.readGenericPassword(KEYCHAIN_SERVICE)
if (keychainValue) {
const parsed = JSON.parse(keychainValue)
const parsed = tryParseCredentialJSON(keychainValue)
if (!parsed) return null
const oauth = parsed.claudeAiOauth
if (oauth && oauth.accessToken) {
return { oauth, source: "keychain", fullData: parsed }
Expand Down
17 changes: 17 additions & 0 deletions plugins/claude/plugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,23 @@ describe("claude plugin", () => {
expect(result.lines.find((line) => line.label === "Extra usage")).toBeTruthy()
})

it("uses keychain credentials when value is hex-encoded JSON", async () => {
const ctx = makeCtx()
ctx.host.fs.exists = () => false
const json = JSON.stringify({ claudeAiOauth: { accessToken: "token", subscriptionType: "pro" } }, null, 2)
const hex = Buffer.from(json, "utf8").toString("hex")
ctx.host.keychain.readGenericPassword.mockReturnValue(hex)
ctx.host.http.request.mockReturnValue({
status: 200,
bodyText: JSON.stringify({
five_hour: { utilization: 1, resets_at: "2099-01-01T00:00:00.000Z" },
}),
})
const plugin = await loadPlugin()
const result = plugin.probe(ctx)
expect(result.lines.find((line) => line.label === "Session")).toBeTruthy()
})

it("throws on http errors and parse failures", async () => {
const ctx = makeCtx()
ctx.host.fs.readText = () => JSON.stringify({ claudeAiOauth: { accessToken: "token" } })
Expand Down