Files
dotfiles/docker/claude-sandbox/docker-compose.yml
T

140 lines
7.7 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}
# Matches the container's own user to the real host user - see
# Dockerfile. Defaults reproduce today's node/1000/1000//home/node
# exactly if these aren't set (e.g. building directly, bypassing
# bin/claude-sandbox).
SANDBOX_USER: ${SANDBOX_USER:-node}
SANDBOX_UID: ${SANDBOX_UID:-1000}
SANDBOX_GID: ${SANDBOX_GID:-1000}
SANDBOX_HOME: ${HOME:-/home/node}
# Tagged per-user, not a fixed :latest - bin/claude-sandbox always
# passes --build, and two different accounts on a shared devbox
# building from their own checkouts would otherwise silently overwrite
# each other's image under the same tag.
image: claude-sandbox:${SANDBOX_USER:-node}
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:-}
# Points git at the gitconfig copy mounted below instead of the
# default ~/.gitconfig - see that mount's comment for why it can't
# just live at ~/.gitconfig directly.
GIT_CONFIG_GLOBAL: ${HOME}/.gitconfig-sandbox/.gitconfig
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}/.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.
#
# settings.json specifically comes from $SANDBOX_SETTINGS_FILE (a
# bin/claude-sandbox-generated copy with sandbox.enabled forced to
# false), not the real file directly - Claude Code's own internal
# per-command sandboxing is redundant on top of this container (and
# can't actually function inside it anyway), and mounting it read-only
# means it can't be patched from inside, so the substitution has to
# happen on the host before the container ever starts.
- ${SANDBOX_SETTINGS_FILE:?set by bin/claude-sandbox}:${HOME}/.claude/settings.json:ro
- ${HOME}/.claude/CLAUDE.md:${HOME}/.claude/CLAUDE.md:ro
- ${HOME}/.claude/plugins:${HOME}/.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}/.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}/specs
# Global git identity (user.name/user.email) - a throwaway copy of it,
# in its own directory rather than a single file bind-mounted at
# ~/.gitconfig: entrypoint.sh's `gh auth setup-git` writes a
# credential-helper entry into it via git's usual write-temp-then-
# rename pattern, which fails on a single-file mount ("Device or
# resource busy" - Linux won't let that mount point's backing inode be
# replaced). GIT_CONFIG_GLOBAL above points git at the file inside
# this directory instead. See bin/claude-sandbox - nothing written
# here ever reaches the user's real ~/.gitconfig.
- ${SANDBOX_GITCONFIG_DIR:?set by bin/claude-sandbox}:${HOME}/.gitconfig-sandbox
# ~/.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. Only one mount is needed now (unlike
# before) - the container's own $HOME equals the host's $HOME, so
# there's no separate node-specific path to mirror it at.
- ${HOME}/dotfiles:${HOME}/dotfiles:ro
- claude-code-bashhistory:/commandhistory
- nvim-data:${HOME}/.local/share/nvim
- nvim-state:${HOME}/.local/state/nvim
- nvim-cache:${HOME}/.cache/nvim
# Required only if SANDBOX_SSH_KEY isn't set - bin/claude-sandbox
# itself enforces that at least one of the two is present.
- ${SSH_AUTH_SOCK:-/dev/null}:/ssh-agent
- ${SSH_KNOWN_HOSTS:-/dev/null}:${HOME}/.ssh/known_hosts:ro
# Dedicated, purpose-built key for headless git push - see README
# "Headless git push without agent forwarding". Read-only single-file
# mount is fine here (unlike the gitconfig mount above): nothing in
# the container ever writes to this file.
- ${SANDBOX_SSH_KEY_FILE:-/dev/null}:${HOME}/.ssh-sandbox-key-src: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}/shared
working_dir: ${WORKSPACE_DIR}
stdin_open: true
tty: true
volumes:
claude-code-bashhistory:
nvim-data:
nvim-state:
nvim-cache: