🎯

auth-flow

🎯Skill

from seanchiuai/multishot

VibeIndex|
What it does

Manages secure authentication for Claude Code in headless Daytona sandboxes by injecting API keys or OAuth tokens as environment variables.

auth-flow

Installation

Install skill:
npx skills add https://github.com/seanchiuai/multishot --skill auth-flow
0
AddedJan 25, 2026

Skill Details

SKILL.md

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.

  1. Get API key from [console.anthropic.com](https://console.anthropic.com)
  2. Store securely (Electron safeStorage)
  3. 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.

  1. User authenticates locally: claude (opens browser)
  2. Token stored at: ~/.config/claude-code/auth.json
  3. User provides token to app
  4. 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 safeStorage API (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:

  1. API Key: Revoke at console.anthropic.com β†’ API Keys
  2. OAuth Token: Revoke at claude.ai β†’ Account Settings β†’ Security β†’ Active Sessions

Best Practices

  1. Prefer API Key for sandbox/headless use - most reliable
  2. Validate on entry - test credentials before saving
  3. Handle expiry - OAuth tokens can expire, detect and prompt
  4. Secure storage - always use safeStorage, never plaintext
  5. Minimal scope - only request permissions needed
  6. Clear on logout - remove credentials when user logs out