auth-flow
π―Skillfrom seanchiuai/multishot
Manages secure authentication for Claude Code in headless Daytona sandboxes by injecting API keys or OAuth tokens as environment variables.
Installation
npx skills add https://github.com/seanchiuai/multishot --skill auth-flowSkill Details
Claude Code authentication in headless Daytona sandboxes. Use when implementing AuthManager, setup wizard, credential validation, or handling OAuth token/API key injection.
Overview
# Auth Flow
> Sources: [Claude Code Setup](https://code.claude.com/docs/en/setup), [API Key Management](https://support.claude.com/en/articles/12304248-managing-api-key-environment-variables-in-claude-code)
MVP Status
AuthManager not implemented. MVP uses SetupWizard to collect credentials at runtime (no persistence). Credentials passed directly to IPC handlers.
Current flow: User enters keys in SetupWizard β passed to window.api.startRun() β injected into sandbox env vars.
---
The Problem
Claude Code requires browser-based OAuth authentication, but Daytona sandboxes are headless (no browser access).
Environment Variables
| Variable | Description | Auth Method |
|----------|-------------|-------------|
| ANTHROPIC_API_KEY | API key from console.anthropic.com | Pay-as-you-go API |
| CLAUDE_CODE_OAUTH_TOKEN | OAuth token from local auth | Subscription-based |
Priority: ANTHROPIC_API_KEY overrides OAuth token if both are set.
Warning: Setting ANTHROPIC_API_KEY bypasses any subscription and charges to API pay-as-you-go rates.
Authentication Options for Sandboxes
Option 1: API Key (Recommended for Sandboxes)
Most reliable for headless environments.
- Get API key from [console.anthropic.com](https://console.anthropic.com)
- Store securely (Electron safeStorage)
- Inject as environment variable in sandbox:
```typescript
const sandbox = await daytona.create({
language: 'typescript',
envVars: {
ANTHROPIC_API_KEY: credentials.apiKey
}
})
```
Pros: Simple, reliable, no OAuth complexity
Cons: Pay-as-you-go pricing (no subscription usage)
Option 2: OAuth Token Transfer
Transfer OAuth credentials from authenticated local machine.
- User authenticates locally:
claude(opens browser) - Token stored at:
~/.config/claude-code/auth.json - User provides token to app
- App injects as environment variable:
```typescript
const sandbox = await daytona.create({
language: 'typescript',
envVars: {
CLAUDE_CODE_OAUTH_TOKEN: credentials.oauthToken
}
})
```
Pros: Uses subscription plan
Cons: Token may expire, manual extraction needed
auth.json Location & Format
```
~/.config/claude-code/auth.json
```
The token is portable across machines (not IP/machine-bound).
Security: Treat like a password. Revoke immediately if compromised via Claude.ai account settings.
AuthManager Implementation
```typescript
import { safeStorage } from 'electron'
interface Credentials {
daytonaApiKey: string
authMethod: 'api_key' | 'oauth_token'
anthropicApiKey?: string
claudeOAuthToken?: string
}
class AuthManager {
private readonly STORAGE_KEY = 'multishot-credentials'
async loadCredentials(): Promise
try {
const encrypted = await this.readFromStorage()
if (!encrypted) return null
const decrypted = safeStorage.decryptString(encrypted)
return JSON.parse(decrypted)
} catch {
return null
}
}
async saveCredentials(credentials: Credentials): Promise
const encrypted = safeStorage.encryptString(JSON.stringify(credentials))
await this.writeToStorage(encrypted)
}
async validateApiKey(apiKey: string): Promise
try {
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
'content-type': 'application/json'
},
body: JSON.stringify({
model: 'claude-3-haiku-20240307',
max_tokens: 1,
messages: [{ role: 'user', content: 'hi' }]
})
})
return response.ok || response.status === 400 // 400 = valid key, bad request
} catch {
return false
}
}
async validateDaytonaKey(apiKey: string): Promise
try {
const { Daytona } = await import('@daytonaio/sdk')
const daytona = new Daytona({ apiKey })
await daytona.list({}, 1, 1) // Simple list call to verify
return true
} catch {
return false
}
}
getEnvVarsForSandbox(credentials: Credentials): Record
if (credentials.authMethod === 'api_key' && credentials.anthropicApiKey) {
return { ANTHROPIC_API_KEY: credentials.anthropicApiKey }
}
if (credentials.claudeOAuthToken) {
return { CLAUDE_CODE_OAUTH_TOKEN: credentials.claudeOAuthToken }
}
return {}
}
async clearCredentials(): Promise
await this.deleteFromStorage()
}
// Platform-specific storage methods
private async readFromStorage(): Promise
// Implementation using electron-store or similar
}
private async writeToStorage(data: Buffer): Promise
// Implementation
}
private async deleteFromStorage(): Promise
// Implementation
}
}
export const authManager = new AuthManager()
```
Setup Wizard Flow
```
βββββββββββββββββββββββββββββββββββββββββββ
β Welcome to Multishot β
β β
β Enter your Daytona API Key: β
β [____________________________________] β
β β
β Get key: app.daytona.io β
βββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββ
β Claude Authentication β
β β
β β API Key (recommended for sandboxes) β
β - Pay-as-you-go pricing β
β - Most reliable β
β β
β β OAuth Token β
β - Uses subscription plan β
β - May require refresh β
β β
β [Continue] β
βββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββ
β (If API Key selected) β
β β
β Anthropic API Key: β
β [____________________________________] β
β β
β Get key: console.anthropic.com β
β β
β [Validate & Save] β
βββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββ
β (If OAuth Token selected) β
β β
β 1. Run 'claude' locally to auth β
β 2. Find token in: β
β ~/.config/claude-code/auth.json β
β 3. Paste token below: β
β β
β [____________________________________] β
β β
β [Validate & Save] β
βββββββββββββββββββββββββββββββββββββββββββ
```
Credential Storage
Requirements:
- Never store plaintext credentials in database or files
- Use Electron's
safeStorageAPI (OS keychain integration) - Environment variables exist only during sandbox lifetime
- Redact credentials in logs
```typescript
import { safeStorage } from 'electron'
// Check if encryption is available
if (safeStorage.isEncryptionAvailable()) {
// Encrypt before storing
const encrypted = safeStorage.encryptString(JSON.stringify(credentials))
// Decrypt when reading
const decrypted = safeStorage.decryptString(encrypted)
const credentials = JSON.parse(decrypted)
}
```
Error Detection & Recovery
Watch for these patterns in sandbox output:
| Error Pattern | Cause | Action |
|---------------|-------|--------|
| "Authentication failed" | Invalid credentials | Re-prompt setup wizard |
| "OAuth token has expired" | Token expired | Request new token |
| "Invalid API key" | Wrong or revoked key | Verify key in console |
| "Rate limit exceeded" | Too many requests | Back off, retry |
| "Auth conflict" | Both token and key set | Clear one method |
```typescript
function detectAuthError(output: string): 'expired' | 'invalid' | 'conflict' | null {
if (output.includes('OAuth token has expired')) return 'expired'
if (output.includes('Authentication failed')) return 'invalid'
if (output.includes('Invalid API key')) return 'invalid'
if (output.includes('Auth conflict')) return 'conflict'
return null
}
// In IPC handler
sandbox.process.getSessionCommandLogs(session, cmdId,
(stdout) => {
const error = detectAuthError(stdout)
if (error) {
sendToRenderer('auth-error', { type: error })
}
},
(stderr) => {
const error = detectAuthError(stderr)
if (error) {
sendToRenderer('auth-error', { type: error })
}
}
)
```
Checking Auth Status
In interactive Claude Code:
```bash
/status
```
Shows current authentication method and account info.
Token Revocation
If credentials are compromised:
- API Key: Revoke at console.anthropic.com β API Keys
- OAuth Token: Revoke at claude.ai β Account Settings β Security β Active Sessions
Best Practices
- Prefer API Key for sandbox/headless use - most reliable
- Validate on entry - test credentials before saving
- Handle expiry - OAuth tokens can expire, detect and prompt
- Secure storage - always use safeStorage, never plaintext
- Minimal scope - only request permissions needed
- Clear on logout - remove credentials when user logs out