API mode
Run the container as an HTTP API server with workspace management, file operations, and optional authentication. It includes the OpenAI-compatible adapter and can mount an optional MCP server.
# docker-compose.yml
services:
claudebox:
image: psyb0t/claudebox:latest
init: true
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
environment:
- CLAUDEBOX_API_MODE=1
- CLAUDEBOX_API_MODE_TOKEN=${CLAUDEBOX_API_MODE_TOKEN:?set this in .env}
- CLAUDEBOX_AVAILABLE_MODELS=haiku,sonnet,opus,opusplan
- CLAUDE_CODE_OAUTH_TOKEN=${CLAUDE_CODE_OAUTH_TOKEN:?set this in .env}
volumes:
- ./claude-state:/home/aicode/.aicodebox
- /your/projects:/workspace
mem_limit: 2g
cpus: 2
pids_limit: 512
logging:
driver: local
options:
max-size: 10m
max-file: "3"
The example publishes only to loopback and intentionally omits /var/run/docker.sock. Put the API behind a reverse proxy with TLS and authentication when clients are not on the same host. Adding the Docker socket gives Claude host container control.
| Variable | Description | Default |
|---|---|---|
CLAUDEBOX_API_MODE | Set to 1 to start in API server mode | (none) |
CLAUDEBOX_API_MODE_PORT | Port the API server listens on | 8080 |
CLAUDEBOX_API_MODE_TOKEN | Bearer token for API authentication (if unset, no auth is required) | (none) |
CLAUDEBOX_AVAILABLE_MODELS | Comma separated model IDs returned by /openai/v1/models. Claudebox has a built-in list, but set it explicitly to change the advertised models. This is not a model-access restriction. | haiku,sonnet,opus,opusplan |
DEBUG | Set to 1 or true for structured JSON debug logging | (none) |
Legacy
CLAUDE_MODE_API,CLAUDE_MODE_API_PORT,CLAUDE_MODE_API_TOKENare still accepted as fallbacks.
The API server outputs structured JSON logs (timestamp, level, logger, function name, line number, and file) for every request, error, and lifecycle event.
API endpoints
POST /run: send a prompt to Claude Code and get a JSON response:
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"}'
| Field | Type | Description | Default |
|---|---|---|---|
prompt | string | The prompt to send to Claude Code | (required) |
workspace | string | Subpath under /workspace (e.g., myproject resolves to /workspace/myproject) | /workspace |
model | string | Model alias or full model name (see Model Selection) | account default |
systemPrompt | string | Replace the default system prompt entirely | (none) |
appendSystemPrompt | string | Append text to the default system prompt without replacing it | (none) |
jsonSchema | object | A JSON Schema for the final output. The server validates it and retries invalid output; persistent validation failure returns an error | (none) |
thinking | string | Claude reasoning level. low, medium, high, xhigh, and max map to Claude Code’s --effort flag. off or none keep Claude Code’s default. | (none) |
eventMode | string | auto, none, or full. full returns complete native Claude stream records | auto |
outputFormat | string | Legacy compatibility: json-verbose selects full events while eventMode is auto; text and json do not change the response shape | (none) |
noContinue | bool | If true, start a fresh session instead of continuing the previous one | false |
resume | string | Resume a specific session by its session ID | (none) |
fireAndForget | bool | Submit in the background and mark the immediate acknowledgement with fireAndForget: true | false |
extraArgs | string[] | Additional Claude CLI arguments. Only allow trusted callers to set these | (none) |
toolsAllowlist | string[] | Passed as Claude Code’s --allowedTools; not a sandbox or an enforced tool restriction with permission bypass | (none) |
noTools | bool | Disable the agent’s internal tools | false |
timeoutSeconds | integer | Run timeout in seconds | (none) |
includeRaw | bool | Include raw stdout and stderr. Failure diagnostics can include stderr regardless | false |
async | bool | If true, return immediately with a runId and run in the background | false |
Accepted runs receive a runId that identifies the run. Authentication and request-validation errors are not run results.
The Claude adapter always requests its complete native stream, including partial assistant messages, tool use, tool results, hook events, subagent text, and final usage. Set "eventMode": "full" to return those records. Each response record has the stable envelope {sequence, attempt, backend, eventType, event}. The nested event is the original Claude record with no field removal or tool-result truncation. "eventMode": "none" returns only the final result. The default "auto" keeps the historical behavior of including events for a jsonSchema request or "outputFormat": "json-verbose". "outputFormat": "text" and "outputFormat": "json" remain accepted legacy inputs but do not change the response serialization.
jsonSchema validates only the final JSON output and can be combined with either event mode.
/run returns completed application/json, not a live event stream. It returns 409 if that workspace path is already busy. Workspace path checks confine the file API, not the Claude subprocess. The agent can access other files and processes permitted by the container; workspace subpaths do not isolate untrusted tenants.
Async runs: when "async": true is set, the request returns immediately with a run ID:
# fire off an async run
curl -X POST http://localhost:8080/run \
-H "Authorization: Bearer token" \
-H "Content-Type: application/json" \
-d '{"prompt": "refactor this entire codebase", "workspace": "myproject", "async": true}'
# → {"runId": "abc123", "workspace": "/workspace/myproject", "status": "running", "fireAndForget": false}
# poll for the result
curl "http://localhost:8080/run/result?runId=abc123" -H "Authorization: Bearer token"
# while running → {"runId": "abc123", "workspace": "/workspace/myproject", "status": "running"}
# when done → full result JSON with runId + workspace injected (see below)
Completed results are cached until first read: once you fetch a completed result, it is purged from the cache. Results that are never read are automatically purged after 6 hours. Failed and cancelled results are also returned once and purged.
GET /run/result?runId=X: poll for the result of an async (or any) run:
| Status | Response |
|---|---|
running | {"runId": "...", "workspace": "...", "status": "running"} |
completed | Full result JSON with runId and workspace injected (then purged from cache) |
failed | {"runId": "...", "workspace": "...", "status": "failed", "error": "..."} (then purged) |
cancelled | {"runId": "...", "workspace": "...", "status": "cancelled"} (then purged) |
Returns 404 if the run ID is not found (never existed, already read, or expired).
Completed result example:
{
"runId": "abc123",
"workspace": "/workspace/myproject",
"status": "completed",
"exitCode": 0,
"text": "the response text",
"usage": { "input_tokens": 100, "output_tokens": 50 },
"sessionId": "..."
}
GET /files/{path}: list a directory or download a file:
curl "http://localhost:8080/files" -H "Authorization: Bearer token" # list workspace root
curl "http://localhost:8080/files/myproject/src" -H "Authorization: Bearer token" # list a subdirectory
curl "http://localhost:8080/files/myproject/src/main.py" -H "Authorization: Bearer token" # download a file
Directory listing response:
{
"path": "myproject/src",
"entries": [
{ "name": "main.py", "type": "file", "size": 1234 },
{ "name": "utils", "type": "dir" }
]
}
File download returns raw file content with appropriate content type.
PUT /files/{path}: upload a file (parent directories are created automatically):
curl -X PUT "http://localhost:8080/files/myproject/src/main.py" \
-H "Authorization: Bearer token" --data-binary @main.py
# → {"status": "ok", "path": "/workspace/myproject/src/main.py", "size": 1234}
DELETE /files/{path}: delete a file:
curl -X DELETE "http://localhost:8080/files/myproject/src/old.py" -H "Authorization: Bearer token"
# → {"status": "ok", "path": "/workspace/myproject/src/old.py"}
GET /healthz: health check endpoint (no authentication required):
{ "ok": true, "adapter": "claude" }
GET /status: returns busy workspaces and all tracked runs (running, completed, failed, cancelled):
{
"busyWorkspaces": ["/workspace/myproject"],
"runs": [
{
"runId": "abc123",
"workspace": "/workspace/myproject",
"status": "running"
}
]
}
DELETE /run/{runId}: cancel a running Claude process by run ID:
curl -X DELETE "http://localhost:8080/run/abc123" -H "Authorization: Bearer token"
# → {"runId": "abc123", "status": "cancelled"}
All file paths are relative to /workspace. Path traversal attempts outside the workspace root are blocked and return a 400 error.
OpenAI-compatible endpoints
Claudebox adapts the chat/completions protocol to Claude Code. Requests can run the agent’s file and shell tools, subject to the request’s tool settings and the container’s permissions.
GET /openai/v1/models: list available models:
curl http://localhost:8080/openai/v1/models -H "Authorization: Bearer your-secret-token"
# {"object":"list","data":[{"id":"haiku",...},{"id":"sonnet",...},{"id":"opus",...},{"id":"opusplan",...}]}
POST /openai/v1/chat/completions: chat completions (streaming and non-streaming):
# non-streaming
curl -X POST http://localhost:8080/openai/v1/chat/completions \
-H "Authorization: Bearer your-secret-token" \
-H "Content-Type: application/json" \
-d '{"model":"haiku","messages":[{"role":"user","content":"hello"}]}'
# streaming (SSE)
curl -X POST http://localhost:8080/openai/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"haiku","messages":[{"role":"user","content":"hello"}],"stream":true}'
Model names: use the same aliases as the CLI (haiku, sonnet, opus, opusplan). Provider prefixes are stripped automatically: claudebox/haiku becomes haiku, openai/sonnet becomes sonnet.
System messages: messages with role: "system" are extracted and passed to Claude Code as --system-prompt.
Reasoning effort: reasoning_effort maps to Claude Code’s --effort flag. low, medium, high, xhigh, and max pass through, minimal maps to low, and none or off keeps Claude Code’s default. Any other value is rejected. The thinking field on /run and the MCP run_prompt tool follow the same rules.
Client-executed tools: standard tools and tool_choice are supported. A tool-call turn returns OpenAI tool_calls; the client runs the selected function and sends the role: "tool" result in the next request. Internal agent tools default off for that turn. Send x-aicodebox-no-tools: 0 to keep them enabled.
Structured output: standard response_format supports json_object and json_schema. It composes with client-executed tools: tool-call turns are not schema-checked, and the final answer is validated against the requested schema.
Ignored fields: temperature, max_tokens, and unsupported OpenAI fields are accepted without effect.
Message handling:
- Single user message: sent directly as the prompt to Claude Code. This is the fast path with no overhead.
- Multi-turn conversations: the full messages array is serialized to a JSON file in the workspace (
_oai_uploads/conv_<id>.json). Claude Code reads the file and responds to the last user message, preserving the full conversation context. - Multimodal content: base64-encoded images and image URLs in message content are automatically downloaded or decoded and saved to the workspace. The content blocks are replaced with local file paths so Claude Code can access the images directly.
Streaming: with "stream": true, plain chat returns standard SSE in message-level chunks. This adapter extracts completed assistant messages for standard OpenAI text chunks. Requests using client-executed tools or schema-constrained output buffer the result before sending the SSE response. Native partial records remain available through the opt-in extension below.
To receive every Claude native stream record too, add "stream_options": {"include_aicodebox_events": true}. The response emits a named aicodebox.native SSE event before normal OpenAI chunks. Its JSON payload is {sequence, attempt, backend, eventType, event} and does not alter the standard OpenAI chunk data. This remains opt-in so strict OpenAI SSE parsers can continue consuming only standard chunks.
For large inputs or outputs, upload files with PUT /files/..., reference their paths in the prompt, and download the resulting files with GET /files/....
Custom headers for claudebox-specific behavior:
| Header | Description |
|---|---|
X-Aicodebox-Workspace | Workspace subpath under /workspace. X-Claude-Workspace remains an alias. |
X-Aicodebox-Continue | Set to 1, true, or yes to continue the previous session. X-Claude-Continue remains an alias. |
X-Aicodebox-Append-System-Prompt | Text to append to the system prompt. X-Claude-Append-System-Prompt remains an alias. |
X-Aicodebox-Json-Schema | JSON Schema fallback when the request has no response_format. |
X-Aicodebox-Resume | Specific session ID to resume. |
X-Aicodebox-Extra-Args | JSON array or comma-separated CLI arguments. |
X-Aicodebox-Timeout-Seconds | Positive timeout in seconds. |
X-Aicodebox-Tools-Allowlist | JSON array or comma-separated names passed as --allowedTools. Not a security boundary with permission bypass. |
X-Aicodebox-No-Tools | Boolean that disables internal agent tools. |
LiteLLM integration example:
import litellm
response = litellm.completion(
model="claudebox/haiku",
messages=[{"role": "user", "content": "hello"}],
api_base="http://localhost:8080/openai/v1",
api_key="your-secret-token", # or any string if no API token is configured
)
print(response.choices[0].message.content)
MCP server
Set CLAUDEBOX_MCP_MODE=1 to mount MCP at /mcp/ on the API port. Set CLAUDEBOX_MCP_MODE_TOKEN separately; the API token does not protect MCP. The prompt tool is run_prompt, not the old claude_run name. See MCP mode for tools, client configuration, standalone placement, and reverse-proxy allowlists.