Architecture

Directory layout

spawn follows the XDG Base Directory Specification for all configuration and state.

Path Purpose
~/.config/spawn/env Default environment variables
~/.local/state/spawn/<agent>/ Agent credentials and session state
~/.local/state/spawn/git/ Copied git config for container mounts
~/.local/state/spawn/ssh/ Copied SSH keys for container mounts
~/.local/state/spawn/gh/ Copied gh CLI config for container mounts

These paths respect XDG_CONFIG_HOME and XDG_STATE_HOME environment variables. For example, if XDG_STATE_HOME is set to /custom/state, spawn stores state at /custom/state/spawn/ instead of ~/.local/state/spawn/.

Container images

spawn uses layered container images. All toolchain images extend spawn-base:latest:

spawn-base:latest
  ├── spawn-cpp:latest
  ├── spawn-rust:latest
  └── spawn-go:latest
  └── spawn-js:latest

Base image contents

The base image (spawn-base:latest) includes:

  • Ubuntu 24.04
  • Node.js, npm, Python 3
  • Claude Code (native installer), Codex (npm)
  • git, gh CLI, curl, wget
  • ripgrep, fd-find, jq, tree
  • Safe-mode wrapper scripts for git/gh
  • Non-root coder user with sudo access

JS/TS image contents

The JavaScript/TypeScript image (spawn-js:latest) extends the base image and adds:

  • Node.js 22 LTS
  • Corepack for pnpm/yarn workflows
  • Bun
  • Deno

Image management

spawn build              # Build all images (4 CPUs, 8GB memory by default)
spawn build base         # Build base only
spawn build rust         # Build a toolchain image
spawn build js           # Build the JS/TS toolchain image
spawn build --memory 16g # Build with more memory if needed
spawn image list         # List spawn images
spawn image rm <name>    # Remove a spawn image

The builder container defaults to 4 CPUs and 8GB memory (--cpus and --memory flags). Apple’s container build defaults to only 2GB, which is insufficient for the Claude Code installer.

Containerfile content is embedded in the spawn binary as string literals, so spawn build works after installation without depending on the source repository.

Run pipeline

When you run spawn ., the following modules execute in sequence:

RunCommand.run()
  → AgentProfile.named()          # Validate agent (claude-code/codex)
  → SettingsSeeder.seed()         # Seed safe-mode permissions (claude-code only)
  → ToolchainDetector.detect()    # Auto-detect or use override
  → ImageResolver.resolve()       # Map toolchain to image name
  → MountResolver.resolve()       # Build mount list
  → EnvLoader.load/loadDefault()  # Load env vars
  → ContainerRunner.run()         # Launch container

Design decisions

Apple’s container CLI

All container interaction goes through Apple’s container CLI, auto-detected at /opt/homebrew/bin/container or /usr/local/bin/container, falling back to PATH lookup. Override with the CONTAINER_PATH environment variable.

TTY via execv

When stdin is a real terminal, spawn uses execv to replace its process with container, giving the container CLI direct TTY access. This is required for interactive I/O. When stdin is a pipe, it falls back to Foundation.Process with signal forwarding.

VirtioFS workaround

VirtioFS preserves host file ownership and permissions. Files owned by the macOS user (uid 501) with 600 permissions are unreadable by the container’s coder user (uid 1001). spawn copies git config and SSH keys to the state directory where it controls permissions, then mounts the copies.

Single-file bind mounts also don’t support atomic rename (EBUSY). ~/.claude.json is handled via a symlink into a directory mount (~/.claude-state/) to work around this.

Credential persistence

Agent credentials are stored on the host at ~/.local/state/spawn/<agent>/ and mounted into containers. This means users authenticate once and credentials survive container restarts. No API keys are required for Claude Pro/Max plan users who authenticate via OAuth.

SSH key handling

SSH keys are copied (not mounted directly) to the state directory. Symlinks are filtered out to prevent exfiltrating files outside ~/.ssh/. Private keys get 0600 permissions on the copies.


This site uses Just the Docs, a documentation theme for Jekyll.