-
-
Save noisysocks/f700d6672b03233da1020d341d6cb369 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # c - Run Claude Code in a Docker sandbox | |
| # | |
| # Threat model: | |
| # ✓ Prevents destructive commands from affecting host filesystem | |
| # ✓ Prevents reading sensitive files outside $CWD (~/.aws, ~/.env, etc.) | |
| # ✓ SSH agent forwarding (private keys never enter the container) | |
| # ✗ Does not restrict network access (open source workflow, by design) | |
| # | |
| # Usage: | |
| # c Start Claude Code in current directory | |
| # c --rebuild Rebuild the sandbox image (updates Claude Code) | |
| # c --shell Drop into a zsh shell in the sandbox | |
| # c <args> Pass additional arguments to claude CLI | |
| # | |
| # Design decisions: | |
| # - CWD is mounted at its real host path (not /workspace) so that Claude | |
| # Code session resume works — sessions are keyed by full CWD path. | |
| # - ~/.claude is mounted at both /home/agent/.claude and the host path | |
| # ($HOME/.claude) because plugin configs use absolute host paths. | |
| # - SSH uses agent forwarding via $SSH_AUTH_SOCK — private keys never | |
| # enter the container. Only public keys and known_hosts are mounted. | |
| # - SSH config (~/.ssh/config) is intentionally NOT mounted — macOS-only | |
| # options (UseKeychain) and IdentityFile paths break in the container. | |
| # - Git commit signing uses SSH (gpg.format=ssh) instead of GPG, so | |
| # signing works through the same agent socket with no extra mounts. | |
| # - Container user "agent" is created with the host UID so that file | |
| # permissions on mounts (CWD, SSH socket, etc.) just work. | |
| # - CACHEBUST arg before the Claude install ensures --rebuild always | |
| # fetches the latest version while keeping apt/Playwright layers cached. | |
| # - Playwright uses bundled Chromium (not Google Chrome) because Chrome | |
| # doesn't support Linux ARM64. | |
| # | |
| set -euo pipefail | |
| IMAGE="claude-sandbox" | |
| # --- Preflight --- | |
| if ! command -v docker &>/dev/null; then | |
| echo "error: docker is not installed" >&2 | |
| echo " https://www.docker.com/products/docker-desktop/" >&2 | |
| exit 1 | |
| fi | |
| if ! docker info &>/dev/null 2>&1; then | |
| echo "error: docker daemon is not running" >&2 | |
| exit 1 | |
| fi | |
| # --- Flags --- | |
| SHELL_MODE=false | |
| if [[ "${1:-}" == "--rebuild" ]]; then | |
| docker rmi "$IMAGE" 2>/dev/null || true | |
| shift | |
| elif [[ "${1:-}" == "--shell" ]]; then | |
| SHELL_MODE=true | |
| shift | |
| fi | |
| # --- Build image if needed --- | |
| if ! docker image inspect "$IMAGE" &>/dev/null 2>&1; then | |
| echo "Building Claude Code sandbox image..." | |
| docker build --build-arg HOST_UID="$(id -u)" --build-arg CACHEBUST="$(date +%s)" -t "$IMAGE" - <<'DOCKERFILE' | |
| FROM node:22-bookworm | |
| # System packages | |
| RUN apt-get update && apt-get install -y --no-install-recommends \ | |
| ca-certificates \ | |
| curl \ | |
| gh \ | |
| git \ | |
| jq \ | |
| less \ | |
| man-db \ | |
| nano \ | |
| openssh-client \ | |
| procps \ | |
| ripgrep \ | |
| vim \ | |
| sudo \ | |
| unzip \ | |
| zsh \ | |
| && apt-get clean \ | |
| && rm -rf /var/lib/apt/lists/* | |
| # Playwright system deps + CLI | |
| RUN npx playwright install-deps \ | |
| && npm install -g @playwright/cli@latest | |
| # Non-root user with passwordless sudo (UID matches host for file permissions) | |
| ARG HOST_UID | |
| RUN useradd -m -s /bin/zsh -u $HOST_UID agent \ | |
| && echo 'agent ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers | |
| USER agent | |
| ENV PATH="/home/agent/.local/bin:$PATH" | |
| ENV SHELL=/bin/zsh | |
| ENV EDITOR=vim | |
| ENV VISUAL=vim | |
| ENV PLAYWRIGHT_MCP_BROWSER=chromium | |
| # Install Playwright browsers | |
| RUN cd /usr/local/lib/node_modules/@playwright/cli && npx playwright install chromium | |
| # Claude Code CLI (ARG ensures cache bust on rebuild) | |
| ARG CACHEBUST | |
| RUN curl -fsSL https://claude.ai/install.sh | bash | |
| WORKDIR /workspace | |
| ENTRYPOINT ["claude", "--dangerously-skip-permissions"] | |
| DOCKERFILE | |
| echo "Done." | |
| fi | |
| # --- Assemble docker run arguments --- | |
| cwd_slug="$(basename "$(pwd)" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]-' '-')" | |
| run_args=( | |
| --rm -it | |
| --name "claude-${cwd_slug}-$$" | |
| --hostname "claude-${cwd_slug}" | |
| ) | |
| # Core mounts | |
| run_args+=(-v "$(pwd):$(pwd)" -w "$(pwd)") | |
| [[ -d "$HOME/.claude" ]] || mkdir -p "$HOME/.claude" | |
| run_args+=(-v "$HOME/.claude:$HOME/.claude") | |
| run_args+=(-v "$HOME/.claude:/home/agent/.claude") | |
| [[ -f "$HOME/.claude.json" ]] && run_args+=(-v "$HOME/.claude.json:/home/agent/.claude.json") | |
| [[ -d "$HOME/.agents" ]] && run_args+=(-v "$HOME/.agents:/home/agent/.agents") | |
| # Git config (read-only) | |
| [[ -f "$HOME/.gitconfig" ]] && \ | |
| run_args+=(-v "$HOME/.gitconfig:/home/agent/.gitconfig:ro") | |
| [[ -f "$HOME/.config/git/config" ]] && \ | |
| run_args+=(-v "$HOME/.config/git/config:/home/agent/.config/git/config:ro") | |
| # SSH agent forwarding (keys never enter the container) | |
| if [[ -n "${SSH_AUTH_SOCK:-}" ]]; then | |
| run_args+=( | |
| -v "$SSH_AUTH_SOCK:/tmp/ssh-agent.sock" | |
| -e SSH_AUTH_SOCK=/tmp/ssh-agent.sock | |
| ) | |
| fi | |
| # SSH known hosts and public keys (read-only) | |
| [[ -f "$HOME/.ssh/known_hosts" ]] && \ | |
| run_args+=(-v "$HOME/.ssh/known_hosts:/home/agent/.ssh/known_hosts:ro") | |
| for pubkey in "$HOME"/.ssh/*.pub; do | |
| [[ -f "$pubkey" ]] && \ | |
| run_args+=(-v "$pubkey:/home/agent/.ssh/$(basename "$pubkey"):ro") | |
| done | |
| # Environment | |
| if [[ -z "${GH_TOKEN:-}" ]] && command -v gh &>/dev/null; then | |
| GH_TOKEN="$(gh auth token 2>/dev/null)" || true | |
| fi | |
| TZ="$(readlink /etc/localtime | sed 's|.*/zoneinfo/||')" 2>/dev/null || true | |
| for var in CLAUDE_CODE_OAUTH_TOKEN GH_TOKEN TERM TZ; do | |
| [[ -n "${!var:-}" ]] && run_args+=(-e "$var=${!var}") | |
| done | |
| # --- Launch --- | |
| if [[ "$SHELL_MODE" == true ]]; then | |
| exec docker run --entrypoint zsh "${run_args[@]}" "$IMAGE" "$@" | |
| else | |
| exec docker run "${run_args[@]}" "$IMAGE" "$@" | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment