claude but dockerized, goth-approved, and dangerously executable. This container gives you the Claude Code in a fully isolated ritual circle – no cursed system installs required.
Because installing things natively is for suckers. This image is for devs who live dangerously, commit anonymously, and like their AI tools in containers.
Two image variants are available:
Everything pre-installed. Big image, zero setup time.
# default — full image
curl -fsSL .../install.sh | bashJust the bare essentials to run claude (Ubuntu, git, curl, wget, jq, Node.js, Docker). Claude has passwordless sudo so it can install whatever it needs on the fly. Smaller image, faster pull, but first-run setup takes longer.
# install with minimal image
CLAUDE_MINIMAL=1 curl -fsSL .../install.sh | bashUse ~/.claude/init.d/*.sh to pre-install your tools on first container create so you don't have to wait for claude to figure it out.
latest (full) |
minimal |
|
|---|---|---|
| Ubuntu 22.04 | yes | yes |
| git, curl, wget, jq | yes | yes |
| Node.js LTS + npm | yes | yes |
| Docker CE + Compose | yes | yes |
| Claude Code CLI | yes | yes |
| Go 1.25.5 + tools | yes | - |
| Python 3.12.11 + tools | yes | - |
| Node.js dev tools | yes | - |
| C/C++ tools | yes | - |
| DevOps (terraform, kubectl, helm, gh) | yes | - |
| Database clients | yes | - |
| Shell utilities (ripgrep, bat, etc.) | yes | - |
- Go 1.25.5 with full toolchain (golangci-lint, gopls, delve, staticcheck, gofumpt, gotests, impl, gomodifytags)
- Latest Node.js with comprehensive dev tools (eslint, prettier, typescript, ts-node, yarn, pnpm, nodemon, pm2, framework CLIs, newman, http-server, serve, lighthouse, storybook)
- Python 3.12.11 via pyenv with linters, formatters, testing (flake8, black, isort, autoflake, pyright, mypy, vulture, pytest, poetry, pipenv)
- Python libraries pre-installed (requests, beautifulsoup4, lxml, pyyaml, toml)
- Docker CE with Docker Compose (full containerization chaos)
- DevOps tools (terraform, kubectl, helm, gh CLI)
- System utilities (jq, tree, ripgrep, bat, exa, fd-find, silversearcher-ag, htop, tmux)
- Shell tools (shellcheck, shfmt)
- C/C++ tools (gcc, g++, make, cmake, clang-format, valgrind, gdb, strace, ltrace)
- Database clients (sqlite3, postgresql-client, mysql-client, redis-tools)
- Editors (vim, nano)
- Archive tools (zip, unzip, tar)
- Networking tools (net-tools, iputils-ping, dnsutils)
git+curl+wget+httpie+ Claude Code- Auto-Git config based on env vars
- Auto-generated
CLAUDE.mdin workspace (lists all available tools for Claude's awareness) - Startup script that configures git, updates claude, and runs with
--dangerously-skip-permissions --continue(falls back to fresh session if no conversation to continue) - Auto-updates claude on interactive startup (skip with
--no-update), background auto-updater disabled - Workspace trust dialog is automatically pre-accepted (no annoying prompts)
- Programmatic mode support — just pass a prompt and optional
--output-format(-pis added automatically) - Custom scripts via
~/.claude/bin— drop executables there and they're in PATH inside the container - Init hooks via
~/.claude/init.d/*.sh— run once on first container create (not on subsequent starts) - Debug logging (
DEBUG=true) with timestamps in both wrapper and entrypoint
- Docker installed and running
There's an install script that sets everything up automatically:
curl -fsSL https://raw.githubusercontent.com/psyb0t/docker-claude-code/master/install.sh | bashTo install the minimal image instead of full:
CLAUDE_MINIMAL=1 curl -fsSL https://raw.githubusercontent.com/psyb0t/docker-claude-code/master/install.sh | bashTo install as a different binary name (e.g. to avoid collision with a native claude install):
# as argument
curl -fsSL .../install.sh | bash -s -- dclaude
# or via env var
CLAUDE_BIN_NAME=dclaude curl -fsSL .../install.sh | bashOr if you prefer manual control:
mkdir -p ~/.claude
mkdir -p "$HOME/.ssh/claude-code"ssh-keygen -t ed25519 -C "claude@claude.ai" -f "$HOME/.ssh/claude-code/id_ed25519" -N ""Then add the public key ($HOME/.ssh/claude-code/id_ed25519.pub) to your GitHub account or wherever you push code.
docker pull psyb0t/claude-code:latest
# or for minimal:
docker pull psyb0t/claude-code:latest-minimalFrom here, check install.sh to see how the wrapper script works if you want to wire it up yourself.
Set these on your host machine (e.g. in ~/.bashrc or ~/.zshrc). The wrapper script forwards them to the container.
| Variable | What it does | Default |
|---|---|---|
CLAUDE_GIT_NAME |
Git commit name inside the container | (none) |
CLAUDE_GIT_EMAIL |
Git commit email inside the container | (none) |
ANTHROPIC_API_KEY |
API key for authentication | (none) |
CLAUDE_CODE_OAUTH_TOKEN |
OAuth token for authentication | (none) |
CLAUDE_DATA_DIR |
Custom .claude data directory (config, sessions, auth, plugins) |
~/.claude |
CLAUDE_SSH_DIR |
Custom SSH key directory | ~/.ssh/claude-code |
CLAUDE_INSTALL_DIR |
Custom install path for the wrapper script (install-time only) | /usr/local/bin |
CLAUDE_BIN_NAME |
Custom binary name (install-time only) | claude |
CLAUDE_ENV_* |
Forward custom env vars to the container (prefix is stripped) | (none) |
CLAUDE_MOUNT_* |
Mount extra volumes (path alone = same path in container, or src:dest) |
(none) |
DEBUG |
Enable debug logging with timestamps in wrapper and entrypoint | (none) |
Set these directly on the container (e.g. in docker-compose). Not used by the wrapper script.
| Variable | What it does | Default |
|---|---|---|
CLAUDE_MODE_API |
Set to 1 to run as HTTP API server instead of interactive/programmatic |
(none) |
CLAUDE_MODE_API_PORT |
Port for the API server | 8080 |
CLAUDE_MODE_API_TOKEN |
Bearer token to require for API requests (optional) | (none) |
To set wrapper vars, export them on your host:
export CLAUDE_GIT_NAME="Your Name"
export CLAUDE_GIT_EMAIL="your@email.com"If not set, git inside the container won't have a default identity configured.
Either log in interactively or set up a long-lived OAuth token:
# generate an OAuth token (interactive, one-time setup)
claude setup-token
# then use it for programmatic runs
CLAUDE_CODE_OAUTH_TOKEN=xxx claude "do stuff"
# or use an API key
ANTHROPIC_API_KEY=sk-ant-xxx claude "do stuff"Use the CLAUDE_ENV_ prefix to forward arbitrary env vars into the container. The prefix is stripped:
# GITHUB_TOKEN=xxx and MY_VAR=hello will be set inside the container
CLAUDE_ENV_GITHUB_TOKEN=xxx CLAUDE_ENV_MY_VAR=hello claude "do stuff"Use the CLAUDE_MOUNT_ prefix to mount additional directories into the container:
# mount at the same path inside the container (just specify the host path)
CLAUDE_MOUNT_DATA=/data claude "process the data"
# mount multiple directories
CLAUDE_MOUNT_1=/opt/configs CLAUDE_MOUNT_2=/var/logs claude "check logs"
# explicit source:dest mapping
CLAUDE_MOUNT_STUFF=/host/path:/container/path claude "do stuff"
# read-only mount
CLAUDE_MOUNT_RO=/data:/data:ro claude "read the data"If the value contains :, it's used as-is (docker -v syntax). Otherwise, the path is mounted at the same location inside the container.
# custom .claude data directory
CLAUDE_DATA_DIR=/path/to/.claude claude "do stuff"
# custom SSH key directory
CLAUDE_SSH_DIR=/path/to/.ssh claude "do stuff"
# install to a different directory
CLAUDE_INSTALL_DIR=/usr/bin curl -fsSL .../install.sh | bashclaudeStarts an interactive session. The container is named by directory path and persists between runs — stop/restart instead of attach, with --continue to resume the last conversation. Claude auto-updates on each interactive start. To skip:
claude --no-updateProgrammatic runs never auto-update.
Just pass a prompt — -p is added automatically:
# one-shot prompt with JSON output
claude "explain this codebase" --output-format json
# use a specific model
claude "explain this codebase" --model sonnet
claude "explain this codebase" --model claude-sonnet-4-6
# streaming output piped to jq
claude "list all TODOs" --output-format stream-json | jq .
# plain text output (default)
claude "what does this repo do"
# custom system prompt (replaces default)
claude "review this" --system-prompt "You are a security auditor"
# append to default system prompt
claude "review this" --append-system-prompt "Focus on SQL injection"
# structured output with JSON schema
claude "extract the author and title" --output-format json \
--json-schema '{"type":"object","properties":{"author":{"type":"string"},"title":{"type":"string"}},"required":["author","title"]}'
# set reasoning effort level
claude "debug this complex issue" --effort high
claude "quick question" --effort lowUses its own _prog container (no TTY — works from scripts, cron, other tools). --continue is passed automatically so programmatic runs share session context via the mounted .claude data dir.
Use --model to pick which Claude model to use:
| Alias | Model | Best for |
|---|---|---|
opus |
Claude Opus 4.6 | Complex reasoning, architecture, hard debugging |
sonnet |
Claude Sonnet 4.6 | Daily coding, balanced speed/intelligence |
haiku |
Claude Haiku 4.5 | Quick lookups, simple tasks, high volume |
opusplan |
Opus (planning) + Sonnet (execution) | Best of both worlds |
sonnet[1m] |
Sonnet with 1M context | Long sessions, huge codebases |
You can also use full model names to pin specific versions:
| Full model name | Notes |
|---|---|
claude-opus-4-6 |
Current Opus |
claude-sonnet-4-6 |
Current Sonnet |
claude-haiku-4-5-20251001 |
Current Haiku |
claude-opus-4-5-20251101 |
Legacy |
claude-sonnet-4-5-20250929 |
Legacy |
claude-opus-4-1-20250805 |
Legacy |
claude-opus-4-20250514 |
Legacy (alias: claude-opus-4-0) |
claude-sonnet-4-20250514 |
Legacy (alias: claude-sonnet-4-0) |
claude-3-haiku-20240307 |
Deprecated, retiring April 2026 |
claude "do stuff" --model opus # latest opus
claude "do stuff" --model haiku # fast and cheap
claude "do stuff" --model claude-sonnet-4-5-20250929 # pin to specific versionIf not specified, the model defaults based on your account type (Max/Team Premium → Opus, Pro/Team Standard → Sonnet).
text (default) — plain text response.
json — single JSON object with the result:
{
"type": "result",
"subtype": "success",
"is_error": false,
"result": "the response text",
"num_turns": 1,
"duration_ms": 3100,
"duration_api_ms": 3069,
"total_cost_usd": 0.156,
"session_id": "...",
"usage": { "input_tokens": 3, "output_tokens": 4, "...": "..." },
"modelUsage": { "...": "..." }
}stream-json — newline-delimited JSON (NDJSON), one event per line. Each event has a type field. Here's what a multi-step run looks like (e.g. claude "install cowsay, run it, fetch a URL" --output-format stream-json):
system — first event, session init with tools, model, version, permissions:
{"type":"system","subtype":"init","cwd":"/your/project","session_id":"...","tools":["Bash","Read","Write","Glob","Grep","..."],"model":"claude-opus-4-6","permissionMode":"bypassPermissions","claude_code_version":"2.1.62","agents":["general-purpose","Explore","Plan","..."],"skills":["keybindings-help","debug"],"plugins":[...],"fast_mode_state":"off"}assistant — Claude's responses. Content is an array of text and/or tool_use blocks:
{
"type": "assistant",
"message": {
"model": "claude-opus-4-6",
"role": "assistant",
"content": [{ "type": "text", "text": "I'll install cowsay first." }],
"usage": {
"input_tokens": 3,
"output_tokens": 2,
"cache_read_input_tokens": 22077,
"...": "..."
}
},
"session_id": "..."
}When Claude calls a tool, content contains a tool_use block:
{
"type": "assistant",
"message": {
"model": "claude-opus-4-6",
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_abc123",
"name": "Bash",
"input": {
"command": "sudo apt-get install -y cowsay",
"description": "Install cowsay"
}
}
],
"usage": { "input_tokens": 1, "output_tokens": 26, "...": "..." }
},
"session_id": "..."
}user — tool execution results (stdout, stderr, error status):
{
"type": "user",
"message": {
"role": "user",
"content": [
{
"tool_use_id": "toolu_abc123",
"type": "tool_result",
"content": "Setting up cowsay (3.03+dfsg2-8) ...",
"is_error": false
}
]
},
"session_id": "...",
"tool_use_result": {
"stdout": "Setting up cowsay (3.03+dfsg2-8) ...",
"stderr": "",
"interrupted": false
}
}rate_limit_event — rate limit status check between turns:
{
"type": "rate_limit_event",
"rate_limit_info": {
"status": "allowed",
"resetsAt": 1772204400,
"rateLimitType": "five_hour",
"overageStatus": "allowed",
"isUsingOverage": false
},
"session_id": "..."
}result — final event with summary, cost, usage breakdown per model:
{
"type": "result",
"subtype": "success",
"is_error": false,
"num_turns": 10,
"duration_ms": 60360,
"duration_api_ms": 46285,
"total_cost_usd": 0.203,
"result": "Here's what I did:\n1. Installed cowsay...\n2. ...",
"session_id": "...",
"usage": {
"input_tokens": 12,
"output_tokens": 1669,
"cache_read_input_tokens": 255610,
"cache_creation_input_tokens": 5037
},
"modelUsage": {
"claude-opus-4-6": {
"inputTokens": 12,
"outputTokens": 1669,
"cacheReadInputTokens": 255610,
"costUSD": 0.201
},
"claude-haiku-4-5-20251001": {
"inputTokens": 1656,
"outputTokens": 128,
"costUSD": 0.002
}
}
}A typical multi-step run produces: system → (assistant → user)× repeated per tool call → rate_limit_event between turns → final assistant text → result.
Set CLAUDE_MODE_API=1 to run the container as an HTTP API server instead of interactive/programmatic mode. Useful for integrating Claude into other services via docker-compose.
# docker-compose.yml
services:
claude:
image: psyb0t/claude-code:latest
ports:
- "8080:8080"
environment:
- CLAUDE_MODE_API=1
- CLAUDE_MODE_API_TOKEN=your-secret-token
- CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-xxx
volumes:
- ~/.claude:/home/claude/.claude
- /your/projects:/workspaces
- /var/run/docker.sock:/var/run/docker.sockPOST /run — run a prompt and return JSON:
curl -X POST http://localhost:8080/run \
-H "Authorization: Bearer your-secret-token" \
-H "Content-Type: application/json" \
-d '{"prompt": "what does this repo do", "workspace": "myproject"}'Request body:
| Field | Type | Description | Default |
|---|---|---|---|
prompt |
string | The prompt to send | required |
workspace |
string | Subpath under /workspaces (e.g. myproject → /workspaces/myproject) |
/workspaces |
model |
string | Model to use (same aliases as CLI) | account default |
system_prompt |
string | Replace the default system prompt entirely | (none) |
append_system_prompt |
string | Append to the default system prompt | (none) |
json_schema |
string | JSON Schema for structured output (result in structured_output field) |
(none) |
effort |
string | Reasoning effort level (low, medium, high, max) |
(none) |
Response is always application/json — same format as --output-format json.
If the workspace is already processing a request, returns 409 Conflict — the client should retry.
GET /files/{path} — list a directory or download a file:
# list root workspace
curl "http://localhost:8080/files" -H "Authorization: Bearer your-secret-token"
# list a subdirectory
curl "http://localhost:8080/files/myproject/src" \
-H "Authorization: Bearer your-secret-token"
# download a file
curl "http://localhost:8080/files/myproject/src/main.py" \
-H "Authorization: Bearer your-secret-token"Directory listing returns {"path": "myproject/src", "entries": [{"name": "foo.py", "type": "file", "size": 1234}, {"name": "lib", "type": "dir"}]}.
PUT /files/{path} — upload a file (creates parent dirs automatically):
curl -X PUT "http://localhost:8080/files/myproject/src/main.py" \
-H "Authorization: Bearer your-secret-token" \
--data-binary @main.pyDELETE /files/{path} — delete a file:
curl -X DELETE "http://localhost:8080/files/myproject/src/old.py" \
-H "Authorization: Bearer your-secret-token"All paths are relative to /workspaces. Path traversal outside root is blocked.
GET /health — health check (no auth required):
curl http://localhost:8080/healthGET /status — show which workspaces are busy:
curl http://localhost:8080/status -H "Authorization: Bearer your-secret-token"POST /run/cancel — kill a running claude process:
curl -X POST "http://localhost:8080/run/cancel?workspace=myproject" \
-H "Authorization: Bearer your-secret-token"Drop executables into ~/.claude/bin/ on the host and they're in PATH inside every container session:
mkdir -p ~/.claude/bin
echo '#!/bin/bash
echo "hello from custom script"' > ~/.claude/bin/my-tool
chmod +x ~/.claude/bin/my-tool
# now available inside the container
claude # my-tool is in PATHScripts in ~/.claude/init.d/*.sh run once on first container create (as root, before dropping to claude user). They don't run again on subsequent docker start — only on fresh docker run after a container is removed.
mkdir -p ~/.claude/init.d
cat > ~/.claude/init.d/setup-my-tools.sh << 'EOF'
#!/bin/bash
apt-get update && apt-get install -y some-package
pip install some-library
EOF
chmod +x ~/.claude/init.d/setup-my-tools.shUseful for installing extra packages, configuring services, or any one-time setup that should survive container restarts but re-run on fresh containers.
- This tool uses
--dangerously-skip-permissions. Because Claude likes to live fast and break sandboxes. - SSH keys are mounted to allow commit/push shenanigans. Keep 'em safe, goblin.
- The host directory is mounted at its exact path inside the container (e.g.
/home/you/projectstays/home/you/project). This means docker volume mounts from inside Claude will use correct host paths. - The container user's UID/GID is automatically matched to the host directory owner, so file permissions just work.
- Docker socket is mounted so Claude can spawn containers within containers. Docker-in-Docker madness enabled.
- Workspace trust dialog is pre-accepted automatically — no confirmation prompts on startup.
- Two container types per workspace:
claude-_path(interactive, with TTY),claude-_path_prog(programmatic, no TTY). Programmatic runs without TTY so they work from scripts, cron jobs, and other tools. ~/.claude/binis in PATH inside the container. Drop custom scripts there and they're available in every session.
WTFPL – do what the fuck you want to.