Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ CONTEXT7_API_KEY=your_context7_api_key_here
# Get your API key from https://www.perplexity.ai/settings/api
PERPLEXITY_API_KEY=your_perplexity_api_key_here

# Permission bypass (optional, OFF by default). Default is auto mode (autonomous +
# background classifier). Set to 1 to add --dangerously-skip-permissions instead
# (full bypass, no in-app safety checks) — for isolated/throwaway containers only.
# CLAUDE_BYPASS_PERMISSIONS=1

# Remote Control (optional, OFF by default). Set to 1 to add --remote-control to
# the entrypoint. NOTE: it needs a full-scope login token (`claude auth login`);
# the inference-only CLAUDE_CODE_OAUTH_TOKEN above cannot drive Remote Control.
Expand Down
34 changes: 25 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ The container implements defense-in-depth:

Claude Code configuration is pre-configured in the container:
- `claude-config.json` - Main Claude configuration with all permissions enabled
- `settings.local.json` - Local settings copied to `~/.claude/` and `/workspace/.claude/`
- `settings.local.json` - Local settings copied to `~/.claude/settings.local.json` (statusLine + permissions)
- `settings.json` - User settings copied to `~/.claude/settings.json`: `permissions.defaultMode: "auto"` + `advisorModel: "opus"` (auto mode must be in user-home, not project scope)
- All permissions are auto-accepted for jailfree operation mode

## Common Commands
Expand Down Expand Up @@ -216,6 +217,9 @@ pnpm 11 vs Node 20 mismatch was caught before the base was bumped to Node 22).

