Problem
You want to run Claude Code as a headless agent across multiple tasks in parallel, but each agent needs an isolated filesystem, its own OAuth credentials, and a way to stream structured logs back to a central API. Running agents directly on a shared host risks file conflicts, credential leakage, and no easy way to tear down state between runs.
Solution
Use a three-tier architecture: a Go WebSocket tunnel client on the user's machine, a Node.js orchestrator on Fly.io, and ephemeral Node.js worker agents on Fly.io Firecracker VMs.
Orchestrator spawns workers (up to 5 parallel):
// orchestrator.ts
import { spawn } from "child_process";
interface AgentTask {
id: string;
prompt: string;
repo: string;
agentConfig: string;
}
function spawnWorker(task: AgentTask): void {
const proc = spawn("claude", [task.prompt], {
cwd: `/workspace/${task.repo}`,
env: {
...process.env,
CLAUDE_CODE_OAUTH_TOKEN: getOAuthKey(task.id),
},
});
proc.stdout.on("data", (chunk) => {
tunnel.send(JSON.stringify({ taskId: task.id, log: chunk.toString() }));
});
}
Agent definitions as JSON in a library system:
{
"name": "code-reviewer",
"model": "claude-sonnet-4-20250514",
"system_prompt": "You are a senior code reviewer...",
"tools": ["Read", "Glob", "Grep"],
"max_tokens": 4096
}
Library directory structure for shared resources:
/data/library/
skill/{namespace}/{name}/
agent/{namespace}/{name}/
mcp/{namespace}/{name}/
hooks/{namespace}/{name}/
rules/{namespace}/{name}/
settings/{namespace}/{name}/
Per-repo MCP servers seeded at boot:
{
"mcpServers": {
"filesystem": { "command": "npx", "args": ["@anthropic/mcp-filesystem"] },
"github": { "command": "npx", "args": ["@anthropic/mcp-github"] },
"postgres": { "command": "npx", "args": ["@anthropic/mcp-postgres"] },
"puppeteer": { "command": "npx", "args": ["@anthropic/mcp-puppeteer"] }
}
}
Per-repo agent definitions with scoped tool access:
<!-- .claude/agents/code-reviewer.md -->
You are a code reviewer. Focus on correctness, security, and performance.
allowedTools: Read, Glob, Grep, WebFetch
deniedTools: Write, Edit, Bash
Why It Works
Firecracker VMs on Fly.io provide sub-second boot times with full isolation, so each Claude Code agent runs in a clean environment that is destroyed after the task completes. The WebSocket tunnel pipes SDK messages from ephemeral workers back to the Go client without exposing ports or managing long-lived connections. The library system with namespace scoping (_global for shared, project-specific otherwise) lets agents share skills and MCP configs without filesystem coupling. By calling the claude CLI directly rather than wrapping it in LangChain or CrewAI, you avoid framework overhead and get the full Claude Code tool suite -- including hooks, rules, and the agent system -- with zero abstraction tax.
Context
- The two key innovations are injecting the Claude Code OAuth key into ephemeral servers at boot and piping Claude Code SDK messages back through WebSocket tunnels to the central Go server
- No framework dependencies (no LangChain, no CrewAI) -- just the Claude Code CLI spawned as a child process
- Fly.io Firecracker VMs provide hardware-level isolation at container-like speeds
- The orchestrator caps at 5 parallel workers to stay within API rate limits
- Agent tool access is controlled via
allowedToolsanddeniedToolsarrays in per-repo agent definitions under.claude/agents/ - The
_globalnamespace makes skills and configs available to all repos without duplication