claude-agent:wolfi is an ARM64 (Apple Silicon) image built on Chainguard Wolfi. It is specifically designed to run headless instances of Claude Code in Apple Containers, with support for parallel multi-agent operation.
FROM --platform=linux/arm64 cgr.dev/chainguard/rust:latest-dev AS builderCompiles CLI productivity tools written in Rust:
| Tool | Purpose | Replaces |
|---|---|---|
rg (ripgrep) |
Ultra-fast text search | grep |
fd (fd-find) |
File search | find |
bat |
Cat with syntax highlighting | cat |
eza |
Modern file listing | ls |
dust |
Disk usage visualization | du |
procs |
Modern process listing | ps |
btm (bottom) |
System monitor | top |
The compiled binaries are copied to stage 2, keeping the final image clean.
FROM --platform=linux/arm64 cgr.dev/chainguard/wolfi-base:latest AS runtimeInstalled system packages:
bash, busybox, curl, wget ← base utilities
git, git-lfs ← version control
openssh-client, ca-certificates ← secure connectivity
jq, unzip, gzip ← data processing
tmux ← terminal multiplexer
gh ← GitHub CLI
nodejs-22, npm ← JavaScript runtime
python-3.13, py3-pip ← Python runtime
Additional tools installed:
claude ← Claude Code CLI (via official install.sh)
opencode ← OpenCode AI CLI
openspec ← @fission-ai/openspec (global npm)
Configured environment variables:
LANG=C.UTF-8
LC_ALL=C.UTF-8
TERM=xterm-256color
PATH=/root/.local/bin:/usr/local/bin:$PATH
CLAUDE_CODE_DISABLE_AUTOUPDATE=1 ← prevents auto-updates in containers
BAT_PAGER=""
BAT_STYLE="numbers,changes,header"Rust aliases (configured in /etc/profile.d/rust-aliases.sh):
alias grep='rg --smart-case --follow'
alias find='fd --follow'
alias cat='bat --paging=never'
alias ls='eza'
alias ll='eza -la --git'
alias du='dust'
alias ps='procs'
alias top='btm'Global git configuration:
git config --global init.defaultBranch main
git config --global core.editor "true" # no-op editor (headless)
git config --global advice.detachedHead falseThe entrypoint supports two modes, selected by the arguments passed to the container.
container run -it claude-agent:wolfi
# or
container run -it claude-agent:wolfi /bin/bash --loginFlow:
- Copies credentials:
~/.claudenew.json→~/.claude.jsonand~/.claudenew/→~/.claude/ - Starts an interactive bash shell with the full profile loaded
container run -d --rm claude-agent:wolfi \
--worktree "feat/oauth2" \
--task "Implement OAuth2 with JWT tokens..."Entrypoint arguments:
| Argument | Description |
|---|---|
--worktree <branch> |
Name of the branch/worktree to create |
--task "<prompt>" |
Prompt for Claude in headless mode |
--project <name> |
(optional) Project name |
Flow:
- Copies credentials from host mounts
git -C /workspace worktree add /worktrees/<branch> -b <branch>- If the branch already exists:
git worktree add /worktrees/<branch> <branch>
- If the branch already exists:
cd /worktrees/<branch>claude --dangerously-skip-permissions -p "<task>"
Why --dangerously-skip-permissions: In headless mode there is no interactive user to approve permissions. The container is a sandboxed environment with access only to the mounted worktree, so it is safe to skip confirmations.
Why run as agent (non-root): Claude CLI blocks --dangerously-skip-permissions when the process runs as root (uid 0). The entrypoint uses su-exec to drop to the agent user before executing Claude.
IMPORTANT: Why the worktree is created inside the container: Git needs access to the repository to register the worktree in .git/worktrees/. Since the repo is mounted at /workspace inside the container, the worktree must be created from there. If it were created directly from the host, the path registered in git would be the host path (/Users/...), which would not exist inside the container.
| Variable | Default | Description |
|---|---|---|
IMAGE |
claude-agent:wolfi |
Docker image name |
DOCKERFILE |
Dockerfile.wolfi |
Dockerfile to use |
NAME |
qubits-team |
Base name for the interactive container |
NETWORK |
claude-agent-net |
Agent bridge network |
SUBNET |
192.168.100.0/24 |
Network CIDR |
CPUS |
8 |
CPUs allocated to each container |
MEMORY |
3G |
RAM allocated to each container |
BRANCH |
agent-<timestamp> |
Agent branch to spawn |
TASK |
Explore the codebase... |
Agent task |
AGENTS_HOME |
<parent-of-git-root>/.worktrees |
Fallback if not in env |
Automatically derived variables:
GIT_ROOT := $(shell git -C $(CURDIR) rev-parse --show-toplevel)
PROJECT_NAME := $(shell basename $(GIT_ROOT))
AGENTS_HOME ?= $(shell dirname $(GIT_ROOT))/.worktrees # fallback
WORKTREES_DIR := $(AGENTS_HOME)
CONTAINER_BRANCH := $(shell echo "$(BRANCH)" | tr '/_ ' '-' | tr '[:upper:]' '[:lower:]')Builds the image without cache.
make build
# equivalent to: container build --no-cache -f Dockerfile.wolfi -t claude-agent:wolfi .Creates the bridge network claude-agent-net if it does not exist. Requires macOS 26+.
make networkLaunches the container in interactive mode (coordinator or development session).
make run
make run NAME=my-agent CPUS=4 MEMORY=8GRequires CLAUDE_CONTAINER_OAUTH_TOKEN to be exported.
Launches a virtual agent in detached (headless) mode. The main target for multi-agent.
make spawn BRANCH=feat/oauth2 TASK="Implement OAuth2 with JWT"
make spawn BRANCH=test/auth TASK="Write unit tests for auth module"
make spawn BRANCH=mutation/payments TASK="Run mutation testing on payment service"- Creates
$AGENTS_HOMEif it does not exist - Launches container named
${PROJECT_NAME}-${CONTAINER_BRANCH} - Shows how to view logs upon completion
Lists active project containers and worktrees on disk.
make list-agentsShows agent logs (snapshot).
make logs-agent BRANCH=feat/oauth2Follows agent logs in real time.
make follow-agent BRANCH=feat/oauth2Stops the agent.
make stop-agent BRANCH=feat/oauth2Removes the container and the image. Does not affect worktrees.
make cleanRemoves the bridge network.
make clean-networkRemoves image and network.
make clean-all# Required for make run, make spawn
export CLAUDE_CONTAINER_OAUTH_TOKEN=<your-oauth-token>
# Recommended (fallback if not set: dirname(GIT_ROOT)/.worktrees)
export AGENTS_HOME=~/agentsWhy CLAUDE_CONTAINER_OAUTH_TOKEN and not CLAUDE_CODE_OAUTH_TOKEN:
The Makefile maps CLAUDE_CONTAINER_OAUTH_TOKEN from the host to CLAUDE_CODE_OAUTH_TOKEN inside the container. This prevents the container from reading the host session token, keeping sessions isolated.
cd /path/to/project/config
export CLAUDE_CONTAINER_OAUTH_TOKEN=<token>
make buildThe build may take several minutes the first time (compiles 7 Rust crates).
Edit the Makefile and remove --no-cache:
build:
container build -f $(DOCKERFILE) -t $(IMAGE) . # without --no-cachecontainer image list | grep "claude-agent.*wolfi"
container run --rm claude-agent:wolfi claude --versionThe claude-agent-net network (CIDR 192.168.100.0/24) allows containers to communicate with each other and access the internet via DNS 1.1.1.1 (Cloudflare).
# Create
container network create --subnet 192.168.100.0/24 claude-agent-net
# List
container network list
# Inspect
container network inspect claude-agent-net
# Remove
container network delete claude-agent-netmake network performs the create idempotently (does not fail if it already exists).
flowchart LR
subgraph Host
A["~/.claude/"]
B["~/.claude.json"]
C["CLAUDE_CONTAINER_OAUTH_TOKEN"]
end
subgraph Container
subgraph "entrypoint.sh"
D["/root/.claudenew/ (ro)"]
E["/root/.claudenew.json (ro)"]
F["cp -r → /root/.claude/"]
G["cp → /root/.claude.json"]
end
H["Claude Code reads /root/.claude/ (rw)"]
I["CLAUDE_CODE_OAUTH_TOKEN"]
end
A -- "ro mount" --> D
B -- "ro mount" --> E
D --> F
E --> G
F --> H
G --> H
C -- "-e flag" --> I
I --> H
The mounts are read-only from the host to prevent the container from modifying the original credentials. The entrypoint makes a local copy so Claude can write to its configuration directory without affecting the host.
Claude CLI blocks --dangerously-skip-permissions when the process runs as root (uid 0). The image includes an agent user (non-root) for headless mode.
# su-exec: privilege drop with exec semantics (Docker standard)
RUN apk add --no-cache su-exec
# agent user (non-root)
RUN addgroup -S agent \
&& adduser -S -G agent -h /home/agent -s /bin/bash agent \
&& ln -sf /root/.local/bin/claude /usr/local/bin/claude/root/.claude/ (copied by entrypoint from host mount)
│
└─► /home/agent/.claude/ (copied + chown → agent)
│
su-exec agent env HOME=/home/agent claude --dangerously-skip-permissions -p "..."
The entrypoint:
- Copies credentials to
/root/.claude/(as usual) - Also copies them to
/home/agent/.claude/withchown agent - Runs
chown agenton the worktree - Executes
su-exec agentto drop to non-root uid before calling Claude
su-exec performs a direct execvp (replaces the process, does not create a subshell). This preserves signals, the PID, and avoids the overhead of an additional shell. It is the standard for Docker container entrypoints.
- Containers run with
--rm(ephemeral) — they do not persist state outside the worktree - Credentials are mounted read-only from the host
CLAUDE_CODE_DISABLE_AUTOUPDATE=1prevents Claude from downloading updates inside the container- Each container has access only to the repo mounted at
/workspaceand$AGENTS_HOMEat/worktrees --dangerously-skip-permissionsis safe in this context because the accessible filesystem is limited to the mounted volumes- Headless mode runs as the
agentuser (non-root) by design
This project uses two different container strategies depending on the environment:
- Runtime: Apple Container CLI
- Dockerfile:
Dockerfile.wolfi(ARM64, Chainguard Wolfi) - Architecture:
linux/arm64(Apple Silicon) - C library:
glibc
The production image is built with Chainguard Wolfi, a hardened, minimal Linux distribution that uses glibc. This is required because Claude Code >= 2.1.63 depends on posix_getdents, a POSIX syscall wrapper that musl (the C library used by Alpine) does not provide. Wolfi provides glibc compatibility with a footprint comparable to Alpine, making it the ideal base for running Claude Code in containers.
Apple Container CLI is used locally to run containers natively on Apple Silicon (ARM64) with low overhead and tight macOS integration (shared networking, volume mounts, bridge networks on macOS 26+).
- Runtime: Docker
- Dockerfile:
Dockerfile(Alpine-based) - Architecture:
linux/amd64 - Purpose: Build validation and testing only
GitHub Actions runners are amd64 Linux machines. Apple Container CLI is not available in this environment — it is a macOS-only tool tied to the Apple Virtualization framework. Therefore, CI uses standard Docker with the Alpine-based Dockerfile for build validation.
The Alpine CI image is sufficient for verifying that the Dockerfile syntax is correct, dependencies resolve, and the build completes successfully. It is not used to run Claude Code headless agents — that is exclusively done via the Wolfi image on local Apple Silicon machines.
| Aspect | Production (local) | CI (GitHub Actions) |
|---|---|---|
| Container runtime | Apple Container CLI | Docker |
| Dockerfile | Dockerfile.wolfi |
Dockerfile (Alpine) |
| Architecture | linux/arm64 |
linux/amd64 |
| Base image | Chainguard Wolfi (glibc) | Alpine (musl) |
| Purpose | Run headless Claude agents | Build validation |
| Claude Code compatible | Yes (glibc + posix_getdents) |
No (musl lacks posix_getdents) |