**Runtime variables** (set when running container):
- `CLAUDE_CODE_OAUTH_TOKEN` - OAuth token for Claude Code authentication (required)
- `CLAUDE_BYPASS_PERMISSIONS` - set to `1` to add `--dangerously-skip-permissions` to the entrypoint
(off by default; default is auto mode). Full bypass, no in-app safety checks — for isolated/throwaway
containers only.
- `CLAUDE_REMOTE_CONTROL` - set to `1` to add `--remote-control` to the entrypoint (off by default).
Requires a full-scope login token (`claude auth login`); the inference-only `CLAUDE_CODE_OAUTH_TOKEN`
cannot drive Remote Control, so this is a no-op with it.
Expand All @@ -228,21 +232,32 @@ pnpm 11 vs Node 20 mismatch was caught before the base was bumped to Node 22).
- `CODEGRAPH_NO_DOWNLOAD=1` (forbids CodeGraph's runtime binary download from GitHub Releases; binary must come from the npm registry)
- `NODE_ENV=production`, plus security limits (`RLIMIT_CORE=0`, `RLIMIT_NOFILE=1024`, `YAMA_PTRACE_SCOPE=1`)

**Pre-configured behavior** (`claude-config.json`, copied to `~/.claude.json`):
- Full permission bypass for `/workspace`: `dangerouslySkipPermissions`, `autoAcceptPermissions`,
`defaultMode: acceptEdits`, all tools + `mcp__*` allowed
**Pre-configured behavior:**
- **Permission mode = `auto`** (set in `~/.claude/settings.json`, NOT `claude-config.json`). `auto`
must live in user-home `settings.json` — Claude Code (v2.1.142+) ignores `defaultMode: "auto"` from
project-scope settings. `claude-config.json` (`~/.claude.json`) is deliberately stripped of all
mode/auto-accept-forcing keys (`dangerouslySkipPermissions`, `autoAcceptPermissions`,
`defaultPermissionMode`, project `permissions.defaultMode`, `autoAccept*`) so settings.json is the
single source of mode. Trust/onboarding keys (`hasTrustDialogAccepted`, `hasCompletedOnboarding`,
`autoTrustNewProjects`, `suppressTrustPrompts`, `bypassPermissionsModeAccepted`) are kept to avoid
first-run dialogs in headless.
- **`advisorModel: "opus"`** (in `settings.json`) — Claude consults Opus at decision points (Anthropic
API only). No-op if the main model outranks Opus (e.g. Fable).
- `claude-config.json` still pre-approves tools for `/workspace` (`allow` rules + `mcp__*`); note auto
mode drops blanket `Bash(*)`/`Agent` rules at runtime (the classifier takes over).
- `alwaysThinkingEnabled: true`, `autoUpdates: false`
- The `typescript-lsp@claude-plugins-official` plugin is enabled (works with `ENABLE_LSP_TOOL=1`)
- The OAuth account is hard-coded in this file — replace it if using a different account
- The OAuth account is hard-coded in `claude-config.json` — replace it if using a different account

## Development Workflow (single read-write mode)

1. **Run from your project**: `cd /path/to/repo && ./run_claude.sh`. The current directory is mounted
**read-write** at `/workspace`; the container runs as your host user (`--user $(id -u):$(id -g)`).
2. **Entrypoint**: `claude --dangerously-skip-permissions`; the entrypoint first
copies the baked agent state from `/home/claude` into the writable tmpfs HOME. Remote Control is
opt-in: `--remote-control` is added only when `CLAUDE_REMOTE_CONTROL=1` (it needs a full-scope
login token; the inference-only `CLAUDE_CODE_OAUTH_TOKEN` cannot drive it).
2. **Entrypoint**: `claude` (auto mode via settings.json); the entrypoint first copies the baked
agent state from `/home/claude` into the writable tmpfs HOME. Two opt-in flags, each added only
when its env var is set: `CLAUDE_BYPASS_PERMISSIONS=1` → `--dangerously-skip-permissions` (full
bypass for isolated containers); `CLAUDE_REMOTE_CONTROL=1` → `--remote-control` (needs a
full-scope `claude auth login` token; the inference-only `CLAUDE_CODE_OAUTH_TOKEN` cannot drive it).
3. **Autonomous agent**: Claude edits/commits the project directly in `/workspace`. For `git push`,
set `DEPLOY_KEY=/path/to/repo_deploy_key` (scoped, read-only mounted). Commit identity comes from
your host `git config` (passed as env).
Expand Down Expand Up @@ -385,6 +400,7 @@ Inside the debug shell, you can run diagnostics manually:
- `tools/package-lock.json` - Lockfile (sha512 integrity) for the toolchain; installed via `npm ci`. Regenerate inside node:22 after editing package.json
- `claude-config.json` - Claude Code configuration with all permissions
- `settings.local.json` - Local Claude settings; also wires the `statusLine` to `/usr/local/bin/claude-statusline.sh`
- `settings.json` - User Claude settings baked to `~/.claude/settings.json`: `permissions.defaultMode: "auto"` + `advisorModel: "opus"`
- `statusline-command.sh` - Claude Code statusLine script (compact line: dir, git branch/dirty, model, duration, context %, 5h/7d rate limits); baked to `/usr/local/bin/claude-statusline.sh` (fixed, HOME-independent path). Deps (jq, git, awk, date, grep) are all present in the image
- `mcp-servers.json` - Base MCP server configurations (always installed)
- `mcp-servers-optional.json` - Optional MCP servers (require API keys)
Expand Down
34 changes: 23 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ COPY --chown=${USER_NAME}:${USER_NAME} claude-config.json /home/${USER_NAME}/.cl
# COPY --chown creates the .claude dir owned by the user (a root `mkdir` here would
# leave it root-owned and break the later openspec/rtk init under USER claude).
COPY --chown=${USER_NAME}:${USER_NAME} settings.local.json /home/${USER_NAME}/.claude/settings.local.json
# User-level settings.json: permission defaultMode "auto" + advisorModel. MUST be
# user-home (~/.claude/settings.json) — Claude Code (v2.1.142+) ignores defaultMode
# "auto" from project-scope settings, so this never goes to /workspace/.claude.
COPY --chown=${USER_NAME}:${USER_NAME} settings.json /home/${USER_NAME}/.claude/settings.json
# Statusline command for Claude Code (compact line: dir, git, model, context, rate
# limits). Installed at a fixed, HOME-independent path so settings.local.json's
# statusLine.command works regardless of the runtime HOME relocation. Deps (jq, git,
Expand Down Expand Up @@ -247,14 +251,18 @@ RUN rtk init -g --auto-patch
RUN npx -y github:JuliusBrussee/caveman#v1.9.0 --non-interactive --only claude --no-mcp-shrink

# Create simple startup script for runtime.
# Permission mode: default is `auto` (set in ~/.claude/settings.json) — autonomous
# with a background classifier, NOT the old `--dangerously-skip-permissions`
# bypass. Auto mode engages only on the Anthropic API with a supported model; if
# unavailable it silently falls back to `default` (prompts). See SECURITY.md.
# --dangerously-skip-permissions: OPT-IN via CLAUDE_BYPASS_PERMISSIONS=1 (off by
# default). Re-adds the full bypass for isolated/throwaway containers where you
# accept no in-app safety checks. The flag is added only when the env var is set,
# so it is never a dead default.
# --remote-control: OPT-IN via CLAUDE_REMOTE_CONTROL=1 (off by default). Remote
# Control requires a full-scope login token (`claude auth login`); the
# long-lived CLAUDE_CODE_OAUTH_TOKEN / `claude setup-token` this image uses is
# inference-only, so RC stays disabled with it. The flag is added only when the
# env var is set, so it is never a dead default. See SECURITY.md.
# --dangerously-skip-permissions: the container boundary is the perimeter (see
# SECURITY.md). NOTE: when enabled, Remote Control opens an outbound control
# channel — covered in the threat model.
# inference-only, so RC stays disabled with it.
RUN echo '#!/bin/bash\n\
set -e\n\
# Runtime starts with --user $(id -u):$(id -g) to match host ownership of the rw\n\
Expand All @@ -266,14 +274,18 @@ if [ "$HOME" != "/home/claude" ] && [ ! -e "$HOME/.claude.json" ]; then\n\
cp -a /home/claude/. "$HOME/" 2>/dev/null || true\n\
fi\n\
cd /workspace 2>/dev/null || cd "$HOME"\n\
RC_ARGS=""\n\
EXTRA_ARGS=""\n\
mode_msg="permission mode: auto (default; falls back to default mode if auto is unavailable)"\n\
if [ "${CLAUDE_BYPASS_PERMISSIONS:-0}" = "1" ]; then\n\
EXTRA_ARGS="$EXTRA_ARGS --dangerously-skip-permissions"\n\
mode_msg="permission mode: bypass (CLAUDE_BYPASS_PERMISSIONS=1 — no in-app safety checks)"\n\
fi\n\
if [ "${CLAUDE_REMOTE_CONTROL:-0}" = "1" ]; then\n\
RC_ARGS="--remote-control"\n\
echo "Starting Claude Code (Remote Control requested — needs a full-scope login token; see SECURITY.md) in $(pwd)..."\n\
else\n\
echo "Starting Claude Code in $(pwd)..."\n\
EXTRA_ARGS="$EXTRA_ARGS --remote-control"\n\
mode_msg="$mode_msg; Remote Control requested (needs a full-scope login token; see SECURITY.md)"\n\
fi\n\
exec claude --dangerously-skip-permissions $RC_ARGS "$@"\n\
echo "Starting Claude Code in $(pwd) — $mode_msg..."\n\
exec claude $EXTRA_ARGS "$@"\n\
' > /home/${USER_NAME}/start-claude.sh \
&& chmod +x /home/${USER_NAME}/start-claude.sh

Expand Down
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,22 @@ A single mode. The wrapper scripts (`run_claude.sh`, `debug-shell.sh`) apply har
- Footgun guards: the scripts refuse `--privileged`, `docker.sock`, `--pid=host`, `--network=host`,
`--cap-add`, or running as host root

The entrypoint runs `claude --dangerously-skip-permissions` — the container boundary is the
perimeter. Remote Control (driving the in-container agent remotely) is **opt-in**: set
`CLAUDE_REMOTE_CONTROL=1` to add `--remote-control`. Note it needs a full-scope login token
(`claude auth login`); the `CLAUDE_CODE_OAUTH_TOKEN` this image uses is inference-only, so Remote
Control stays disabled with it.
The entrypoint runs `claude` in **auto mode** (`permissions.defaultMode: "auto"` in
`~/.claude/settings.json`) — autonomous, but with a background classifier that blocks dangerous
actions (prompt-injection-driven commands, `curl | bash`, force-push, pushing to `main`, prod
deploys). The OS-level container boundary is still the perimeter. Auto mode engages on the Anthropic
API with a supported model; if it's unavailable for your account it **silently falls back to
`default`** (prompts on each action) — confirm the status bar shows `auto` on first run.

Two opt-in env vars (off by default, each added to the launch only when set):
- `CLAUDE_BYPASS_PERMISSIONS=1` — re-adds `--dangerously-skip-permissions` (full bypass, no in-app
safety checks) for isolated/throwaway containers.
- `CLAUDE_REMOTE_CONTROL=1` — adds `--remote-control` (needs a full-scope `claude auth login` token;
the inference-only `CLAUDE_CODE_OAUTH_TOKEN` cannot drive it).

A stronger **advisor** model (`advisorModel: "opus"`) is configured by default: Claude consults Opus
at decision points (requires the Anthropic API). It's a no-op if you run a main model that outranks
Opus (e.g. `--model fable`, where an Opus advisor is rejected).

> ⚠️ **On a host where the Docker daemon runs as root, `docker run` is equivalent to host root.**
> The wrapper scripts and their guards protect against *accidental* misconfiguration, **not** a
Expand Down
29 changes: 21 additions & 8 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,19 @@ may be root. Consequently:

## Known limitations (by design)

- **The image ENTRYPOINT is permissive** (`--dangerously-skip-permissions`). The
container boundary is the perimeter; in-app permission prompts are disabled. A bare `docker run`
without a wrapper still has no host-level hardening. Always use the wrapper scripts.
- **The image ENTRYPOINT runs in auto mode by default, not full bypass.** `permissions.defaultMode`
is `"auto"` (`~/.claude/settings.json`): the agent runs autonomously, but a background classifier
vets actions and blocks dangerous ones. Full bypass (`--dangerously-skip-permissions`) is now
**opt-in** via `CLAUDE_BYPASS_PERMISSIONS=1`, for isolated/throwaway containers that accept no
in-app checks. The OS-level container boundary is still the perimeter; a bare `docker run` without
a wrapper has no host-level hardening — always use the wrapper scripts. Two by-design auto-mode
behaviors: (a) on entering auto, blanket `Bash(*)` / `Agent` allow rules are dropped (the
classifier takes over; narrow rules carry over); (b) in non-interactive `-p` runs, repeated
classifier blocks abort the session (no user to prompt).
- **Auto mode can silently fall back to `default`.** If the account/model doesn't support auto
(e.g. Team/Enterprise without admin-enabled auto, or a non-API provider), Claude Code starts in
`default` mode with no error — meaning it prompts on most actions. Verify the status bar shows
`auto` on first run; set `CLAUDE_BYPASS_PERMISSIONS=1` if you need unattended operation regardless.
- **Remote Control is opt-in and off by default.** Set `CLAUDE_REMOTE_CONTROL=1` to add
`--remote-control`, which opens an outbound connection to the Remote Control service. It also
requires a **full-scope login token** (`claude auth login`): the long-lived `CLAUDE_CODE_OAUTH_TOKEN`
Expand All @@ -67,8 +77,11 @@ may be root. Consequently:
On a root host this is an exfiltration channel under prompt injection — restrict egress at the
Docker-network/daemon level (cap-drop=ALL prevents in-container iptables) or remove those MCP
servers from `mcp-servers.json`.
- **Prompt injection (the main runtime risk).** With `--dangerously-skip-permissions`, any file in
the project is executable instructions for the autonomous agent. The project is mounted
- **Prompt injection (the main runtime risk).** Any file in the project is potential instructions for
the autonomous agent. In the default **auto mode** the classifier blocks the worst injected actions
(exfiltration to external endpoints, `curl | bash`, force-push, prod deploys) — but it is a research
preview, not a guarantee, and boundaries you state in chat can be lost to context compaction. With
`CLAUDE_BYPASS_PERMISSIONS=1` there are **no** in-app checks at all. The project is mounted
**read-write**, so an injected agent can modify project files, push (if a deploy key is set), and
exfiltrate code via egress. Residual risk is **Medium** with a scoped deploy key (below). **Use on
trusted projects only** and review what the agent commits.
Expand Down Expand Up @@ -100,9 +113,9 @@ Two additional entrypoints exist for IDE use; both change the posture relative t
- **ACP adapter (`run_acp.sh` → `claude-agent-acp`).** The editor (e.g. Zed) launches the container
over stdio and drives it via the Agent Client Protocol. Crucially, this path does **not** pass
`--dangerously-skip-permissions`; tool calls (edits, shell commands) are sent back to the editor as
**permission requests a human approves**. So the ACP path is *less* permissive than the skip-all
agent entrypoint — a human gates actions, which is a genuine deviation from "the container boundary
is the only perimeter." It keeps the same hardening (`cap-drop=ALL`, `no-new-privileges`, bridge
**permission requests a human approves**. So the ACP path is *less* permissive than the autonomous
agent entrypoint — a human gates each action, versus auto mode's background classifier (or full
bypass under `CLAUDE_BYPASS_PERMISSIONS=1`). It keeps the same hardening (`cap-drop=ALL`, `no-new-privileges`, bridge
network, non-root `--user`, tmpfs HOME) with a raised `--pids-limit` (interactive tooling forks
more). The project is bind-mounted **read-write at its host-absolute path** (path coherence for the
editor); the same `DEPLOY_KEY` model gates `git push`. Outbound network and prompt-injection risks
Expand Down
12 changes: 1 addition & 11 deletions claude-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,8 @@
"autoConnectIde": false,
"claudeInChromeDefaultEnabled": false,
"showExpandedTodos": true,
"dangerouslySkipPermissions": true,
"autoAcceptPermissions": true,
"autoTrustNewProjects": true,
"suppressTrustPrompts": true,
"suppressPermissionPrompts": true,
"defaultPermissionMode": "accept",
"customApiKeyResponses": {
"approved": [],
"rejected": []
Expand All @@ -29,7 +25,7 @@
"allow": [
"Bash(*)",
"Edit(*)",
"Read(*)",
"Read(*)",
"Write(*)",
"List(*)",
"Glob(*)",
Expand All @@ -41,24 +37,18 @@
"NotebookWrite(*)",
"mcp__*"
],
"defaultMode": "acceptEdits",
"additionalDirectories": ["/workspace/input", "/workspace/output", "/workspace/data", "/workspace/temp"]
},
"hasCompletedProjectOnboarding": true,
"hasClaudeMdExternalIncludesApproved": false,
"hasClaudeMdExternalIncludesWarningShown": false,
"autoAcceptAllTools": true,
"autoAcceptMcpTools": true,
"history": [],
"mcpContextUris": [],
"mcpServers": {
},
"enabledMcpjsonServers": [],
"disabledMcpjsonServers": [],
"hasTrustDialogAccepted": true,
"hasAllToolsApproved": true,
"autoAcceptEdits": true,
"skipConfirmationDialogs": true,
"projectOnboardingSeenCount": 1,
"thinkingMigrationComplete": true,
"allowWebSearch": true,
Expand Down
6 changes: 6 additions & 0 deletions settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"permissions": {
"defaultMode": "auto"
},
"advisorModel": "opus"
}
Loading