Tips · Oct 7, 2026 · 14 min read
Run Claude Code Headless in Docker, Plus Codex and OpenCode

TL;DR: To run Claude Code headless, call claude -p with --bare, --output-format json, an explicit --permission-mode and a tight --allowedTools list. Run it inside a non-root container and pass ANTHROPIC_API_KEY at runtime. Codex's equivalent is codex exec --json with CODEX_API_KEY. OpenCode's is opencode run, or a password-protected opencode serve on port 4096. In all three cases the container, not the CLI, is your security boundary, and the queue, persistence and multi-tenancy are still yours to build.
This is "way 2" from our guide to building an AI agent: adopt an existing harness and bring your own container. Flags were checked against vendor docs; Claude Code and Codex flags were also checked against local --help output.
The three CLIs at a glance
Each CLI has a non-interactive mode, a machine-readable output format and a way to run without anyone approving tool calls.
| Claude Code | Codex CLI | OpenCode | |
|---|---|---|---|
| One-shot command | claude -p "<task>" | codex exec "<task>" | opencode run "<task>" |
| Machine-readable output | --output-format json or stream-json | --json (JSONL events) | --format json |
| Schema-shaped answer | --json-schema '<schema>' | --output-schema schema.json | format option in the SDK/server API |
| Unattended permissions | --permission-mode dontAsk plus --allowedTools | --sandbox workspace-write, or danger-full-access inside a container | --auto plus a permission config |
| API key | ANTHROPIC_API_KEY | CODEX_API_KEY | The provider's own variable, such as ANTHROPIC_API_KEY |
| Continue a session | --resume <session_id> | codex exec resume --last | --session <id> or --continue |
Claude Code headless mode: the flags that matter
Anthropic documents headless use on its Run Claude Code programmatically page. These are the flags you'll use in a container:
-p(--print) runs one task and exits: 0 on success, non-zero on failure. On SIGTERM it exits 143 and leaves the turn unfinished; send SIGINT first to end the turn cleanly.--bareskips auto-discovery of hooks, skills, plugins, MCP servers, auto memory andCLAUDE.md. Anthropic recommends it for scripts and says it will become the default for-p. It also closes a real hole: without it, a-prun executes the hooks in a repo's.claude/settings.jsonand connects the servers in its.mcp.json, even in a folder you never trusted. Pass instructions with--append-system-promptor--append-system-prompt-file.--output-format jsonreturnsresult,session_idandtotal_cost_usd(a client-side estimate). Usestream-jsonwith--verboseand--include-partial-messagesto stream. Add--json-schema '<schema>'for validated output instructured_output.--allowedToolspre-approves tools with permission-rule syntax.Bash(npm test *)allowsnpm testwith or without arguments, andEditcovers every file-editing tool.--permission-modesets the baseline:dontAskdenies calls that would otherwise prompt, while calls needing no approval, such as certain read-only Bash commands, still run. Use--toolsto limit the available built-in tools.acceptEditsauto-approves file edits. Always pass a mode. The built-in default for-pdepends on the version and on whether the session fetches feature flags, and it can beauto.--max-turnsand--max-budget-usdcap a run. There is no turn limit by default.--resume <session_id>continues a conversation if its transcript is still on disk.--no-session-persistenceskips writing it.
Authentication. In -p mode, ANTHROPIC_API_KEY is always used when set. Bare mode accepts only that key or an apiKeyHelper, so a subscription token from claude setup-token needs a non-bare run, and it suits only your own team's automation. If the container serves your customers, use an API key (or Bedrock, Google Cloud's Agent Platform or Microsoft Foundry credentials): Anthropic's Agent SDK overview says third-party products may not offer claude.ai login unless approved, and may not call themselves "Claude Code".
Run Claude Code in Docker
A starting-point Dockerfile
This is a starting point, not a tested production image. It installs all three CLIs through npm (Claude Code's npm package needs Node.js 22 or later), so you can delete the ones you don't need.
# Starting point only. Pin exact versions and review before production use.
FROM node:22-bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends git ripgrep jq ca-certificates \
&& rm -rf /var/lib/apt/lists/*
ARG CLAUDE_CODE_VERSION=latest
ARG CODEX_VERSION=latest
ARG OPENCODE_VERSION=latest
RUN npm install -g \
@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} \
@openai/codex@${CODEX_VERSION} \
opencode-ai@${OPENCODE_VERSION}
# The image is the version: no self-updates, no telemetry or other nonessential traffic.
# The scrub strips credentials from the env of commands Claude Code runs.
ENV DISABLE_AUTOUPDATER=1 \
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1 \
OPENCODE_DISABLE_AUTOUPDATE=true
# The node base image ships a non-root "node" user (uid 1000).
USER node
WORKDIR /workspace
Turning off nonessential traffic also stops feature-flag fetching, which can make auto the -p default: another reason to always pass --permission-mode. The subprocess scrub narrows what a prompt-injected shell command can read, but the proxy pattern below is stronger.
Run it in a locked-down container
These flags are adapted from Anthropic's secure deployment guide, with a writable workspace and a tmpfs home. The array works in bash and zsh; later sections reuse it.
docker build -t agent-cli .
HARDEN=(--cap-drop ALL --security-opt no-new-privileges --read-only
--tmpfs /tmp:rw,nosuid,size=512m,mode=1777
--tmpfs /home/node:rw,nosuid,size=1g,uid=1000,gid=1000
--memory 4g --cpus 2 --pids-limit 512)
# 1. Install dependencies first, with no API key in the environment.
docker run --rm -v "$PWD":/workspace agent-cli npm ci
# 2. Run the agent. -e VAR passes the host's value without putting it on the command line.
docker run --rm "${HARDEN[@]}" \
-e ANTHROPIC_API_KEY \
-v "$PWD":/workspace \
agent-cli \
claude --bare -p "Run the test suite and fix any failing tests. Change only src/ and test/." \
--permission-mode dontAsk \
--allowedTools "Read,Edit,Bash(npm test *)" \
--max-turns 30 \
--max-budget-usd 5.00 \
--output-format json > result.json
jq -r '.result, .total_cost_usd' result.json
The separate install step matters because dependency lifecycle scripts run arbitrary code, and they shouldn't see your key. Mount a disposable clone or worktree, not your real checkout, and review the diff before merging. The instruction to change only src/ and test/ is a prompt, not a filesystem restriction; enforce narrower access with mounts and permissions if you require it. If your host user isn't uid 1000, the container can't write to the mount and git may reject the repo as "dubious ownership"; fix the ownership or change the uid.
--dangerously-skip-permissions also works here. Claude Code refuses it as root, which is one reason the image uses a non-root user. Still prefer dontAsk with an allowlist: Anthropic's dev container docs warn that in bypass mode a malicious project can exfiltrate anything in the container, including credentials.
Codex CLI in Docker with codex exec
OpenAI's non-interactive mode docs cover codex exec. It prints progress to stderr and only the final message to stdout. --json turns stdout into JSONL events (thread.started, turn.started, turn.completed with token usage, turn.failed, item.*, error). The default sandbox is read-only. --sandbox workspace-write allows edits but keeps the network off unless you add -c 'sandbox_workspace_write.network_access=true'. --full-auto is deprecated. Codex expects a git repository (--skip-git-repo-check overrides that).
Codex's Linux sandbox uses bubblewrap and seccomp, which OpenAI's security docs say may not work inside Docker. Their advice is to make the container the boundary and run with --sandbox danger-full-access, which is what the hardened container is for.
docker run --rm "${HARDEN[@]}" \
-e CODEX_API_KEY \
-v "$PWD":/workspace \
agent-cli \
codex exec --sandbox danger-full-access --ephemeral --json \
"Run the test suite and fix any failing tests. Change only src/ and test/." \
> events.jsonl
For a schema-shaped answer, add --output-schema ./schema.json -o ./summary.json; relative paths land in your mounted directory. codex exec resume --last "<follow-up>" continues a run, which needs the session files: drop --ephemeral and persist ~/.codex on a volume.
Authentication. CODEX_API_KEY works with codex exec, codex review and the TypeScript SDK. OpenAI says to set it only for the invocation that needs it, never job-wide where repository code also runs. Copying a ChatGPT login's ~/.codex/auth.json into a container is an advanced path OpenAI says not to use for public or open-source repositories.
OpenCode server: opencode serve and opencode run --attach
opencode run works one-shot like the others, and OpenCode adds a documented HTTP server on top:
docker run --rm "${HARDEN[@]}" \
-e ANTHROPIC_API_KEY \
-e OPENCODE_CONFIG_CONTENT='{"share":"disabled","permission":{"external_directory":"deny","doom_loop":"deny"}}' \
-v "$PWD":/workspace \
agent-cli \
opencode run --auto --format json "Run the test suite and fix any failing tests."
Add --model provider/model to pick a model. Per OpenCode's permissions docs, the defaults are permissive (most tools allow; only external_directory and doom_loop ask), and --auto approves anything not explicitly denied. OpenCode's documented guardrail is that permission config, so the container is your real boundary. OPENCODE_CONFIG_CONTENT overrides conflicting project settings, but configs are merged: other repo settings, including MCP configuration, remain. Review those settings before running a repo, and use the global --pure flag when you don't need external plugins.
opencode serve runs a headless server, by default on 127.0.0.1:4096, with its OpenAPI 3.1 spec at /doc. OPENCODE_SERVER_PASSWORD turns on HTTP basic auth (username opencode by default). Without it there is no auth, and the API includes endpoints that run shell commands and set provider credentials.
export OPENCODE_SERVER_PASSWORD="$(openssl rand -hex 24)"
docker run -d --name opencode "${HARDEN[@]}" \
-e ANTHROPIC_API_KEY \
-e OPENCODE_SERVER_PASSWORD \
-e OPENCODE_CONFIG_CONTENT='{"share":"disabled","permission":{"external_directory":"deny","doom_loop":"deny"}}' \
-v "$PWD":/workspace \
-p 127.0.0.1:4096:4096 \
agent-cli \
opencode serve --hostname 0.0.0.0 --port 4096
AUTH="opencode:$OPENCODE_SERVER_PASSWORD"
curl -s -u "$AUTH" http://127.0.0.1:4096/global/health
SID=$(curl -s -u "$AUTH" -X POST http://127.0.0.1:4096/session \
-H 'Content-Type: application/json' -d '{"title":"fix-tests"}' | jq -r .id)
curl -s -u "$AUTH" -X POST "http://127.0.0.1:4096/session/$SID/message" \
-H 'Content-Type: application/json' \
-d '{"parts":[{"type":"text","text":"Run the test suite and fix any failing tests."}]}'
# From a machine with opencode installed (reads OPENCODE_SERVER_PASSWORD):
opencode run --attach http://127.0.0.1:4096 --session "$SID" "Summarize what you changed."
The container binds 0.0.0.0 so Docker can forward the port, but the host publishes it on loopback only. For remote access, put it behind TLS and your own authenticating gateway, since basic auth over plain HTTP exposes the password. For long tasks, use POST /session/:id/prompt_async and the GET /event SSE stream. OpenCode's docs note that Anthropic prohibits using Claude Pro/Max subscriptions through OpenCode plugins, so use an API key.
Security checklist for all three
| Risk | Control |
|---|---|
| Agent escalates inside the container | Non-root user, --cap-drop ALL, --security-opt no-new-privileges, read-only root filesystem |
| Runaway processes or memory | --memory, --cpus, --pids-limit, plus --max-turns / --max-budget-usd |
| Secrets in the image or repo | Never ENV or COPY keys into the image. Keep .env, ~/.ssh, ~/.aws, ~/.kube/config, .npmrc and private keys out of the mount |
| Key read by an injected command | CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1, or keep the key outside the container entirely (below) |
| Untrusted repo config runs code | Claude: --bare. OpenCode: review all repo config and use --pure to disable external plugins; inline config only overrides conflicting keys |
| Data exfiltration | Deny-by-default egress (below) |
Keep the model key out of the container. Anthropic's recommended pattern is a proxy outside the agent's boundary: point ANTHROPIC_BASE_URL at a proxy that injects the key and forwards to the API, so the agent never sees the credential. OpenAI's Codex GitHub Action uses a Responses API proxy for the same reason.
Control egress at the network layer. The default Docker bridge allows any outbound connection. For Claude Code with an API key, start the allowlist with api.anthropic.com and add hosts from Anthropic's network requirements only for features you use. Codex and OpenCode need their model provider's API host. Add your git host and package registries only if the agent itself needs them. Enforce it with a cloud firewall, or an internal Docker network whose only exit is an allowlisting proxy, rather than trusting each tool to honor HTTPS_PROXY.
Anthropic's init-firewall.sh and OpenAI's secure devcontainer are reference starting points; read their caveats. A hostname allowlist without TLS inspection can be bypassed with domain fronting, and DNS-based rules need protection against DNS rebinding. An allowed domain plus a broad credential is still an exfiltration path, so use short-lived, repo-scoped git tokens.
Running it beyond your laptop
On a VM
The same image runs on any Linux VM: one container per task from a job runner or systemd unit, in a private subnet whose only egress is your proxy. Anthropic's hosting guide treats 1 GiB RAM, 5 GiB disk and 1 CPU per agent as a floor, so size from measured peaks. Our Claude Agent SDK hosting guide covers the SDK-based version of this setup.
In CI with GitHub Actions
GitHub-hosted runners give you a fresh VM per job, and both vendors ship actions: anthropics/claude-code-action@v1 and openai/codex-action@v1. The Codex action defaults to safety-strategy: drop-sudo and keeps the key behind a proxy. A minimal scheduled Claude run:
name: Nightly test triage
on:
schedule:
- cron: "0 3 * * *"
jobs:
triage:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
- run: npm ci
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: "Run the test suite and summarize any failures"
claude_args: '--max-turns 20 --allowedTools "Bash(npm test *)"'
On sandbox providers
If you'd rather not operate VMs, these providers run the same workload as a service:
| Provider | What you get | Good fit when |
|---|---|---|
| Docker Sandboxes | microVM sandboxes through the sbx CLI, locally or on Docker-managed cloud. Lists Claude Code, Codex and OpenCode as supported agents | You want the same setup on a laptop and in the cloud |
| E2B | SDK-created sandboxes. Pausing saves filesystem and memory, and paused sandboxes are kept until you kill them | Per-task sandboxes you pause and resume |
| Modal Sandboxes | gVisor or VM runtime. Default lifetime 5 minutes, configurable up to 24 hours | Many parallel, bounded jobs |
| Vercel Sandbox | Firecracker microVMs, persistent by default. The default image includes coding agents | You already deploy on Vercel |
| Fly.io Sprites | Persistent Linux machines that sleep when idle, with filesystem checkpoints in about a second | A long-lived "agent computer" per user or repo |
We wrote up our own experience with several of these in we tried every container service, then built our own.
Anthropic's hosted options for Claude Code
For your own team's work, Anthropic can run Claude Code for you. Cloud sessions (claude --cloud "<task>") run on Anthropic-managed VMs. They're available on Pro, Max and Team plans and to Enterprise users with eligible seats, and they need a claude.ai sign-in rather than an API key. Self-hosted environments, in public beta on Team and Enterprise, run those sessions on runners in your network; the control plane stays with Anthropic and the conversation still goes to api.anthropic.com. Both serve your team, not your product's end users. For that, Anthropic offers Claude Managed Agents; see our camelRun vs Claude Managed Agents comparison.
What you still have to build
A working docker run is the easy part. For a service, you still own:
- A queue and concurrency limits. Each task is a CLI process holding RAM for its whole run. Add wall-clock timeouts;
--max-turnsbounds turns, not time. - Persistence. Claude transcripts live under
~/.claude/projects/and Codex sessions under~/.codex. With the tmpfs home above, everything the CLIs write to home disappears with the container, so mount a volume per session if you need resume, and push changes to a branch before exit. - Auth and identity. The CLIs don't know your users, and OpenCode's server has one shared credential. Put your own gateway in front and give each task scoped git credentials.
- Multi-tenancy. One container per tenant or per task. Never share a server or config directory across customers; on a shared host, give each tenant its own
CLAUDE_CONFIG_DIR. - Observability. Capture the
stream-jsonor JSONL events. Claude Code exports OpenTelemetry withCLAUDE_CODE_ENABLE_TELEMETRY=1and theOTEL_*variables; Codex's OTel export is opt-in. - Cost controls.
total_cost_usdis a client-side estimate, and Codex reports token usage per turn. Per-tenant budgets and alerts are yours. - Upgrades. Pin versions and rebuild deliberately; these CLIs ship often.
When you don't need a coding CLI at all
Everything above assumes the agent works on a codebase: read files, run a shell, build and test. Many teams instead want an agent inside their product whose tools are the app's own APIs, such as looking up an order or updating a ticket. A coding CLI in a container is an awkward fit there: you run a Linux box per conversation to get a loop, wire your APIs in as MCP servers, then build everything in the list above.
A hosted agent runtime is a different architecture for that case. It is not a host for these CLIs, and you can't run Claude Code, Codex or OpenCode on it. With camelRun, you define the model, instructions and tools in your own TypeScript or Python code (or serve tools over HTTP, MCP or OpenAPI). camelRun runs the agent loop, keeps each agent's history and files, and wakes agents when there's work. Agents are keyed by your own ids, one per customer, ticket or thread. This is the TypeScript example from the camelRun quickstart, unchanged:
import { Agents, schema, tool } from "@camelai/run";
const agents = new Agents(); // reads CAMELAI_API_KEY
// A tool is an ordinary function: it runs here, in your process.
const weather = tool({
description: "Today's weather in a city",
input: schema.Object({ city: schema.String() }),
execute: ({ city }) => ({ city, forecast: "sunny", highC: 24 }),
});
// The same key is the same agent, with its history, every time you run this.
const agent = await agents.upsert("quickstart", {
instructions: "You are a concise assistant.",
tools: { weather },
});
const run = await agent.run("Should I bring an umbrella in Lisbon today?");
console.log(run.text);
await agents.close();
| Coding CLI in a container | Hosted agent runtime (camelRun) | |
|---|---|---|
| Where the loop runs | The CLI process in your container | camelRun |
| What model-written code can do | Anything a Linux shell can, within your container | JavaScript in a sandbox: no shell, no Python, no package installs, no network |
| Your tools | CLIs and MCP servers inside the container | Functions in your code, or HTTP/MCP/OpenAPI endpoints |
| State | Local disk unless you persist it | Each agent's history and files are kept for you |
| Best for | Changing, building and testing code | Agents inside a product |
Be clear about that trade-off. If your agent needs to build and test code or run arbitrary CLIs, stay with the container approach in this guide. For more on where each layer fits, see agent harness vs framework vs runtime.
Try camelRun
If your agent belongs inside your product rather than in a repo, start with the camelRun quickstart or read more on the camelRun page. It costs $0.01 per agent-hour of active run time, plus model usage, and sleeping agents cost only storage. The runtime is open source (AGPL-3.0, with MIT SDKs), and you can self-host it with one Docker container plus Postgres.