~/.claude/CLAUDE.md (already carried into the container) tells agents to
write specs to ~/specs/ instead of a project-local path - that instruction
was already in effect inside the sandbox, but nothing mounted the
directory it points at, so an agent following it would write into a path
that vanishes with the container. Bind-mounted read-write at the fixed
host path ~/specs (auto-created if missing), same pattern as the shared
scratch space but not configurable - it's tied to the CLAUDE.md
convention, not a per-run choice.
Also adds bubblewrap/socat, which Claude Code checks for before offering
its own internal per-command sandboxing and /remote-control. This clears
the "Sandbox disabled: dependencies are missing" warning, but doesn't
actually fix /remote-control: bwrap still can't create a namespace on this
host ("kernel does not allow non-privileged user namespaces"), and even
adding CAP_SYS_ADMIN to the container only got further to a mount-
propagation permission error, not further progress. Making it actually
work would need privilege escalation approaching full --privileged mode,
which would undo most of this session's hardening - not pursued. Kept the
packages anyway since they at least make the startup warning accurate
rather than noise, in case a future decision revisits this tradeoff.
All 22 existing tests still pass.
103 lines
5.3 KiB
YAML
103 lines
5.3 KiB
YAML
# Not meant to be run directly with plain `docker compose up`: WORKSPACE_DIR,
|
|
# SSH_AUTH_SOCK, etc. must point at real host paths for a specific project.
|
|
# Use `bin/claude-sandbox <path-to-project>` instead, which sets these and
|
|
# invokes this file.
|
|
services:
|
|
claude:
|
|
build:
|
|
context: ../..
|
|
dockerfile: docker/claude-sandbox/Dockerfile
|
|
args:
|
|
TZ: ${TZ:-UTC}
|
|
image: claude-sandbox:latest
|
|
cap_add:
|
|
- NET_ADMIN
|
|
- NET_RAW
|
|
environment:
|
|
GITEA_SSH_HOST: ${GITEA_SSH_HOST:-}
|
|
GITEA_API: ${GITEA_API:-}
|
|
GITEA_TOKEN: ${GITEA_TOKEN:-}
|
|
# Read-only fine-grained GitHub PAT (Contents + Metadata: read-only,
|
|
# no write scopes). Used by `gh` and, via `gh auth setup-git`, by git
|
|
# itself for github.com over HTTPS - see the firewall/Dockerfile for
|
|
# why that's the only path GitHub is reachable through at all.
|
|
GH_TOKEN: ${GH_TOKEN:-}
|
|
ALLOWED_DOMAINS: ${ALLOWED_DOMAINS:-}
|
|
# Base URL of a Loki push endpoint (e.g. http://10.0.30.11:3100) to
|
|
# ship DNS-query and firewall allow/reject logs to. Its host is
|
|
# automatically firewall-allowlisted (see entrypoint.sh). Optional -
|
|
# logging is skipped entirely if unset.
|
|
LOKI_URL: ${LOKI_URL:-}
|
|
SSH_AUTH_SOCK: /ssh-agent
|
|
ENABLE_FIREWALL: ${ENABLE_FIREWALL:-1}
|
|
# Docker's own tty allocation defaults to a generic TERM (e.g. plain
|
|
# "xterm", dropping 256-color and any extended-keys terminfo bits),
|
|
# not whatever the host's actual terminal/tmux session is using.
|
|
# Forward the real value so nvim negotiates key sequences (e.g. the
|
|
# Ctrl-H/Backspace disambiguation config/tmux.conf sets up) against
|
|
# the terminfo entry the rest of the chain actually expects.
|
|
TERM: ${TERM:-xterm-256color}
|
|
volumes:
|
|
# Mounted at the same absolute path as on the host (rather than a
|
|
# fixed /workspace) so Claude Code's project/session-history keying
|
|
# (based on cwd) lines up with the host, letting a session started
|
|
# here be resumed on the host with `claude --resume`.
|
|
- ${WORKSPACE_DIR:?set WORKSPACE_DIR to the project directory to mount}:${WORKSPACE_DIR}
|
|
- ${HOME}/.claude:/home/node/.claude
|
|
# settings.json/CLAUDE.md/plugins are config and hook/plugin *code*,
|
|
# not runtime state - hooks run automatically with no approval gate,
|
|
# so a compromised session writing to them would plant something that
|
|
# then executes unsandboxed on the host next time Claude Code starts
|
|
# there. Layer them back read-only on top of the read-write parent
|
|
# mount above (Docker applies more specific mounts on top of broader
|
|
# ones); everything else under ~/.claude (session/project history,
|
|
# credentials refresh, caches, etc.) still needs to stay read-write.
|
|
- ${HOME}/.claude/settings.json:/home/node/.claude/settings.json:ro
|
|
- ${HOME}/.claude/CLAUDE.md:/home/node/.claude/CLAUDE.md:ro
|
|
- ${HOME}/.claude/plugins:/home/node/.claude/plugins:ro
|
|
# ~/.claude.json is a single file mixing live app state (project
|
|
# registry, onboarding flags, caches) with config (e.g. mcpServers, if
|
|
# ever set) that can't be split at the mount level - left read-write
|
|
# since most of its content must stay writable for normal operation.
|
|
# See README Security notes for this residual risk.
|
|
- ${HOME}/.claude.json:/home/node/.claude.json
|
|
# ~/.claude/CLAUDE.md (mounted above) tells agents to write specs to
|
|
# ~/specs/ rather than a project-local path - that instruction is
|
|
# already in effect inside the container (CLAUDE.md carries over), so
|
|
# the directory it points at needs to actually exist and be writable
|
|
# here too, or an agent following it just writes into an ephemeral
|
|
# path inside the container that vanishes with the session.
|
|
- ${HOME}/specs:/home/node/specs
|
|
# ~/.claude/hooks is a symlink into ~/dotfiles/config/claude/hooks (an
|
|
# absolute host path); mount dotfiles at the same path so it resolves
|
|
# in the container too. Read-only: hooks are only ever read/executed
|
|
# from here, never written.
|
|
- ${HOME}/dotfiles:${HOME}/dotfiles:ro
|
|
# Also mount at node's own $HOME/dotfiles: node's actual $HOME is
|
|
# /home/node (not the host user's $HOME above), and plenty of this
|
|
# repo's own config (nvim's clipboard-copy path, vimrc sourcing, etc.)
|
|
# assumes dotfiles lives at $HOME/dotfiles for whichever user it's
|
|
# running as.
|
|
- ${HOME}/dotfiles:/home/node/dotfiles:ro
|
|
- claude-code-bashhistory:/commandhistory
|
|
- nvim-data:/home/node/.local/share/nvim
|
|
- nvim-state:/home/node/.local/state/nvim
|
|
- nvim-cache:/home/node/.cache/nvim
|
|
- ${SSH_AUTH_SOCK:?set SSH_AUTH_SOCK to your running ssh-agent socket}:/ssh-agent
|
|
- ${SSH_KNOWN_HOSTS:-/dev/null}:/home/node/.ssh/known_hosts:ro
|
|
# Read-write scratch space for passing files back and forth with the
|
|
# sandboxed session (screenshots, temp scripts) without touching the
|
|
# actual project checkout - separate from WORKSPACE_DIR above, and a
|
|
# real host directory (not a named volume) so it's actually visible on
|
|
# both sides, persisting across sessions.
|
|
- ${SANDBOX_SHARE_DIR}:/home/node/shared
|
|
working_dir: ${WORKSPACE_DIR}
|
|
stdin_open: true
|
|
tty: true
|
|
|
|
volumes:
|
|
claude-code-bashhistory:
|
|
nvim-data:
|
|
nvim-state:
|
|
nvim-cache:
|