I use Claude Code for everything. Writing code, debugging shit, deploying infrastructure, managing repos, writing articles for this very blog, and even automating browser sessions on the fly. It’s become the backbone of how I work. But giving an AI agent full access to your system is fucking terrifying — not because Claude is malicious, but because it runs with --dangerously-skip-permissions and has the power to do whatever it wants. One bad command and your host is toast.
The obvious answer is: put it in a container. But actually doing that well is a whole other problem. I built claudebox to solve it. What started as a simple containerized Claude Code wrapper has grown into seven different ways to run Claude — each one actually useful, none of them bullshit.
The Rename
This used to be called docker-claude-code, image psyb0t/claude-code, binary claude. It’s now claudebox, image psyb0t/claudebox, binary claudebox. SSH keys moved from ~/.ssh/claude-code to ~/.ssh/claudebox.
If you’re upgrading: uninstall the old binary, pull the new image, re-run the install script. Your ~/.claude config dir and session history survive the rename untouched.
Seven Interfaces, One Container
claudebox isn’t just a wrapper anymore. It’s seven different interfaces to Claude Code running inside Docker:
- Interactive CLI — persistent container, session resumption, the original mode
- Programmatic CLI — non-interactive, works from scripts and CI, its own dedicated container
- HTTP API server — REST API with workspace management, file ops, sync and async runs
- OpenAI-compatible endpoint — drop-in replacement at
/openai/v1/chat/completionswith streaming SSE - MCP server — five tools Claude can use from other agents via the Model Context Protocol
- Telegram bot — per-chat workspaces, file sharing, shell commands from your phone
- Cron scheduler — YAML-defined scheduled jobs with sub-minute resolution, per-job history, and optional Telegram notifications
Install
curl -fsSL https://raw.githubusercontent.com/psyb0t/docker-claudebox/master/install.sh | bashThis generates SSH keys at ~/.ssh/claudebox, pulls the image (always — re-running the installer is the supported way to upgrade), and drops the claudebox binary at /usr/local/bin/claudebox. If you need to pass env vars to the installer, export them on a separate line first — piping VAR=x curl ... | bash doesn’t forward the variable to the script:
export CLAUDEBOX_INSTALL_DIR=/usr/local/bin
curl -fsSL https://raw.githubusercontent.com/psyb0t/docker-claudebox/master/install.sh | bashThen:
claudeboxFirst run prompts for authentication. After that it just works. The wrapper handles the entire container lifecycle — creates a new container if one doesn’t exist for the current directory, restarts and reattaches if one already does.
Image Variants
Full — psyb0t/claudebox:latest
Ubuntu base loaded with everything a developer actually needs: Go with the full toolchain (golangci-lint, gopls, delve), Python 3.12 via pyenv (flake8, black, mypy, pyright, vulture, pytest, poetry), Node.js LTS with the usual ecosystem, C/C++ toolchain, Docker CE with Compose, Terraform, kubectl, helm, GitHub CLI, database clients for SQLite/PostgreSQL/MySQL/Redis, and a pile of utilities (jq, ripgrep, fd-find, bat, shellcheck, shfmt, httpie). The container auto-generates a CLAUDE.md listing every available tool so Claude knows what it has to work with.
Minimal — psyb0t/claudebox:latest-minimal
Just the essentials: Ubuntu, git, curl, wget, jq, Node.js, Docker, Claude Code. Smaller image, faster pull. Claude has passwordless sudo so it installs what it needs on the fly. Use init hooks to pre-bake your setup so you’re not waiting on package installs every fresh container.
Interactive Mode
Run claudebox from any directory and you get a live session. Container persists between runs, session continues from where you left off. Each workspace gets its own container named after the directory path.
Utility commands:
claudebox --version # show version
claudebox doctor # health check
claudebox auth # manage authentication
claudebox setup-token # interactive OAuth token setup
claudebox stop # stop the running container for this workspace
claudebox clear-session # wipe session history, next run starts fresh
claudebox --update # pull the latest image and reinstallSession continuity. claudebox runs Claude with --continue, so it resumes the last conversation from the current directory. Kill the terminal, come back the next day, start it again — Claude picks up exactly where it left off. No session, no problem, it starts fresh.
UID/GID matching. The entrypoint detects the workspace owner and adjusts the container’s user to match. Files created inside the container have correct ownership on the host. No chown -R bullshit.
Programmatic Mode
Pass a prompt and claudebox runs non-interactively. Uses a dedicated _prog container per workspace — separate from the interactive one, no TTY required, works from scripts, cron, other tools:
# basic run
claudebox "explain this codebase"
# pick a model
claudebox "explain this codebase" --model sonnet
claudebox "audit this" --model opus
# output formats
claudebox "list all TODOs" --output-format json
claudebox "list all TODOs" --output-format json-verbose | jq .
claudebox "list all TODOs" --output-format stream-json | jq .
# reasoning effort
claudebox "debug this complex issue" --effort high
claudebox "quick question" --effort low
# custom system prompt
claudebox "review this" --system-prompt "You are a security auditor"
claudebox "review this" --append-system-prompt "Focus on SQL injection"
# structured output
claudebox "extract author and title" --output-format json \
--json-schema '{"type":"object","properties":{"author":{"type":"string"},"title":{"type":"string"}}}'
# session control
claudebox "start over" --no-continue
claudebox "keep going" --resume abc123-def456Model aliases: opus (Opus 4.6), sonnet (Sonnet 4.6), haiku (Haiku 4.5), opusplan (Opus for planning + Sonnet for execution), sonnet[1m] (Sonnet with 1M context window). Or pass a full model name to pin a specific version.
Output formats: text (default), json (single result object with cost and token breakdown), json-verbose (same as json but with a turns array showing every tool call, tool result, and assistant message — full visibility into what Claude did), stream-json (NDJSON, one event per line — system init, assistant responses, tool use, tool results, rate limit events, final result).
API Mode
Set CLAUDEBOX_MODE_API=1 to run the container as an HTTP API server. Plug it into a docker-compose stack and other services can talk to Claude over HTTP:
services:
claudebox:
image: psyb0t/claudebox:latest
ports:
- "8080:8080"
environment:
- CLAUDEBOX_MODE_API=1
- CLAUDEBOX_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.sockEndpoints:
- POST /run — send a prompt, get a result. Fields:
prompt,workspace,model,system_prompt,append_system_prompt,json_schema,effort,no_continue,resume. Returns 409 if the workspace is already processing. - POST /run with
"async": true— returns arunIdimmediately. Poll GET /run/result?runId=X until it completes. Fire and forget. - POST /run/cancel — kill the running process for a workspace
- GET /files/{path} — list directory or download file
- PUT /files/{path} — upload a file (parent dirs created automatically)
- DELETE /files/{path} — delete a file
- GET /health — health check, no auth required
- GET /status — show which workspaces are currently busy
All paths are relative to /workspaces. Auth via Bearer token in Authorization header — set CLAUDEBOX_MODE_API_TOKEN to enable it. Busy workspace tracking returns 409 Conflict so you don’t accidentally queue up overlapping runs on the same workspace.
OpenAI-Compatible Endpoint
POST /openai/v1/chat/completions — a drop-in OpenAI adapter that routes requests to Claude Code inside the container. Works with anything that speaks the OpenAI API: LiteLLM, Open WebUI, custom clients, whatever.
curl http://localhost:8080/openai/v1/chat/completions \
-H "Authorization: Bearer your-secret-token" \
-H "Content-Type: application/json" \
-d '{
"model": "sonnet",
"messages": [{"role": "user", "content": "explain this codebase"}],
"stream": true
}'Streaming works via Server-Sent Events. Multi-turn conversations work — pass the full message history and Claude maintains context. Multimodal works too — send base64 image content in the message and Claude can see it.
Custom headers to control behavior:
X-Claude-Workspace— which workspace to run inX-Claude-Continue— whether to continue the previous sessionX-Claude-Append-System-Prompt— append extra instructions to the system prompt
For LiteLLM, point it at http://your-host:8080/openai/v1 as a custom OpenAI provider and it works without any special configuration.
Hardening pass. The OpenAI adapter got a real test suite and a security audit: an SSRF guard rejects requests that try to smuggle internal URLs through workspace fields, finish_reason values are mapped properly so OpenAI clients see stop/length/tool_calls instead of garbage, multi-turn conversations correctly stick to the same workspace across follow-ups, and unsupported request fields now return 400 instead of being silently dropped. Backed by 24 unit tests and 3 integration tests so the surface stays honest as it grows.
MCP Server
Enable the MCP server at /mcp/ to let other agents and tools call into your Claude container via the Model Context Protocol. Five tools exposed:
- claude_run — run a prompt in a workspace, get the result back
- list_files — list files in a workspace directory
- read_file — read a file from a workspace
- write_file — write a file to a workspace
- delete_file — delete a file from a workspace
This means other Claude instances, custom agents, or any MCP-compatible client can use your claudebox instance as a tool — delegating work to a fresh Claude session with full file access.
Telegram Mode
Set CLAUDEBOX_MODE_TELEGRAM=1 and you get a Telegram bot that talks to Claude. Each chat gets its own workspace and settings. Send text, files, photos, videos, voice messages. Run shell commands with /bash. Get files back with /fetch.
Configuration lives in a YAML file with per-chat model, effort, workspace, system prompt, and budget:
# ~/.claude/telegram.yml
allowed_chats:
- 123456789
- -987654321
default:
model: sonnet
effort: high
continue: true
chats:
123456789:
workspace: my-project
model: opus
effort: max
system_prompt: "You are a senior engineer"
max_budget_usd: 5.00
-987654321:
workspace: team-stuff
model: sonnet
allowed_users:
- 123456789services:
claudebox-telegram:
image: psyb0t/claudebox:latest
environment:
- CLAUDEBOX_MODE_TELEGRAM=1
- CLAUDEBOX_TELEGRAM_BOT_TOKEN=123456:ABC-DEF
- CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-xxx
volumes:
- ~/.claude:/home/claude/.claude
- ~/telegram-workspaces:/workspaces
- /var/run/docker.sock:/var/run/docker.sockBot commands:
- any text → sent to Claude as a prompt
- send a file/photo/video/voice → saved to workspace, caption becomes the prompt
/model [name]— show current model with selectable buttons, or set directly:haiku,sonnet,opus,opusplan,reset/effort [level]— show/select effort:low,medium,high,xhigh,max,reset/system_prompt [text]— show, set, or reset the system prompt override for this chat/append_system_prompt [text]— same for the appended system prompt/bash <command>— run a shell command in the workspace/fetch <path>— send a workspace file back as a Telegram attachment/cancel— kill the running Claude process for this chat/status— show which chats currently have running processes/config— display this chat’s current configuration/reload— hot-reload the YAML config without restarting the container
Claude can push files back by putting [SEND_FILE: path] in its response. Images come through as photos, videos as videos, everything else as documents. Long responses are automatically split across multiple messages.
Markdown rendering. The bot translates Claude’s markdown output into Telegram’s HTML flavor before sending — bold, italic, inline code, code blocks, blockquotes, headings, lists, and links all render natively in the chat. No more raw **asterisks** and backticks polluting your messages. NUL bytes in tool output get mapped to a private-use area placeholder so they survive the round-trip to Telegram without truncating the message.
Cron Mode
Set CLAUDEBOX_MODE_CRON=1 and point CLAUDEBOX_MODE_CRON_FILE at a YAML file to run scheduled Claude jobs. Standard 5-field cron for minute resolution, or 6-field for sub-minute — */30 * * * * * fires every 30 seconds.
model: haiku # default model for all jobs
append_system_prompt: |
The current date and time is {system_datetime}.
telegram_chat_id: -1001234567890 # optional: post results to this Telegram chat
jobs:
- name: hourly_check
schedule: "0 * * * *"
instruction: |
Look at the git log for the last hour. Summarize commits.
- name: every_30_seconds
schedule: "*/30 * * * * *" # 6-field for sub-minute
model: sonnet
instruction: Write the current UTC timestamp to ./status.txt.
- name: nightly_cleanup
schedule: "0 3 * * *"
model: opus
system_prompt: |
You are a cleanup agent. Current time: {system_datetime}.
instruction: |
Find files older than 7 days under ./tmp and delete them.Template variables available in instruction, system_prompt, and append_system_prompt: {system_datetime} (current UTC datetime) and {job_name} (the job’s name field). Per-job model, system_prompt, and append_system_prompt override the root defaults.
services:
claudebox-cron:
image: psyb0t/claudebox:latest
environment:
- CLAUDEBOX_MODE_CRON=1
- CLAUDEBOX_MODE_CRON_FILE=/home/claude/.claude/cron.yaml
- CLAUDEBOX_WORKSPACE=/workspace
- CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-xxx
volumes:
- ./cron.yaml:/home/claude/.claude/cron.yaml:ro
- ./workspace:/workspace
- ~/.claude:/home/claude/.claude
- /var/run/docker.sock:/var/run/docker.sockThe scheduler runs in the foreground — docker logs shows every tick. If a job is still running when the next tick fires, that tick is skipped. Job history streams to ~/.claude/cron/history/<workspace-slug>/<timestamp>-<job-name>/ as activity.jsonl, stderr.log, and meta.json.
Set telegram_chat_id (root or per-job) plus CLAUDEBOX_TELEGRAM_BOT_TOKEN to get Claude’s result posted to Telegram after each job finishes. The Telegram bot doesn’t need to be running — cron uses the token directly.
Per-job reasoning effort. Set effort at the root for a default and override per job — same scale as the CLI (low, medium, high, xhigh, max). Cheap models for cheap jobs, max effort for the gnarly nightly audit.
Prior-run history, auto-injected. Every cron tick now appends a system block telling Claude where its previous runs live — the history root, the workspace history directory, and the per-job history directory (~/.claude/cron/history/<workspace-slug>/*-<job-name>/). Claude doesn’t read them eagerly; it gets the paths and decides whether to Glob or Read when the job actually calls for trend analysis or regression detection. First-ever run skips the hint because there’s nothing yet. This unlocks “compare to last week”, “did this metric regress”, “what changed since yesterday’s run” without wiring any of it per-job. Combined with telegram_chat_id you get a daily digest agent that actually knows what it said yesterday.
Customization
Always-Active Skills
Drop SKILL.md files into ~/.claude/.always-skills/ and they get auto-injected into every claudebox invocation — interactive, programmatic, API, Telegram, cron, all of them. Persistent context that follows Claude into every session without touching individual project CLAUDE.md files.
Init Hooks
Scripts in ~/.claude/init.d/*.sh run once on first container create, as root, before dropping to the claude user. They don’t run again on subsequent docker start — only on fresh containers. Use this for extra package installs or one-time setup:
mkdir -p ~/.claude/init.d
cat > ~/.claude/init.d/setup.sh << 'EOF'
#!/bin/bash
apt-get update && apt-get install -y some-package
pip install some-library
EOF
chmod +x ~/.claude/init.d/setup.shCustom Scripts
Drop executables into ~/.claude/bin/ on the host and they're in PATH inside every container. Persists across all sessions, all workspaces.
Environment Variable Forwarding
Use the CLAUDEBOX_ENV_ prefix to pass arbitrary env vars into the container — the prefix is stripped:
CLAUDEBOX_ENV_GITHUB_TOKEN=xxx CLAUDEBOX_ENV_MY_VAR=hello claudebox "do stuff"Extra Volume Mounts
CLAUDEBOX_MOUNT_ prefix to mount additional directories:
# Mount at same path on both sides
CLAUDEBOX_MOUNT_DATA=/data claudebox "process the data"
# Explicit source:dest
CLAUDEBOX_MOUNT_STUFF=/host/path:/container/path claudebox "do stuff"
# Read-only
CLAUDEBOX_MOUNT_RO=/data:/data:ro claudebox "read the data"The Workspace Model
claudebox creates two containers per workspace: claude-<path> for interactive sessions and claude-<path>_prog for programmatic runs. They don't share state and can run simultaneously. Interactive session doesn't block your scripts. Scripts don't interrupt your session.
The ~/.claude directory mounts into every container — configuration, API keys, always-skills, init hooks, custom scripts all shared across workspaces. SSH keys from ~/.ssh/claudebox mount in automatically. Isolation is at the workspace level, identity is shared.
Docker socket mounts through so Claude can build images, spin up compose stacks, manage containers from inside its own container. Since the workspace mounts at its real host path ($PWD:$PWD), volume mounts from inside Claude resolve correctly on the host. Claude writes a docker-compose.yml, runs it, the paths work.
Security Model
This runs with --dangerously-skip-permissions. Claude has passwordless sudo inside the container and can do whatever it wants.
The security boundary is the container. Claude can't touch your host filesystem beyond the mounted workspace and ~/.claude config. If it goes rogue, docker stop and docker rm and it's gone. Spin up a fresh one in seconds.
The Docker socket mount is the exception — it gives access to the host Docker daemon. Don't mount it if that concerns you. Everything else is contained.
SSH keys live in ~/.ssh/claudebox — a dedicated keypair generated during install. Your personal keys never enter the container. Configure which key is used via CLAUDEBOX_SSH_DIR if needed.
API mode auth via Bearer token in the Authorization header. Set CLAUDEBOX_MODE_API_TOKEN. If you don't set it, the API runs unauthenticated — fine for local use, bad idea exposed to a network.
The Bottom Line
I run every Claude session inside claudebox now. Seven modes, zero host pollution. The isolation means I don't think twice about letting it install packages, rewrite configs, or run whatever commands it needs to get the job done. The session continuity means I close my terminal and come back hours later and pick up exactly where I left off. The API and OpenAI endpoint mean I can wire Claude into other services without writing glue code. The Telegram bot means I can kick off a task from my phone while I'm away from my desk. The cron scheduler means Claude is working while I sleep.
Go grab it: github.com/psyb0t/docker-claudebox
Licensed under WTFPL — because the only thing more dangerous than an AI with root access is an AI with root access and a restrictive license.