-
Notifications
You must be signed in to change notification settings - Fork 577
feature: support for ~/.claude/settings.json #248
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
77d0dce
d90147a
0f3a98f
4aa0e53
850f8e5
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 |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| /** | ||
| * Shared utilities for reading and parsing ~/.claude/settings.json | ||
| * | ||
| * This file centralizes all logic related to the Claude settings file | ||
| * to avoid code duplication across multiple files. | ||
| */ | ||
|
|
||
| import fs from 'fs/promises'; | ||
| import path from 'path'; | ||
| import os from 'os'; | ||
| import { createLogger } from '@automaker/utils'; | ||
|
|
||
| const logger = createLogger('ClaudeSettings'); | ||
|
|
||
| export interface ClaudeSettings { | ||
| env?: Record<string, string>; | ||
| apiKey?: string; | ||
| api_key?: string; | ||
| oauthToken?: string; | ||
| oauth_token?: string; | ||
| primaryApiKey?: string; | ||
| } | ||
|
|
||
| /** | ||
| * Get the path to ~/.claude/settings.json | ||
| */ | ||
| export function getSettingsPath(): string { | ||
| return path.join(os.homedir(), '.claude', 'settings.json'); | ||
| } | ||
|
|
||
| /** | ||
| * Load and parse ~/.claude/settings.json | ||
| * Returns null if file doesn't exist or is invalid | ||
| */ | ||
| export async function loadClaudeSettings(): Promise<ClaudeSettings | null> { | ||
| try { | ||
| const content = await fs.readFile(getSettingsPath(), 'utf-8'); | ||
| return JSON.parse(content); | ||
| } catch (error: unknown) { | ||
| // It's fine if the file doesn't exist, but other errors should be logged | ||
| const nodeError = error as NodeJS.ErrnoException; | ||
| if (nodeError.code !== 'ENOENT') { | ||
| logger.error('Error reading or parsing ~/.claude/settings.json:', error); | ||
| } | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Extract authentication token from settings | ||
| * Checks multiple locations in order of priority: | ||
| * 1. env.ANTHROPIC_AUTH_TOKEN - Claude Code format | ||
| * 2. oauthToken / oauth_token - root-level OAuth tokens | ||
| * 3. apiKey / api_key / primaryApiKey - root-level API keys | ||
| */ | ||
| export function extractAuthToken(settings: ClaudeSettings): string | null { | ||
| // Check env section (Claude Code format) | ||
| if (settings.env?.ANTHROPIC_AUTH_TOKEN) { | ||
| return settings.env.ANTHROPIC_AUTH_TOKEN; | ||
| } | ||
|
|
||
| // Check for OAuth tokens at root level | ||
| if (settings.oauthToken) { | ||
| return settings.oauthToken; | ||
| } | ||
| if (settings.oauth_token) { | ||
| return settings.oauth_token; | ||
| } | ||
|
|
||
| // Check for API keys at root level | ||
| if (settings.apiKey) { | ||
| return settings.apiKey; | ||
| } | ||
| if (settings.api_key) { | ||
| return settings.api_key; | ||
| } | ||
| if (settings.primaryApiKey) { | ||
| return settings.primaryApiKey; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Check if settings file exists and contains authentication | ||
| */ | ||
| export async function hasSettingsFileAuth(): Promise<boolean> { | ||
| const settings = await loadClaudeSettings(); | ||
| return settings ? extractAuthToken(settings) !== null : false; | ||
| } | ||
|
|
||
| /** | ||
| * Get auth token from settings file | ||
| */ | ||
| export async function getSettingsFileToken(): Promise<string | null> { | ||
| const settings = await loadClaudeSettings(); | ||
| return settings ? extractAuthToken(settings) : null; | ||
| } | ||
|
|
||
| /** | ||
| * Get the env section from settings file | ||
| * Returns null if file doesn't exist or has no env section | ||
| */ | ||
| export async function getSettingsEnv(): Promise<Record<string, string> | null> { | ||
| const settings = await loadClaudeSettings(); | ||
| if (!settings) { | ||
| return null; | ||
| } | ||
|
|
||
| // Return the env section if it exists and has content | ||
| if (settings.env && typeof settings.env === 'object' && Object.keys(settings.env).length > 0) { | ||
| return settings.env; | ||
| } | ||
|
|
||
| // Build env from root-level tokens if no env section | ||
| const env: Record<string, string> = {}; | ||
|
|
||
| // Check for OAuth tokens at root level | ||
| if (settings.oauthToken) { | ||
| env.ANTHROPIC_API_KEY = settings.oauthToken; | ||
| } else if (settings.oauth_token) { | ||
| env.ANTHROPIC_API_KEY = settings.oauth_token; | ||
| } | ||
| // Check for API keys at root level | ||
| else if (settings.apiKey) { | ||
| env.ANTHROPIC_API_KEY = settings.apiKey; | ||
| } else if (settings.api_key) { | ||
| env.ANTHROPIC_API_KEY = settings.api_key; | ||
| } else if (settings.primaryApiKey) { | ||
| env.ANTHROPIC_API_KEY = settings.primaryApiKey; | ||
| } | ||
|
|
||
| return Object.keys(env).length > 0 ? env : null; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,11 @@ | |
| * | ||
| * Wraps the @anthropic-ai/claude-agent-sdk for seamless integration | ||
| * with the provider architecture. | ||
| * | ||
| * Supports authentication via: | ||
| * 1. ~/.claude/settings.json - Loads env variables from settings file | ||
| * 2. Claude CLI (via claude login) - uses OAuth tokens from ~/.claude/ | ||
| * 3. Anthropic API Key - via ANTHROPIC_API_KEY env var or in-memory storage | ||
| */ | ||
|
|
||
| import { query, type Options } from '@anthropic-ai/claude-agent-sdk'; | ||
|
|
@@ -13,6 +18,27 @@ import type { | |
| InstallationStatus, | ||
| ModelDefinition, | ||
| } from './types.js'; | ||
| import { getSettingsEnv, hasSettingsFileAuth } from '../lib/claude-settings.js'; | ||
|
|
||
| /** | ||
| * Helper to get Anthropic API key from in-memory storage. | ||
| * Used as fallback when no env var or settings file is available. | ||
| */ | ||
| function getInMemoryApiKey(): string | undefined { | ||
| // Try dynamic import of in-memory storage | ||
| try { | ||
| // Dynamic import to avoid circular dependency | ||
| const setupCommon = require('../routes/setup/common.js'); | ||
| const apiKey = setupCommon.getApiKey?.('anthropic'); | ||
| if (apiKey) { | ||
| return apiKey; | ||
| } | ||
| } catch { | ||
| // Setup routes not available | ||
| } | ||
|
|
||
| return undefined; | ||
| } | ||
|
|
||
| export class ClaudeProvider extends BaseProvider { | ||
| getName(): string { | ||
|
|
@@ -21,6 +47,9 @@ export class ClaudeProvider extends BaseProvider { | |
|
|
||
| /** | ||
| * Execute a query using Claude Agent SDK | ||
| * | ||
| * Loads environment variables from ~/.claude/settings.json if available. | ||
| * This allows the SDK to use authentication tokens and other settings. | ||
| */ | ||
| async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> { | ||
| const { | ||
|
|
@@ -35,6 +64,46 @@ export class ClaudeProvider extends BaseProvider { | |
| sdkSessionId, | ||
| } = options; | ||
|
|
||
| // Track original environment to restore later | ||
| const originalEnv: Record<string, string | undefined> = {}; | ||
| const envKeysToRestore: string[] = []; | ||
|
|
||
| // Load settings from ~/.claude/settings.json | ||
| const settingsEnv = await getSettingsEnv(); | ||
|
|
||
| // Set environment variables from settings file if available | ||
| if (settingsEnv) { | ||
| for (const [key, value] of Object.entries(settingsEnv)) { | ||
| // Save original value if it exists | ||
| if (key in process.env) { | ||
| originalEnv[key] = process.env[key]; | ||
| } | ||
| envKeysToRestore.push(key); | ||
| // Set the environment variable | ||
| process.env[key] = value; | ||
| } | ||
| // Map ANTHROPIC_AUTH_TOKEN to ANTHROPIC_API_KEY if present | ||
| if (settingsEnv.ANTHROPIC_AUTH_TOKEN && !settingsEnv.ANTHROPIC_API_KEY) { | ||
| if ('ANTHROPIC_API_KEY' in process.env) { | ||
| originalEnv.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; | ||
| } | ||
| if (!envKeysToRestore.includes('ANTHROPIC_API_KEY')) { | ||
| envKeysToRestore.push('ANTHROPIC_API_KEY'); | ||
| } | ||
| process.env.ANTHROPIC_API_KEY = settingsEnv.ANTHROPIC_AUTH_TOKEN; | ||
| } | ||
| } | ||
|
|
||
| // If no API key env var is set yet (from settings or original env), try in-memory storage | ||
| if (!process.env.ANTHROPIC_API_KEY) { | ||
| const inMemoryApiKey = getInMemoryApiKey(); | ||
| if (inMemoryApiKey) { | ||
| originalEnv.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; | ||
| envKeysToRestore.push('ANTHROPIC_API_KEY'); | ||
| process.env.ANTHROPIC_API_KEY = inMemoryApiKey; | ||
| } | ||
| } | ||
|
Comment on lines
71
to
105
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. 🧩 Analysis chain🏁 Script executed: # First, let's examine the actual file to understand the full context
cat -n apps/server/src/providers/claude-provider.ts | head -250Repository: AutoMaker-Org/automaker Length of output: 9075 🏁 Script executed: # Also check if the file exists and get its size
wc -l apps/server/src/providers/claude-provider.tsRepository: AutoMaker-Org/automaker Length of output: 115 🏁 Script executed: # Search for the Options type/interface definition in the codebase
rg -t ts -A 10 "interface\s+Options|type\s+Options" --max-count=20Repository: AutoMaker-Org/automaker Length of output: 886 🏁 Script executed: # Check if there are any existing mutex or synchronization patterns in the codebase
rg -t ts "Mutex|Lock|acquire|release" --max-count=30Repository: AutoMaker-Org/automaker Length of output: 3823 🏁 Script executed: # Look for how executeQuery is called to understand if it's actually concurrent
rg -t ts "executeQuery|executeQuery\(" apps/server/src/providers/ -B 2 -A 2Repository: AutoMaker-Org/automaker Length of output: 1228 🏁 Script executed: # Check if Claude SDK is available and search for Options type definition
find node_modules/@anthropic-ai -name "*.d.ts" -o -name "*.ts" 2>/dev/null | head -20Repository: AutoMaker-Org/automaker Length of output: 49 🏁 Script executed: # Try to find the Options interface definition in node_modules
find node_modules/@anthropic-ai -type f -name "*.d.ts" -exec grep -l "Options" {} \; 2>/dev/null | head -5Repository: AutoMaker-Org/automaker Length of output: 49 🏁 Script executed: # Search for where executeQuery is called/invoked
rg -t ts "executeQuery\(" --max-count=20 -B 2 -A 2Repository: AutoMaker-Org/automaker Length of output: 8680 🏁 Script executed: # Check for concurrent request handling patterns or queuing mechanisms
rg -t ts "Promise\.all|Promise\.race|Promise\.allSettled" apps/server/src/ -B 2 -A 2 --max-count=15Repository: AutoMaker-Org/automaker Length of output: 1511 🏁 Script executed: # Look for any concurrency-related comments or TODOs
rg -t ts "(concurrent|parallel|race|queue|mutex|lock|serialize)" apps/server/src/providers/ -iRepository: AutoMaker-Org/automaker Length of output: 299 🏁 Script executed: # Check package.json for Claude SDK version and see if it's listed as a dependency
cat package.json | grep -A 5 -B 5 "anthropic\|claude" | head -40Repository: AutoMaker-Org/automaker Length of output: 49 🏁 Script executed: # Check if there's a package-lock.json or yarn.lock that might reference the SDK
find . -name "package.json" -type f | xargs grep -l "anthropic" 2>/dev/null | head -5Repository: AutoMaker-Org/automaker Length of output: 131 🏁 Script executed: # Look at agent-service to understand how executeQuery is called
sed -n '1,100p' apps/server/src/services/agent-service.tsRepository: AutoMaker-Org/automaker Length of output: 2949 🏁 Script executed: # Check if there are simultaneous calls to executeQuery in auto-mode-service
grep -n "executeQuery" apps/server/src/services/auto-mode-service.tsRepository: AutoMaker-Org/automaker Length of output: 399 🏁 Script executed: # Search for test files related to concurrency or parallel execution
find apps/server/tests -name "*.test.ts" | xargs grep -l "concurrent\|parallel" 2>/dev/nullRepository: AutoMaker-Org/automaker Length of output: 143 🏁 Script executed: # Look at the auto-mode-service to see the context around executeQuery calls
sed -n '1100,1120p' apps/server/src/services/auto-mode-service.tsRepository: AutoMaker-Org/automaker Length of output: 787 🏁 Script executed: # Check the context around line 1830
sed -n '1820,1850p' apps/server/src/services/auto-mode-service.tsRepository: AutoMaker-Org/automaker Length of output: 1297 🏁 Script executed: # Look for the context around line 2047
sed -n '2035,2060p' apps/server/src/services/auto-mode-service.tsRepository: AutoMaker-Org/automaker Length of output: 1232 🏁 Script executed: # Check if any of these executeQuery calls happen within Promise.all or similar concurrent patterns
grep -n "Promise.all\|Promise.race\|Promise.allSettled" apps/server/src/services/auto-mode-service.tsRepository: AutoMaker-Org/automaker Length of output: 49 🏁 Script executed: # Check the structure - are executeQuery calls awaited sequentially?
sed -n '2040,2080p' apps/server/src/services/auto-mode-service.tsRepository: AutoMaker-Org/automaker Length of output: 2091 🏁 Script executed: # Check if there are websocket or HTTP handlers that might call executeQuery from different concurrent requests
grep -n "executeQuery\|agentService\|provider.executeQuery" apps/server/src/routes/*.ts apps/server/src/routes/**/*.ts 2>/dev/null | head -40Repository: AutoMaker-Org/automaker Length of output: 1596 🏁 Script executed: # Look at how agent-service methods are called from routes (potential concurrency point)
grep -rn "startConversation\|handleUserMessage" apps/server/src/routes --include="*.ts" | head -20Repository: AutoMaker-Org/automaker Length of output: 174 🏁 Script executed: # Check if there's any concurrency protection in agent-service when handling requests
sed -n '150,300p' apps/server/src/services/agent-service.tsRepository: AutoMaker-Org/automaker Length of output: 5481 🏁 Script executed: # Check for WebSocket or HTTP request handlers that might handle multiple concurrent requests
find apps/server/src/routes -name "*.ts" -type f | xargs grep -l "WebSocket\|ws\|concurrent" 2>/dev/nullRepository: AutoMaker-Org/automaker Length of output: 946 🏁 Script executed: # Look for rate limiting, queueing, or concurrency control mechanisms
rg -t ts "queue|Queue|limit|Limit|serial|Serial|concurrency" apps/server/src/ --max-count=20Repository: AutoMaker-Org/automaker Length of output: 6170 🏁 Script executed: # Look at the send.ts route to see how executeQuery is called from HTTP handlers
cat -n apps/server/src/routes/agent/routes/send.ts | head -100Repository: AutoMaker-Org/automaker Length of output: 1914 🏁 Script executed: # Check if there's a single provider instance or multiple
grep -n "ProviderFactory\|getProviderForModel\|new ClaudeProvider" apps/server/src -r --include="*.ts"Repository: AutoMaker-Org/automaker Length of output: 1578 🏁 Script executed: # Look at how multiple concurrent requests could hit executeQuery
sed -n '1,50p' apps/server/src/routes/agent/routes/send.tsRepository: AutoMaker-Org/automaker Length of output: 1564 🏁 Script executed: # Search for the Options type from Claude SDK - check what properties it supports
rg -t ts -A 30 "export.*type.*Options|export.*interface.*Options" apps/server/src --max-count=5Repository: AutoMaker-Org/automaker Length of output: 6323 🏁 Script executed: # Check the actual import and see if we can find SDK type definitions
grep -A 20 "from '@anthropic-ai/claude-agent-sdk'" apps/server/src/providers/claude-provider.tsRepository: AutoMaker-Org/automaker Length of output: 759 🏁 Script executed: # Try to find Claude SDK in package.json to see the version
grep -A 2 -B 2 "claude-agent-sdk\|@anthropic-ai" apps/server/package.jsonRepository: AutoMaker-Org/automaker Length of output: 226 🏁 Script executed: # Look for any documentation or comments about the Claude SDK Options
rg -t ts "Claude.*Option|SDK.*Option" apps/server/src --max-count=10Repository: AutoMaker-Org/automaker Length of output: 598 🏁 Script executed: # Check if there are concurrent sendMessage calls that could overlap
grep -n "sendMessage\|executeQuery" apps/server/src/services/agent-service.ts | head -20Repository: AutoMaker-Org/automaker Length of output: 149 🏁 Script executed: # Look at the actual session handling to see if there's any serialization
sed -n '290,330p' apps/server/src/services/agent-service.tsRepository: AutoMaker-Org/automaker Length of output: 1194 Risk: Global state mutation causes concurrency issues in concurrent request scenarios. The code modifies
Suggested improvements:
import { Mutex } from 'async-mutex';
const envMutex = new Mutex();
async *executeQuery(options: ExecuteOptions) {
const release = await envMutex.acquire();
try {
// ... environment manipulation
} finally {
release();
// ... restore environment
}
}
🤖 Prompt for AI Agents |
||
|
|
||
| // Build Claude SDK options | ||
| const defaultTools = ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch']; | ||
| const toolsToUse = allowedTools || defaultTools; | ||
|
|
@@ -92,6 +161,15 @@ export class ClaudeProvider extends BaseProvider { | |
| } catch (error) { | ||
| console.error('[ClaudeProvider] executeQuery() error during execution:', error); | ||
| throw error; | ||
| } finally { | ||
| // Restore original environment | ||
| for (const key of envKeysToRestore) { | ||
| if (originalEnv[key] !== undefined) { | ||
| process.env[key] = originalEnv[key]; | ||
| } else { | ||
| delete process.env[key]; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -100,13 +178,25 @@ export class ClaudeProvider extends BaseProvider { | |
| */ | ||
| async detectInstallation(): Promise<InstallationStatus> { | ||
| // Claude SDK is always available since it's a dependency | ||
| const hasApiKey = !!process.env.ANTHROPIC_API_KEY; | ||
| // Check for authentication from multiple sources | ||
| const hasEnvApiKey = !!process.env.ANTHROPIC_API_KEY; | ||
| const hasInMemoryApiKey = !!getInMemoryApiKey(); | ||
| const hasSettingsAuth = await hasSettingsFileAuth(); | ||
|
|
||
| // Authenticated if we have any auth source | ||
| const authenticated = hasEnvApiKey || hasInMemoryApiKey || hasSettingsAuth; | ||
|
|
||
| const status: InstallationStatus = { | ||
| installed: true, | ||
| method: 'sdk', | ||
| hasApiKey, | ||
| authenticated: hasApiKey, | ||
| hasApiKey: hasEnvApiKey || hasInMemoryApiKey, | ||
| authenticated, | ||
| // Additional info about auth method | ||
| authMethod: hasSettingsAuth | ||
| ? 'settings_file' | ||
| : hasEnvApiKey || hasInMemoryApiKey | ||
| ? 'api_key' | ||
| : 'none', | ||
| }; | ||
|
|
||
| return status; | ||
|
|
||
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.
Modifying the global
process.envobject is not safe for concurrent execution. If two requests callexecuteQueryat the same time, one request might use the environment variables set by the other, leading to incorrect authentication or data leakage. This is a critical issue for a server application.Ideally, the credentials should be passed directly to the SDK's
queryfunction if its API supports it. If the SDK strictly relies on environment variables, you might need to consider a different approach, such as using child processes for execution or implementing a mutex/lock to ensure only oneexecuteQueryruns at a time (which would hurt performance).