-
Notifications
You must be signed in to change notification settings - Fork 0
Add Linux support for Claude Code OAuth token retrieval #258
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,9 @@ import ( | |
| "encoding/json" | ||
| "fmt" | ||
| "net/http" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "runtime" | ||
| "sync" | ||
| "time" | ||
|
|
@@ -40,12 +42,12 @@ type anthropicUsageBucket struct { | |
| ResetsAt string `json:"resets_at"` | ||
| } | ||
|
|
||
| // keychainCredentials maps the JSON stored in macOS Keychain for Claude Code | ||
| type keychainCredentials struct { | ||
| ClaudeAiOauth *keychainOAuthEntry `json:"claudeAiOauth"` | ||
| // claudeCodeCredentials maps the JSON stored in macOS Keychain or ~/.claude/.credentials.json | ||
| type claudeCodeCredentials struct { | ||
| ClaudeAiOauth *claudeCodeOAuthEntry `json:"claudeAiOauth"` | ||
| } | ||
|
|
||
| type keychainOAuthEntry struct { | ||
| type claudeCodeOAuthEntry struct { | ||
| AccessToken string `json:"accessToken"` | ||
| RefreshToken string `json:"refreshToken"` | ||
| ExpiresAt int64 `json:"expiresAt"` | ||
|
|
@@ -54,25 +56,55 @@ type keychainOAuthEntry struct { | |
| RateLimitTier any `json:"rateLimitTier"` | ||
| } | ||
|
|
||
| // fetchClaudeCodeOAuthToken reads the OAuth token from macOS Keychain. | ||
| // Returns ("", nil) on non-macOS platforms. | ||
| // fetchClaudeCodeOAuthToken reads the OAuth token from the platform-specific credential store. | ||
| // macOS: reads from Keychain via `security` command. | ||
| // Linux: reads from ~/.claude/.credentials.json file. | ||
| // Returns ("", nil) on unsupported platforms. | ||
| func fetchClaudeCodeOAuthToken() (string, error) { | ||
| if runtime.GOOS != "darwin" { | ||
| switch runtime.GOOS { | ||
| case "darwin": | ||
| return fetchOAuthTokenFromKeychain() | ||
| case "linux": | ||
| return fetchOAuthTokenFromCredentialsFile() | ||
| default: | ||
| return "", nil | ||
| } | ||
| } | ||
|
|
||
| // fetchOAuthTokenFromKeychain reads the OAuth token from macOS Keychain. | ||
| func fetchOAuthTokenFromKeychain() (string, error) { | ||
| out, err := exec.Command("security", "find-generic-password", "-s", "Claude Code-credentials", "-w").Output() | ||
| if err != nil { | ||
| return "", fmt.Errorf("keychain lookup failed: %w", err) | ||
| } | ||
|
|
||
| var creds keychainCredentials | ||
| if err := json.Unmarshal(out, &creds); err != nil { | ||
| return "", fmt.Errorf("failed to parse keychain JSON: %w", err) | ||
| return parseOAuthTokenFromJSON(out) | ||
| } | ||
|
|
||
| // fetchOAuthTokenFromCredentialsFile reads the OAuth token from ~/.claude/.credentials.json. | ||
| func fetchOAuthTokenFromCredentialsFile() (string, error) { | ||
| homeDir, err := os.UserHomeDir() | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to get home directory: %w", err) | ||
| } | ||
|
|
||
| data, err := os.ReadFile(filepath.Join(homeDir, ".claude", ".credentials.json")) | ||
| if err != nil { | ||
| return "", fmt.Errorf("credentials file read failed: %w", err) | ||
| } | ||
|
|
||
| return parseOAuthTokenFromJSON(data) | ||
| } | ||
|
Comment on lines
+85
to
+97
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To improve maintainability and avoid hardcoding file path components, consider defining func fetchOAuthTokenFromCredentialsFile() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get home directory: %w", err)
}
const (
claudeConfigDir = ".claude"
claudeCredentialsFile = ".credentials.json"
)
credentialsPath := filepath.Join(homeDir, claudeConfigDir, claudeCredentialsFile)
data, err := os.ReadFile(credentialsPath)
if err != nil {
return "", fmt.Errorf("credentials file read failed: %w", err)
}
return parseOAuthTokenFromJSON(data)
}References
|
||
|
|
||
| // parseOAuthTokenFromJSON parses Claude Code credentials JSON and extracts the OAuth access token. | ||
| func parseOAuthTokenFromJSON(data []byte) (string, error) { | ||
| var creds claudeCodeCredentials | ||
| if err := json.Unmarshal(data, &creds); err != nil { | ||
| return "", fmt.Errorf("failed to parse credentials JSON: %w", err) | ||
| } | ||
|
|
||
| if creds.ClaudeAiOauth == nil || creds.ClaudeAiOauth.AccessToken == "" { | ||
| return "", fmt.Errorf("no OAuth access token found in keychain") | ||
| return "", fmt.Errorf("no OAuth access token found in credentials") | ||
| } | ||
|
|
||
| return creds.ClaudeAiOauth.AccessToken, nil | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 CLAUDE.md rule states quota is macOS-only but code now enables it on Linux without updating the rule
CLAUDE.md line 127 states: "CC statusline quota display is macOS-only (requires Keychain access to Claude Code OAuth token)". The new code at
commands/cc_statusline.go:218adds|| runtime.GOOS == "linux"to enable quota display on Linux, directly contradicting this documented constraint. The same violation applies to the fallback output atcommands/cc_statusline.go:282and the daemon fetch guard atdaemon/cc_info_timer.go:398. CLAUDE.md must be updated to reflect that quota is now supported on both macOS (Keychain) and Linux (~/.claude/.credentials.json).Prompt for agents
Was this helpful? React with 👍 or 👎 to provide feedback.