System Implementation Review: Conductor's Claude Code Integration
End-to-End Flow Overview
┌──────────────────────────────────────────────────────────────────────────────┐ │ CONDUCTOR ARCHITECTURE │ │ │ │ ┌─────────────┐ IPC ┌──────────────┐ Unix ┌─────────────────┐ │ │ │ Tauri Frontend│◄──────►│ Rust Backend │◄──Socket──►│ Node.js Sidecar │ │ │ │ (WebKit) │ invoke │ (conductor) │ │ (index.bundled) │ │ │ └─────────────┘ └──────────────┘ └────────┬────────┘ │ │ │ │ │ │ SQLite DB stdin/stdout pipes │ │ (conductor.db) │ │ │ ┌───────▼────────┐ │ │ │ Claude Code │ │ │ │ CLI (bundled) │ │ │ └────────────────┘ │ └──────────────────────────────────────────────────────────────────────────────┘
Phase 1: "Add Claude Code" — Credential Acquisition
When you press "Add Claude Code" in the UI, the Tauri frontend collects credentials and stores them in the SQLite database at: ~/Library/Application Support/com.conductor.app/conductor.db
The settings table uses a key-value schema. Credentials were historically stored as individual keys (anthropic_api_key, anthropic_auth_token, anthropic_base_url), but migration #46 consolidated them into a single claude_env_vars key:
-- Migration: "migrate provider settings to claude_env_vars" WITH provider_settings AS ( SELECT MAX(CASE WHEN key = 'anthropic_base_url' THEN value END) as anthropic_base_url, MAX(CASE WHEN key = 'anthropic_auth_token' THEN value END) as anthropic_auth_token, MAX(CASE WHEN key = 'anthropic_api_key' THEN value END) as anthropic_api_key, MAX(CASE WHEN key = 'http_proxy' THEN value END) as http_proxy, MAX(CASE WHEN key = 'bedrock_profile' THEN value END) as bedrock_profile, MAX(CASE WHEN key = 'vertex_project_id' THEN value END) as vertex_project_id FROM settings ) INSERT OR REPLACE INTO settings (key, value) SELECT 'claude_env_vars', LTRIM( CASE WHEN anthropic_base_url IS NOT NULL THEN 'ANTHROPIC_BASE_URL=' || anthropic_base_url ELSE '' END || CASE WHEN anthropic_auth_token IS NOT NULL THEN CHAR(10) || 'ANTHROPIC_AUTH_TOKEN=' || anthropic_auth_token ELSE '' END || CASE WHEN anthropic_api_key IS NOT NULL THEN CHAR(10) || 'ANTHROPIC_API_KEY=' || anthropic_api_key ELSE '' END -- ... plus HTTP_PROXY, BEDROCK, VERTEX mappings )
The claude_env_vars value is a newline-delimited string of KEY=VALUE pairs — essentially a .env file stored in SQLite. This is what the Rust backend reads and passes to the sidecar on every request.
Security note: Credentials are stored in plaintext in the SQLite database. No OS Keychain integration. Anyone with read access to ~/Library/Application Support/com.conductor.app/conductor.db can extract your API keys.
Phase 2: Auth Verification
After storing credentials, Conductor verifies them by spawning Claude Code and calling its accountInfo() SDK method.
Rust backend sends a JSON-RPC message over the Unix socket to the sidecar: { "type": "claude_auth", "id": "request-123", "agentType": "claude", "options": { "cwd": "/path/to/workspace" } }
Sidecar (index.bundled.js:30004) handles this: FrontendAPI.onClaudeAuth((request) => { return claudeAuthRpc({ id: request.id, cwd: request.options.cwd }); });
claudeAuthRpc → getClaudeAccountInfo() (index.bundled.js:29229): async function getClaudeAccountInfo(cwd) { const emptyPromptInput = async function* () {}(); // empty generator — no actual prompt const sdkOptions = { cwd, pathToClaudeCodeExecutable, systemPrompt: DEFAULT_PROMPT }; const queryResult = Wg({ prompt: emptyPromptInput, options: sdkOptions }); const accountInfo = await queryResult.accountInfo(); // SDK method on Claude Code void queryResult.interrupt(); // immediately kill the process after getting info return { accountInfo }; }
This spawns a real Claude Code process just to call .accountInfo(), which returns the account's email, plan type, and capabilities. The process is immediately interrupted after. Claude Code reads its own credentials from ~/.claude/ — at this stage, Conductor doesn't inject any env vars.
The response flows back: { "type": "claude_auth_output", "agentType": "claude", "accountInfo": { ... }, "error": null }
Phase 3: Workspace Initialization
After auth succeeds, Conductor calls workspaceInit to discover the workspace's capabilities.
Sidecar handler (index.bundled.js:30010): FrontendAPI.onWorkspaceInit(async (request) => { return workspaceInitRpc2({ cwd: request.options.cwd, ghToken: request.options.ghToken, claudeEnvVars: request.options.claudeEnvVars // from SQLite settings }); });
getClaudeWorkspaceInitData() (index.bundled.js:29261) — this is the first place credentials are actively injected:
async function getClaudeWorkspaceInitData(options) { const envForClaude = {}; Object.assign(envForClaude, getShellEnvironment(options.cwd)); // shell env, creds STRIPPED // Overlay process.env for (const [key, value] of Object.entries(process.env)) { envForClaude[key] = value; } // Apply user-configured claude_env_vars (from SQLite) if (options.claudeEnvVars) { const parsed = parseEnvString(options.claudeEnvVars); for (const [key, value] of Object.entries(parsed)) { if (value === "") delete envForClaude[key]; else envForClaude[key] = value; } } if (options.ghToken) envForClaude["GH_TOKEN"] = options.ghToken;
1const queryResult = Wg({ prompt: emptyPromptInput, options: { ...sdkOptions, env: envForClaude } });
2 const [slashCommands, mcpServers] = await Promise.all([
3 queryResult.supportedCommands(),
4 queryResult.mcpServerStatus()
5 ]);
6 void queryResult.interrupt();
7 return { slashCommands, mcpServers };}
This spawns Claude Code with the full environment to discover available slash commands and MCP server status for the workspace. The process is killed immediately after.
Phase 4: Sidecar Bootstrap — The Runtime Harness
The Node.js sidecar is the persistent process that manages Claude Code's lifecycle. It starts when Conductor launches.
Startup (index.bundled.js:30226): var sidecar = new UnifiedSidecar(); sidecar.start();
UnifiedSidecar constructor (index.bundled.js:29936):
constructor() {
this.socketPath = path7.join(os3.tmpdir(), conductor-sidecar-${process.pid}.sock);
this.server = net.createServer((socket) => this.handleConnection(socket));
1// Parent process watchdog — if Tauri dies, sidecar self-terminates
2 const parentPid = process.ppid;
3 setInterval(() => {
4 if (process.ppid !== parentPid) this.shutdownAndExit();
5 }, 2000);}
start() (index.bundled.js:30160): async start() { await this.cleanup(); // remove stale socket files const claudeInit = initializeClaudeHandler(); // verify Claude binary exists this.server.listen(this.socketPath, () => { emitSocketPathForParent(this.socketPath); // tell Rust backend where to connect }); }
initializeClaudeHandler() (index.bundled.js:29192) — resolves the bundled Claude binary:
function initializeClaudeHandler() {
const sidecarDir = path6.dirname(process.argv[1]); // directory of index.bundled.js
const claudeExecutablePath = path6.join(sidecarDir, "claude");
const version = execSync("${claudeExecutablePath}" -v).trim();
pathToClaudeCodeExecutable = claudeExecutablePath; // store globally
return { success: true };
}
The path resolves to /Applications/Conductor.app/Contents/Resources/bin/claude.
Communication: The Rust backend connects to the Unix socket. All messages are newline-delimited JSON-RPC over the socket.
Phase 5: Query Execution — Scaffolding the Claude Code Process
When you send a message in the UI, this is the full chain:
5a. Session Creation (index.bundled.js:29409)
async function handleClaudeQuery(sessionId, prompt, options) { // Can we reuse an existing Claude Code process? const canReuse = session && session.generator && !settingsChanged;
1if (canReuse) {
2 // Push message into existing process's stdin queue
3 session.sendMesssage(prompt);
4 } else {
5 // Create fresh session state
6 session = {
7 currentSettings: {
8 claudeEnvVars: options.claudeEnvVars,
9 additionalDirectories: options.additionalDirectories,
10 chromeEnabled: options.chromeEnabled,
11 enterpriseDataPrivacy: options.enterpriseDataPrivacy
12 },
13 currentModel: options.model,
14 turnId: options.turnId,
15 cwd: options.cwd,
16 isProcessing: true
17 };
18 activeSessions.set(sessionId, session);
19 void processWithGenerator(sessionId, prompt, options);
20 }}
5b. Environment Assembly (index.bundled.js:29581-29634)
This is the critical credential injection point:
// Layer 1: Shell environment (credentials STRIPPED) const envForClaude = {}; Object.assign(envForClaude, getShellEnvironment(options.cwd));
// Layer 2: Node process.env for (const [key, value] of Object.entries(process.env)) { envForClaude[key] = value; }
// Layer 3: Conductor-specific vars envForClaude["CLAUDE_CODE_ENABLE_TASKS"] = "true";
// Layer 4: conductorEnv (from frontend) if (options.conductorEnv) { Object.assign(envForClaude, options.conductorEnv); }
// Layer 5: claudeEnvVars from SQLite (HIGHEST PRIORITY) if (options.claudeEnvVars) { const parsed = parseEnvString(options.claudeEnvVars); for (const [key, value] of Object.entries(parsed)) { if (value === "") delete envForClaude[key]; // empty = unset else envForClaude[key] = value; } }
// Layer 6: GitHub token if (options.ghToken) envForClaude["GH_TOKEN"] = options.ghToken;
Credential stripping in getShellEnvironment() (index.bundled.js:1532): var STRIPPED_ENV_KEYS = [ "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "CONDUCTOR_CWD" ]; Shell environment variables with these keys are deleted before merging — this prevents shell-level credentials from overriding the user's configured credentials from the database.
Diagnostic logging redacts credentials:
logInfo(ANTHROPIC_AUTH_TOKEN: ${envForClaude.ANTHROPIC_AUTH_TOKEN ? Set (${envForClaude.ANTHROPIC_AUTH_TOKEN.substring(0, 10)}...) : "(not set)"});
5c. SDK Options Construction (index.bundled.js:29724-29839)
const sdkOptions = { maxTurns: 1000, model: modelToUse, // mapped per provider (Bedrock/Vertex/default) cwd: options.cwd, pathToClaudeCodeExecutable, // /Applications/Conductor.app/.../bin/claude systemPrompt: { type: "preset", preset: "claude_code" }, settingSources: ["user", "project", "local"], env: envForClaude, // the assembled environment canUseTool, // tool approval callback additionalDirectories: [...], // allowed write directories disallowedTools: ["AskUserQuestion"], // Conductor provides its own permissionMode: "default", mcpServers: { conductor: createConductorMCPServer(sessionId) }, hooks: { UserPromptSubmit: [...], Stop: [...], PostToolUse: [...] } };
5d. Claude Code Process Spawn (index.bundled.js:7997-8074)
The Wg() function creates a ProcessTransport that spawns Claude Code:
// Argument construction args = [ pathToClaudeCodeExecutable, // /Applications/.../bin/claude "--output-format", "stream-json", "--verbose", "--input-format", "stream-json", "--model", modelToUse, "--permission-mode", permissionMode, "--mcp-config", JSON.stringify({ mcpServers: sdkOptions.mcpServers }), "--setting-sources", "user,project,local" // + conditional: --thinking, --max-turns, --resume, --session-id, etc. ];
// Spawn const process = spawn("node", args, { cwd: workingDirectory, stdio: ["pipe", "pipe", debugEnabled ? "pipe" : "ignore"], env: envForClaude, // credentials injected here windowsHide: true });
The spawned process is node /path/to/claude --output-format stream-json ... with the full environment containing ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN.
5e. I/O Message Loop (index.bundled.js:29843-29897)
The harness creates an async generator for multi-turn conversation:
// Input: async generator that yields user messages const promptInput = async function* () { while (true) { let message = messageQueue.length > 0 ? messageQueue.shift() : await new Promise(resolve => { waitingForMessage = resolve; }); yield { type: "user", message: { role: "user", content: message } }; } }();
// Execute query const queryResult = Wg({ prompt: promptInput, options: sdkOptions }); queries.set(sessionId, queryResult);
// Output: forward all messages to Tauri frontend for await (const message of queryResult) { FrontendAPI.sendMessage({ id: sessionId, type: "message", agentType: "claude", data: message }); if (message.type === "result") { session.isProcessing = false; enforceMaxIdleSessions(); } }
Phase 6: Runtime Security Controls
Tool approval gate (canUseTool callback, index.bundled.js:29639):
- ExitPlanMode — requires explicit UI approval via FrontendAPI.requestExitPlanMode()
- Edit, MultiEdit, Write, NotebookEdit — file paths validated against cwd + additionalDirectories allowlist using path.resolve() normalization
- All other tools — auto-allowed
Git checkpointing (hooks on UserPromptSubmit and Stop):
- Snapshot created at turn start and end via checkpointer.sh
- Enables "time travel" to undo Claude's changes per-turn
Session lifecycle management:
- 30-minute idle timeout, max 5 idle sessions
- Sweep every 60 seconds
- SIGTERM with 5-second grace period, then SIGKILL
- Parent PID watchdog every 2 seconds
Security Summary ┌─────────────────────────┬──────────────────────────────────────────────────┬────────────────────────┐ │ Aspect │ Implementation │ Risk Level │ ├─────────────────────────┼──────────────────────────────────────────────────┼────────────────────────┤ │ Credential storage │ Plaintext in SQLite (conductor.db) │ Medium — no Keychain │ ├─────────────────────────┼──────────────────────────────────────────────────┼────────────────────────┤ │ Credential transmission │ Environment variables on spawn() │ Low — standard pattern │ ├─────────────────────────┼──────────────────────────────────────────────────┼────────────────────────┤ │ Claude binary │ Unmodified official CLI (v2.1.70) │ Low │ ├─────────────────────────┼──────────────────────────────────────────────────┼────────────────────────┤ │ Process isolation │ Piped stdio, no shared handles │ Low │ ├─────────────────────────┼──────────────────────────────────────────────────┼────────────────────────┤ │ File access control │ Path allowlist with path.resolve() │ Low │ ├─────────────────────────┼──────────────────────────────────────────────────┼────────────────────────┤ │ Log redaction │ First 10 chars of tokens only │ Low │ ├─────────────────────────┼──────────────────────────────────────────────────┼────────────────────────┤ │ Shell env stripping │ API keys removed before merge │ Good mitigation │ ├─────────────────────────┼──────────────────────────────────────────────────┼────────────────────────┤ │ Socket permissions │ /tmp/conductor-sidecar-{pid}.sock, default perms │ Medium │ ├─────────────────────────┼──────────────────────────────────────────────────┼────────────────────────┤ │ Session cleanup │ Idle timeout + watchdog │ Good │ ├─────────────────────────┼──────────────────────────────────────────────────┼────────────────────────┤ │ Binary freshness │ Bundled v2.1.70 may lag upstream patches │ Medium │ └─────────────────────────┴──────────────────────────────────────────────────┴────────────────────────┘
✻ Worked for 5m 40s
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
❯
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
⏵⏵ accept edits on (shift+tab to cycle) ✗ Auto-update failed · Try claude doctor or npm i -g @anthropic-ai/claude-code