# claude-sandbox Technical Wiki

> A Docker sandbox layer that runs Claude Code agents inside isolated per-workspace containers launched by Superset, keeping the host git worktree as the single source of truth.

## Context Links

- [Agent index](https://grok-wiki.com/public/wiki/hans-claude-container-cf30219c8958/llms.txt)
- [Human interactive wiki](https://grok-wiki.com/public/wiki/hans-claude-container-cf30219c8958)
- [GitHub repository](https://github.com/hans/claude-container)

## Repository Metadata

- Repository: hans/claude-container

- Generated: 2026-05-18T19:25:52.289Z
- Updated: 2026-05-21T21:36:26.367Z
- Runtime: Grok CLI
- Format: Basic
- Pages: 6

## Page Index

- 01. [Overview](https://grok-wiki.com/public/wiki/hans-claude-container-cf30219c8958/pages/01-overview.md) - What claude-sandbox is, its core design goals (isolation without Container Use, host worktree as source of truth), and how it integrates with Superset workspaces and Claude Code.
- 02. [Setup and Superset Integration](https://grok-wiki.com/public/wiki/hans-claude-container-cf30219c8958/pages/02-setup-and-superset-integration.md) - Prerequisites, building the image, copying the .superset/ launcher into a repo, configuring a Superset agent, and the first workspace launch flow.
- 03. [The launch.sh Launcher](https://grok-wiki.com/public/wiki/hans-claude-container-cf30219c8958/pages/03-the-launch.sh-launcher.md) - The host-side script that performs preflight checks, detects prompts via argv/env/stdin, computes stable container names, decides between docker run and docker exec for reattach, and assembles all Docker arguments.
- 04. [Mount Strategies: Worktrees, Symlinks, and Git](https://grok-wiki.com/public/wiki/hans-claude-container-cf30219c8958/pages/04-mount-strategies-worktrees-symlinks-and-git.md) - How launch.sh bind-mounts the worktree, resolves .git worktree pointers by mounting the parent .git directory, scans external symlinks and mounts their targets, and optionally mounts ~/.ssh read-only.
- 05. [Container Image and Entrypoint](https://grok-wiki.com/public/wiki/hans-claude-container-cf30219c8958/pages/05-container-image-and-entrypoint.md) - The Dockerfile that builds an Ubuntu 24.04 image with Claude Code, uv, Node 22, ripgrep, and a UID-1000 claude user; the minimal entrypoint.sh that sources /workdir/.env before exec.
- 06. [Environment Variables, Customization, and Troubleshooting](https://grok-wiki.com/public/wiki/hans-claude-container-cf30219c8958/pages/06-environment-variables-customization-and-troubleshooting.md) - All CLAUDE_SANDBOX_* host and container variables, ANTHROPIC_* forwarding, network modes, per-project Dockerfile extension, and the documented failure modes with exact fixes.

## Source File Index

- `.dockerignore`
- `.superset/launch.sh`
- `.superset/setup.sh`
- `Dockerfile`
- `entrypoint.sh`
- `README.md`
- `SETUP.md`

---

## 01. Overview

> What claude-sandbox is, its core design goals (isolation without Container Use, host worktree as source of truth), and how it integrates with Superset workspaces and Claude Code.

- Page Markdown: https://grok-wiki.com/public/wiki/hans-claude-container-cf30219c8958/pages/01-overview.md
- Generated: 2026-05-18T19:25:10.465Z

### Source Files

- `README.md`
- `SETUP.md`

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [README.md](README.md)
- [SETUP.md](SETUP.md)
- [.superset/launch.sh](.superset/launch.sh)
- [.superset/setup.sh](.superset/setup.sh)
- [Dockerfile](Dockerfile)
- [entrypoint.sh](entrypoint.sh)
- [.dockerignore](.dockerignore)

</details>

# Overview

claude-sandbox runs Claude Code sessions inside per-workspace Docker containers launched by Superset. Each Superset workspace (a git worktree) receives an isolated execution sandbox while the host worktree remains the single source of truth: code, git history, and diffs live on the host and stay visible to Superset's diff viewer and other tools.

The container is deliberately *not* a Container Use (`cu`) setup. It provides a clean execution environment for the agent's bash tool and Claude Code process; all persistent state and git operations occur on the host bind mount at `/workdir`.

This design lets teams keep their normal Superset + git workflows while giving Claude Code a hermetic sandbox that cannot accidentally pollute the host or other workspaces.

## Core Design Goals

- **Isolation without Container Use.** The container supplies a separate filesystem view, process space, and network namespace. Git history, commits, and file changes never leave the host worktree. Superset continues to see the worktree exactly as before.
- **Host worktree as source of truth.** The worktree directory is bind-mounted at `/workdir` inside the container. Writes from Claude Code or its bash tool appear immediately on the host. Git worktree pointers and external symlinks are explicitly handled so the agent's view matches the host's view.
- **Per-workspace sandboxes.** Superset workspaces run in parallel; each receives its own named container. No shared state exists between sandboxes except what the user explicitly mounts.
- **Generic image, project-local extension.** The published image contains only a baseline Ubuntu toolchain plus Claude Code. Per-project Python packages, binaries, or tools are added either by ad-hoc `docker exec` (ephemeral) or by deriving a project `Dockerfile` from `claude-sandbox:latest` (persistent).

Sources: [README.md:3-11](README.md), [.dockerignore:1-4](.dockerignore)

## Integration with Superset Workspaces and Claude Code

Superset workspaces are ordinary git worktrees. To run Claude Code inside the sandbox, the user (or organization) configures a Superset agent whose **Command (No Prompt)** and **Command (With Prompt)** both point at `.superset/launch.sh` relative to the worktree root.

When Superset launches the agent it `cd`s into the worktree and executes the launcher, appending any prompt text as arguments. The launcher script therefore becomes the single integration point between Superset and the Docker runtime.

Deployment is intentionally per-repository:

- Copy the `.superset/` directory (containing `launch.sh` and `setup.sh`) into each repo that should use sandboxed agents.
- Commit it so every worktree carries its own launcher.
- In Superset's agent settings, set the command fields to `.superset/launch.sh`.

`setup.sh` runs on the host when Superset creates a new worktree; it copies a parent `.env` (if present) and prints reminders that project dependencies must be installed *inside* the container on first use.

Sources: [SETUP.md:1-5](SETUP.md), [SETUP.md:40-55](SETUP.md), [.superset/setup.sh:1-27](.superset/setup.sh)

## Launch Flow and Container Lifecycle

`.superset/launch.sh` performs these steps on every invocation:

1. Preflight checks: Docker present and running, target image exists, host `~/.claude` and `~/.claude.json` present.
2. On macOS, extracts the `Claude Code-credentials` keychain item into `~/.claude/.credentials.json` so the Linux container can authenticate.
3. Computes a stable container name from the worktree basename plus a short hash of its absolute path.
4. If a container with that name is already running, executes `docker exec` into it (invoking the container entrypoint so `.env` is re-sourced) — this provides instant reattach when the user closes and reopens a Superset terminal pane.
5. Otherwise builds a `docker run --rm -it` command with the mounts and environment described below and starts a fresh container.

The container is always started with `--rm`. When the `claude` process exits (user `/quit`, crash, or Superset pane closed while Claude is idle), the container is destroyed. The next launch creates a clean one. All worktree changes survive because they live on the host bind mount.

Sources: [.superset/launch.sh:1-15](.superset/launch.sh), [.superset/launch.sh:110-115](.superset/launch.sh)

## Filesystem, Git, and Symlink Fidelity

The launcher constructs the following bind mounts for every container:

- `$PWD:/workdir` — the worktree itself (read-write).
- `$HOME/.claude:/home/claude/.claude` and `$HOME/.claude.json:...` — Claude Code configuration and session state (read-write).
- Host user's `~/.gitconfig` (read-only).
- When `$PWD/.git` is a file (the git-worktree case), the parent `.git` directory is mounted at the same absolute host path so the `gitdir:` pointer resolves inside the container. This also supports submodules.
- Optional `~/.ssh` (read-only, opt-in via `CLAUDE_SANDBOX_MOUNT_SSH=1`).
- Every symlink inside the worktree whose target lies outside `$PWD` is resolved; its target directory is bind-mounted at the identical absolute host path. Targets are mounted read-only by default; `CLAUDE_SANDBOX_SYMLINK_MOUNTS_RW=1` or `CLAUDE_SANDBOX_SYMLINK_RW_PATHS` can selectively enable writes.

The container runs as `-u $(id -u):$(id -g)` so file ownership and permissions on the host bind mounts match the developer's host user. Git is configured at the system level inside the image to treat `/workdir` as a safe directory regardless of UID mismatch.

These mechanisms together guarantee that `git status`, `git commit`, edits to symlinked datasets, and Superset's own diff viewer all see exactly the same content whether the change originated from the host editor or from Claude Code's bash tool inside the container.

Sources: [.superset/launch.sh:118-129](.superset/launch.sh), [.superset/launch.sh:132-147](.superset/launch.sh), [.superset/launch.sh:154-166](.superset/launch.sh)

## Container Image and Environment

The image ([Dockerfile](Dockerfile)) starts from `ubuntu:24.04` and installs:

- Core tools: git, openssh-client, build-essential, ripgrep, fd, jq, vim, etc.
- `uv` (Python package manager) pinned to `/usr/local/bin`.
- Node 22 + `@anthropic-ai/claude-code` installed globally.
- A `claude` user (UID 1000) whose home is mode 0777 so the runtime UID/GID can write into it.

`entrypoint.sh` is the image ENTRYPOINT. On every start (both `docker run` and `docker exec` paths) it sources `/workdir/.env` (if present) with `set -a`, then `exec`s the command (normally `claude --dangerously-skip-permissions ...`).

Host-side `CLAUDE_SANDBOX_*` variables control the launcher itself (image name, network mode, symlink behavior, etc.). Any `ANTHROPIC_*` variable present on the host is forwarded into the container. Project secrets live in the worktree's `.env` file and become visible to the agent via the entrypoint.

Sources: [Dockerfile:1-5](Dockerfile), [entrypoint.sh:1-14](entrypoint.sh), [README.md:112-130](README.md)

## Network and Security Trade-offs

- Default network mode `bridge` gives the container outbound Internet but prevents it from reaching `localhost` services on the host.
- `host` mode shares the host network stack (useful for local APIs) at the cost of reduced isolation.
- `none` disables networking entirely.

Because the container runs as the developer's own UID and receives full read/write access to the worktree (plus any symlinked targets), it has the same filesystem powers the developer has on the host. The sandbox protects the *host system* from the agent, not the worktree contents from the agent. Users should only symlink directories they are comfortable exposing.

Sources: [README.md:140-160](README.md) (network section), [.superset/launch.sh:166-230](.superset/launch.sh) (symlink mount logic)

## What the Project Deliberately Does Not Provide

- Container Use / nested branch / auto-commit semantics — git lives on the host.
- Built-in multi-agent orchestration — Superset launches one container per workspace; parallelism is managed by the workspace system.
- GPU passthrough or Windows support.
- A long-lived daemon — containers are `--rm` and ephemeral.

These boundaries keep the tool small, auditable, and focused on the single job of giving Claude Code a clean, host-faithful execution environment inside Superset.

Sources: [README.md:200-230](README.md)

## Summary

claude-sandbox is a thin, Superset-native launcher plus a minimal Docker image that gives every workspace an isolated Claude Code runtime while preserving the host git worktree as the authoritative source of code, history, and collaboration state. All complex fidelity logic (gitdir pointers, escaping symlinks, credential staging, UID matching, reattach) lives in `.superset/launch.sh`; the container itself stays generic and rebuilds only when the base toolchain changes.

Sources: [README.md:3-11](README.md)

---

## 02. Setup and Superset Integration

> Prerequisites, building the image, copying the .superset/ launcher into a repo, configuring a Superset agent, and the first workspace launch flow.

- Page Markdown: https://grok-wiki.com/public/wiki/hans-claude-container-cf30219c8958/pages/02-setup-and-superset-integration.md
- Generated: 2026-05-18T19:25:12.224Z

### Source Files

- `SETUP.md`
- `.superset/setup.sh`
- `README.md`

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:
- [SETUP.md](SETUP.md)
- [README.md](README.md)
- [.superset/setup.sh](.superset/setup.sh)
- [.superset/launch.sh](.superset/launch.sh)
- [Dockerfile](Dockerfile)
- [entrypoint.sh](entrypoint.sh)
- [.dockerignore](.dockerignore)
</details>

# Setup and Superset Integration

This page explains how to prepare the claude-sandbox Docker image and wire it into Superset so Claude Code runs inside an isolated container while the workspace remains a normal host-side git worktree. The integration lets each Superset workspace launch its own sandbox, with the worktree bind-mounted at `/workdir` so all edits, git history, and diffs stay visible to Superset exactly as before.

The flow centers on two scripts in the `.superset/` directory: `launch.sh` (invoked by Superset for every agent start) and `setup.sh` (run once per worktree by Superset). The container itself is intentionally generic; project-specific dependencies are added either ad-hoc via `docker exec` or persistently via a thin derived Dockerfile.

## Prerequisites

Before the first launch, the host must satisfy four checks. These are performed both manually and automatically inside `launch.sh`.

```bash
docker version            # Docker Desktop or engine must be running
claude --version          # Claude Code must be installed on the host
ls ~/.claude.json         # host login state must exist (run `claude /login` if not)
security find-generic-password -s "Claude Code-credentials" -w >/dev/null \
    && echo OK            # macOS only: keychain entry must exist
```

If any check fails, correct it before proceeding. `launch.sh` repeats the Docker, image, and `~/.claude*` checks on every invocation and exits with a clear message when something is missing.

**Sources:** [SETUP.md:7-17](), [README.md:16-20](), [.superset/launch.sh:34-59]()

## Building the Image

From the repository root, build the image once:

```bash
docker build -t claude-sandbox:latest .
```

The build pulls Ubuntu 24.04, installs git, build tools, ripgrep, fd, Python + uv, Node 22, and the `@anthropic-ai/claude-code` npm package, then creates a `claude` user and installs `entrypoint.sh`. Subsequent builds are fast due to layer caching. Rebuild only when `Dockerfile` changes.

Verify the image:

```bash
docker run --rm claude-sandbox:latest claude --version
```

The `.dockerignore` keeps the build context small and prevents accidental inclusion of secrets or the `.superset/` launcher itself.

**Sources:** [Dockerfile:1-65](), [SETUP.md:19-36](), [.dockerignore:1-21]()

## Copying the Launcher into a Repository

`launch.sh` is deliberately a relative-path script so each repository carries its own copy and can be customized per project. The recommended approach is to copy the entire `.superset/` directory:

```bash
cp -r .superset /path/to/your/repo/
cd /path/to/your/repo/
git add .superset/
git commit -m "Add Superset sandbox launcher"
```

`setup.sh` runs on the host when Superset first creates a worktree. It is idempotent and currently does two things: copies `../.env` into the worktree if one does not yet exist, and prints a reminder that Python or Node dependencies must be installed *inside* the running container (the image is intentionally generic).

An alternative global install (drop `launch.sh` on `$PATH`) is supported for users who prefer not to add files to every repository.

**Sources:** [SETUP.md:38-52](), [.superset/setup.sh:1-27]()

## Configuring a Superset Agent

In Superset, go to **Settings → Agents**. Either duplicate the built-in `claude` preset or create a new agent. The minimal configuration is:

| Field                  | Value                                      |
|------------------------|--------------------------------------------|
| Label                  | `Claude (sandbox)`                         |
| Enabled                | ON                                         |
| Command (No Prompt)    | `.superset/launch.sh`                      |
| Command (With Prompt)  | `.superset/launch.sh`                      |
| Prompt Command Suffix  | *(empty)*                                  |
| Environment            | `CLAUDE_SANDBOX_NETWORK=bridge` (optional) |

If you installed the launcher globally, replace the command with the absolute path. The **Environment** field is the place to set `CLAUDE_SANDBOX_*` variables (one per line); Superset injects them into the shell that runs `launch.sh`.

**Sources:** [SETUP.md:54-70](), [README.md:34-55]()

## First Workspace Launch Flow

When you select the sandbox agent and send a prompt, Superset invokes `.superset/launch.sh` with the worktree as `$PWD`. The script performs the following steps:

1. **Preflight** – verifies Docker is on PATH, the daemon is reachable, the chosen image exists, and the host `~/.claude` directory and `.claude.json` are present. On macOS it also extracts the keychain credential blob into `~/.claude/.credentials.json` so the Linux container can authenticate.
2. **Container naming** – computes a stable name `claude-sandbox-<basename>-<path-hash>` so every worktree gets its own container.
3. **Reattach check** – if a container with that name is already running, the script does `docker exec` (explicitly calling `entrypoint.sh` so `.env` is re-sourced) and exits. This is how closing and reopening a Superset terminal pane reattaches to the same sandbox.
4. **Docker run** – otherwise it assembles a long `docker run` command with:
   - `--rm -it --name ...`
   - bind mount of the worktree at `/workdir`
   - read-write mount of `~/.claude` and `~/.claude.json`
   - read-only mount of `~/.gitconfig`
   - UID/GID matching (`-u $(id -u):$(id -g)`) so bind-mount writes succeed
   - special handling for git worktree `.git` files (mounts the parent `.git` directory at its absolute host path)
   - automatic mounting of external symlink targets (read-only by default, configurable via `CLAUDE_SANDBOX_SYMLINK_*` variables)
   - forwarding of all `ANTHROPIC_*` and selected Superset workspace variables
5. **Entrypoint** – inside the container, `entrypoint.sh` sources `/workdir/.env` (if present) and then `exec`s the `claude` command (plus any initial prompt).

The first launch therefore performs a full `docker run`; subsequent launches in the same workspace usually take the fast `docker exec` path.

```mermaid
flowchart TD
  Start[Superset invokes launch.sh] --> Preflight{Preflight OK?}
  Preflight -->|No| Exit[Exit with message]
  Preflight -->|Yes| Exists{Container running?}
  Exists -->|Yes| Exec[docker exec entrypoint.sh claude ...]
  Exists -->|No| Run[docker run ... entrypoint.sh claude ...]
  Run --> Claude[Claude Code starts]
  Exec --> Claude
```

**Sources:** [.superset/launch.sh:34-130](), [entrypoint.sh:1-14](), [Dockerfile:55-64]()

## Reattachment and Container Lifecycle

Because the container is started with `--rm`, it disappears as soon as the `claude` process exits (user `/quit`, crash, or explicit exit). Worktree state is never inside the container, so nothing is lost. While the container lives, any later launch of the same agent in the same workspace reattaches via `docker exec`, preserving the Claude session, REPL history, and any ad-hoc packages installed during that lifetime.

**Sources:** [SETUP.md:72-80](), [.superset/launch.sh:110-115]()

## Environment Variables and Per-Project Customization

Host-side variables (read by `launch.sh`) are set in the agent's **Environment** field or exported before starting Superset. Container-side secrets belong in a `.env` file at the worktree root; `entrypoint.sh` sources it on every launch and reattach. `setup.sh` will propagate `../.env` into a new worktree automatically.

For persistent extra tools, create a thin derived image:

```dockerfile
FROM claude-sandbox:latest
RUN pip install --break-system-packages mne nibabel
```

Build it, then point `launch.sh` at it with `CLAUDE_SANDBOX_IMAGE=claude-sandbox-myproj` in the agent's environment.

**Sources:** [README.md:95-140](), [.superset/setup.sh:8-20]()

## Troubleshooting First-Launch Issues

| Symptom | Cause | Fix |
|---------|-------|-----|
| `docker: command not found` | Docker not on PATH | Start Docker Desktop or install Docker Engine. |
| `image 'claude-sandbox:latest' not found` | Image never built | `docker build -t claude-sandbox:latest .` |
| `Claude configuration file not found at: /home/claude/.claude.json` | Host `~/.claude.json` missing or stale container | `claude /login` on host, then `docker rm -f` any old container. |
| `couldn't read 'Claude Code-credentials' from keychain` | macOS keychain entry absent | Run `claude /login` on the host. |
| `fatal: not a git repository` inside container | Worktree `.git` pointer cannot resolve | Ensure current `launch.sh` is deployed (it auto-mounts the parent `.git`). |
| `permission denied` writing files | UID mismatch on bind mount | Do not override the `-u $(id -u):$(id -g)` logic in `launch.sh`. |

**Sources:** [SETUP.md:82-100]()

The design keeps the sandbox generic, the worktree authoritative, and the integration portable across projects simply by copying `.superset/`. All behavior is driven by the verified scripts and Dockerfile described above.

**Sources:** [README.md:1-15]()

---

## 03. The launch.sh Launcher

> The host-side script that performs preflight checks, detects prompts via argv/env/stdin, computes stable container names, decides between docker run and docker exec for reattach, and assembles all Docker arguments.

- Page Markdown: https://grok-wiki.com/public/wiki/hans-claude-container-cf30219c8958/pages/03-the-launch.sh-launcher.md
- Generated: 2026-05-18T19:24:45.517Z

### Source Files

- `.superset/launch.sh`
- `README.md`

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [.superset/launch.sh](.superset/launch.sh)
- [README.md](README.md)
- [SETUP.md](SETUP.md)
- [entrypoint.sh](entrypoint.sh)
- [Dockerfile](Dockerfile)
- [.superset/setup.sh](.superset/setup.sh)
- [.dockerignore](.dockerignore)
</details>

# The launch.sh Launcher

`.superset/launch.sh` is the host-side entry point that Superset (and manual users) invoke to run Claude Code inside a per-workspace Docker container. It performs preflight checks, detects an optional initial prompt from argv, environment, or stdin, computes a stable container name from the current worktree, decides whether to start a fresh container or reattach to an existing one, and assembles the complete `docker run` or `docker exec` command line with all required bind mounts, environment forwarding, and identity settings.

The script is deliberately kept in the repository (under `.superset/`) rather than installed globally so that each project can carry its own copy and customize image selection or behavior via environment variables. It is the only component that runs on the host before the container starts; once inside the container, `entrypoint.sh` takes over.

## Invocation and Environment

Superset configures an agent with:

- **Command (No Prompt)**: `.superset/launch.sh`
- **Command (With Prompt)**: `.superset/launch.sh`

When the user supplies a prompt, Superset appends it as additional argv arguments after any configured suffix. The script also accepts a prompt via the `CLAUDE_SANDBOX_PROMPT` environment variable or via stdin when the input is not a TTY. This design lets the same script serve both Superset-driven launches and direct terminal use.

```bash
# Example direct invocation with a prompt
CLAUDE_SANDBOX_IMAGE=claude-sandbox-myproj .superset/launch.sh "explain the auth flow"
```

Sources: [.superset/launch.sh:1-15](.superset/launch.sh), [README.md:39-58](README.md), [SETUP.md:78-94](SETUP.md)

## Prompt Detection

The script resolves a single `PROMPT` value using a strict priority order:

1. Non-empty argv tail (`$*`)
2. `CLAUDE_SANDBOX_PROMPT` environment variable
3. Stdin content when the descriptor is not a TTY

If no prompt is present, the script launches Claude Code in interactive mode. The resolved prompt (if any) is later passed as the final positional argument to the `claude` binary inside the container.

```bash
PROMPT=""
if [ "$#" -gt 0 ] && [ -n "${1:-}" ]; then
    PROMPT="$*"
elif [ -n "${CLAUDE_SANDBOX_PROMPT:-}" ]; then
    PROMPT="$CLAUDE_SANDBOX_PROMPT"
elif [ ! -t 0 ]; then
    PROMPT="$(cat)"
fi
```

Sources: [.superset/launch.sh:24-32](.superset/launch.sh)

## Preflight Checks

Before any Docker operation, the script validates the runtime environment and fails fast with actionable messages:

- `docker` binary exists on `PATH`
- Docker daemon is reachable (`docker info`)
- The target image (default `claude-sandbox:latest`) has been built locally
- Host Claude Code configuration exists (`$HOME/.claude/` directory and `$HOME/.claude.json` file)
- On macOS, the script extracts the `Claude Code-credentials` generic password from the system keychain and writes it to `$HOME/.claude/.credentials.json` so the Linux container can authenticate without access to the host keychain

These checks run on every invocation, including reattaches, ensuring a clear error if the user has not yet built the image or logged in on the host.

Sources: [.superset/launch.sh:35-81](.superset/launch.sh), [SETUP.md:7-17](SETUP.md)

## Stable Container Naming

Each worktree receives a deterministic container name of the form:

```
claude-sandbox-${WORKTREE_BASENAME}-${8-char-hash}
```

The basename is derived from `basename "$PWD"` with all characters outside `[A-Za-z0-9_.-]` replaced by `-`, runs collapsed, and trailing dashes trimmed. The hash is the first 8 characters of the SHA-1 digest of the full absolute worktree path (computed with `shasum -a 1` on macOS or `sha1sum` on Linux).

This scheme guarantees that two worktrees sharing the same directory name (common with git worktrees) still produce distinct containers, while repeated launches of the same worktree always target the same container for reattachment.

Sources: [.superset/launch.sh:83-97](.superset/launch.sh)

## Reattach vs. Fresh Launch Decision

After computing the name, the script checks whether a container with that exact name is currently running:

```bash
if docker ps --format '{{.Names}}' | grep -qx "$NAME"; then
    exec docker exec -it "$NAME" /usr/local/bin/entrypoint.sh "${claude_argv[@]}"
fi
```

When a match is found, it uses `docker exec` and explicitly invokes `entrypoint.sh` (instead of relying on the container's `ENTRYPOINT`) so that `/workdir/.env` is sourced on reattach exactly as it is on the initial `docker run`. If no container matches, the script proceeds to assemble a full `docker run --rm -it ...` command.

The `--rm` flag means the container is automatically removed when the `claude` process exits. Closing the Superset terminal pane while Claude is still running leaves the container alive for later reattachment; exiting Claude itself (`/quit` or crash) cleans up the container.

Sources: [.superset/launch.sh:110-115](.superset/launch.sh), [SETUP.md:119-128](SETUP.md), [README.md:119-128](README.md)

## Docker Argument Assembly

The bulk of the script builds the `docker_args` array. Core flags and mounts are always present:

```bash
docker_args=(
    run --rm -it
    --name "$NAME"
    -v "$PWD:/workdir"
    -v "$HOME/.claude:/home/claude/.claude"
    -v "$HOME/.claude.json:/home/claude/.claude.json"
    -v "$HOME/.gitconfig:/home/claude/.gitconfig:ro"
    -w /workdir
    -e HOME=/home/claude
    -u "$(id -u):$(id -g)"
    --network "$NETWORK"
)
```

Additional logic appends mounts and environment variables for three special cases.

### Git Worktree and Submodule Support

When `$PWD/.git` is a file (the git worktree pointer format `gitdir: /abs/path/...`), the script extracts the parent `.git` directory two levels up and bind-mounts it at the identical absolute host path inside the container. The same pattern supports submodules. The mount is read-write because git writes objects and refs there.

Sources: [.superset/launch.sh:132-147](.superset/launch.sh), [README.md:139-151](README.md)

### Symlink Escape Handling

Worktrees frequently contain symlinks pointing outside the worktree (shared datasets, `results/` directories in sibling worktrees, etc.). The container only sees `/workdir`, so such links would otherwise appear broken.

When `CLAUDE_SANDBOX_MOUNT_SYMLINKS` is not `0`, the script:

- Walks the worktree with `find`, pruning `.git`, `.venv`, `venv`, and `node_modules`
- Resolves each symlink target with `realpath` (or a Python fallback)
- Skips targets inside the worktree and broken targets
- For each external target, adds a bind mount at the same absolute host path
- Chooses `ro` or `rw` mode per target using `CLAUDE_SANDBOX_SYMLINK_MOUNTS_RW`, `CLAUDE_SANDBOX_SYMLINK_RW_PATHS`, or per-prefix rules
- Deduplicates targets, preferring `rw` when both modes appear
- Logs each mount to stderr

The scan runs only on the initial `docker run`; reattaches via `docker exec` cannot add new mounts.

Sources: [.superset/launch.sh:154-234](.superset/launch.sh), [README.md:156-195](README.md)

### Environment Forwarding

All host variables matching the `ANTHROPIC_` prefix are forwarded with `-e`. Two Superset-specific workspace variables (`SUPERSET_WORKSPACE_NAME` and `SUPERSET_ROOT_PATH`) are forwarded by name when present. Per-project secrets belong in a `/workdir/.env` file that `entrypoint.sh` sources on every launch.

Sources: [.superset/launch.sh:236-247](.superset/launch.sh), [entrypoint.sh:7-12](entrypoint.sh)

## Integration with entrypoint.sh and the Container Image

The container image (`Dockerfile`) installs a minimal Ubuntu 24.04 base with Node 22, Claude Code, uv, git, and build tools. It creates a `claude` user, sets `/workdir` as the working directory, and installs `entrypoint.sh` as both `ENTRYPOINT` and the explicit reattach target.

`entrypoint.sh` unconditionally sources `/workdir/.env` (if present) using `set -a` before `exec`ing the command passed by Docker. This ensures that both fresh runs and reattaches see the same project environment.

`.dockerignore` deliberately excludes `.superset/` from the image build context; `launch.sh` and `setup.sh` are strictly host-side tooling.

Sources: [Dockerfile:60-65](Dockerfile), [entrypoint.sh:1-14](entrypoint.sh), [.dockerignore:1-22](.dockerignore)

## Configuration Surface

All runtime behavior is controlled by environment variables read by `launch.sh` before Docker is invoked. The most important are:

| Variable                          | Effect                                                                 | Default          |
|-----------------------------------|------------------------------------------------------------------------|------------------|
| `CLAUDE_SANDBOX_IMAGE`            | Docker image to run                                                    | `claude-sandbox:latest` |
| `CLAUDE_SANDBOX_NETWORK`          | `--network` value (`bridge`, `host`, `none`, or custom)                | `bridge`         |
| `CLAUDE_SANDBOX_MOUNT_SSH`        | Mount `~/.ssh` read-only                                               | off              |
| `CLAUDE_SANDBOX_MOUNT_SYMLINKS`   | Enable/disable the external-symlink scan                               | `1` (on)         |
| `CLAUDE_SANDBOX_SYMLINK_MOUNTS_RW`| Mount all external symlink targets read-write                          | off              |
| `CLAUDE_SANDBOX_SYMLINK_RW_PATHS` | Colon-delimited prefixes that receive `rw` mounts                      | unset            |
| `CLAUDE_SANDBOX_SKIP_PERMISSIONS` | Pass `--dangerously-skip-permissions` to Claude Code                   | `1` (on)         |

Additional variables are documented in [README.md:95-110](README.md).

## Execution Flow Summary

1. Resolve `PROMPT` from argv / env / stdin.
2. Run preflight checks (Docker, image, host Claude config, macOS credentials).
3. Compute deterministic container name from `$PWD`.
4. If a container with that name is running, `docker exec` into it via `entrypoint.sh`.
5. Otherwise assemble the full `docker run` argument list (mounts, uid/gid, git worktree support, symlink mounts, forwarded env) and execute it.

The script uses `exec` for the final Docker command so that signals and the controlling terminal are handled correctly.

Sources: [.superset/launch.sh:249-250](.superset/launch.sh)

The `launch.sh` script is the complete, self-contained contract between Superset and the Docker sandbox. Every security, identity, and filesystem detail required for a seamless per-worktree Claude Code experience is expressed in its argument assembly logic.

Sources: [.superset/launch.sh:16-251](.superset/launch.sh)

---

## 04. Mount Strategies: Worktrees, Symlinks, and Git

> How launch.sh bind-mounts the worktree, resolves .git worktree pointers by mounting the parent .git directory, scans external symlinks and mounts their targets, and optionally mounts ~/.ssh read-only.

- Page Markdown: https://grok-wiki.com/public/wiki/hans-claude-container-cf30219c8958/pages/04-mount-strategies-worktrees-symlinks-and-git.md
- Generated: 2026-05-18T19:25:17.573Z

### Source Files

- `.superset/launch.sh`
- `README.md`

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:
- [.superset/launch.sh](.superset/launch.sh)
- [README.md](README.md)
- [Dockerfile](Dockerfile)
- [entrypoint.sh](entrypoint.sh)
- [SETUP.md](SETUP.md)
- [.superset/setup.sh](.superset/setup.sh)
- [.dockerignore](.dockerignore)
</details>

# Mount Strategies: Worktrees, Symlinks, and Git

`.superset/launch.sh` launches Claude Code inside an isolated Docker container while the Superset workspace (a git worktree) remains the single source of truth on the host. The container receives a bind mount of the worktree at `/workdir`, but three additional strategies are required so that git, external data references, and SSH continue to work exactly as they do on the host.

Without these mounts, `git` commands inside the container fail with "not a git repository" (because the worktree's `.git` file contains an absolute pointer to a directory outside the bind mount), symlinks to shared results or datasets appear broken, and optional SSH-based git operations have no credentials. The script solves the problem uniformly: any host path that must be visible inside the container is bind-mounted at the identical absolute path.

## Primary Worktree Bind Mount

Every container starts with the current worktree directory mounted read-write at a conventional container path:

```bash
-v "$PWD:/workdir"
-w /workdir
-u "$(id -u):$(id -g)"
```

The working directory and UID/GID flags ensure that file creation, modification, and deletion inside the container are immediately visible on the host and carry the correct ownership. This is the only mount that is always present; all other strategies exist to make references that escape this directory resolve correctly.

Sources: [.superset/launch.sh:118-130]()

## Git Worktree and Submodule Pointer Resolution

Superset workspaces are git worktrees. In a worktree the file `$PWD/.git` is not a directory but a pointer:

```
gitdir: /absolute/path/to/main-repo/.git/worktrees/<name>
```

The absolute path lies outside the worktree directory. When only the worktree is bind-mounted, any `git` invocation inside the container fails because it cannot follow the pointer.

`launch.sh` detects this case, extracts the `gitdir` line, walks up two directory levels to reach the real `.git` directory (the same pattern works for submodules under `.git/modules`), and bind-mounts that parent directory at its original absolute host path:

```bash
if [ -f "$PWD/.git" ]; then
    gitdir="$(sed -n 's/^gitdir: *//p' "$PWD/.git")"
    ...
    parent_git="$(dirname "$(dirname "$gitdir")")"
    if [ -d "$parent_git" ]; then
        docker_args+=(-v "$parent_git:$parent_git")
    fi
fi
```

The mount is read-write because commits and ref updates must write objects and packfiles into the shared repository. The identical technique also makes git submodules function inside the sandbox.

Sources: [.superset/launch.sh:132-147](), [README.md:139-154]()

## External Symlink Target Mounting

It is common for a worktree to contain symlinks that point outside the worktree—for example `results/` symlinked from a sibling worktree, a shared dataset directory, or a scratch area. Because the container only sees the contents of `/workdir`, any such symlink would resolve to a non-existent path inside the container.

The script performs a one-time scan (controlled by `CLAUDE_SANDBOX_MOUNT_SYMLINKS`) on initial `docker run`:

- Walks the worktree with `find`, pruning `.git`, `venv`, `.venv`, and `node_modules` to avoid noise and cost.
- For every symlink, resolves the target with a cross-platform helper (`realpath` or `python3 -c os.path.realpath`).
- Skips targets that already lie inside the (realpath-resolved) worktree.
- Skips broken targets (Docker would reject the mount).
- Chooses read-only or read-write mode per target (see table below).
- Deduplicates identical targets, preferring read-write when any reference requests it.
- Appends a bind mount at the exact absolute host path with the chosen mode and logs the decision to stderr.

```bash
# inside the symlink block
while IFS= read -r -d '' link; do
    target="$(resolve_path "$link")" || continue
    ...
    link_target_modes+=("$target|$(symlink_mount_mode "$target")")
done < <(find ... -type l -print0 ...)
```

The scan runs only on first launch; subsequent `docker exec` reattaches cannot add new mounts. The same "mount the external absolute path at the same absolute path" pattern used for the `.git` parent directory is reused here.

Sources: [.superset/launch.sh:154-234](), [README.md:156-200]()

## Optional Read-Only ~/.ssh Mount

When `CLAUDE_SANDBOX_MOUNT_SSH=1` and `~/.ssh` exists on the host, the directory is bind-mounted read-only at the container's home:

```bash
-v "$HOME/.ssh:/home/claude/.ssh:ro"
```

This enables the agent to perform git push/pull over SSH using the user's existing keys and known_hosts without copying secrets into the image or requiring separate credential management.

Sources: [.superset/launch.sh:149-152](), [README.md:105]()

## Configuration via Environment Variables

The mount behaviors are driven entirely by host-side variables injected into the Superset agent environment (or exported before launching Superset). The relevant subset is:

| Variable                        | Effect on Mounts                                                                 | Default     |
|---------------------------------|----------------------------------------------------------------------------------|-------------|
| `CLAUDE_SANDBOX_MOUNT_SSH`      | When `1`, adds read-only `~/.ssh` mount                                          | off         |
| `CLAUDE_SANDBOX_MOUNT_SYMLINKS` | When `0`, disables the entire external-symlink scan                              | `1` (on)    |
| `CLAUDE_SANDBOX_SYMLINK_MOUNTS_RW` | When `1`, mounts every discovered external target read-write                  | `0` (ro)    |
| `CLAUDE_SANDBOX_SYMLINK_RW_PATHS` | Colon-separated list of prefixes (absolute or relative to worktree) that receive rw | unset       |

`CLAUDE_SANDBOX_SYMLINK_RW_PATHS` entries are normalized relative to `$PWD` when they are not absolute. When multiple symlinks point at the same target with conflicting modes, read-write wins after sorting.

Sources: [.superset/launch.sh:166-190](), [README.md:101-110]()

## Supporting Container Setup

The Dockerfile creates a `claude` user (UID 1000) with a world-writable `/home/claude`, installs `git` and `openssh-client`, and registers `/workdir` as a `safe.directory` so that the bind-mounted worktree is trusted even when host UID differs from the nominal container UID. `entrypoint.sh` only sources an optional `/workdir/.env` file after the mounts have already been established by `launch.sh`.

`.dockerignore` keeps the build context small and prevents accidental secret leakage into the image, but has no effect on the runtime bind mounts chosen by `launch.sh`.

Sources: [Dockerfile:1-80](), [entrypoint.sh:1-15](), [.dockerignore:1-20]()

## Mount Resolution Pattern (Conceptual)

```mermaid
graph LR
    subgraph Host
        W[Worktree $PWD]
        G[Parent .git]
        S[External symlink targets]
        SSH[~/.ssh]
    end
    subgraph Container
        /workdir
        G_same[same abs path as G]
        S_same[same abs paths as S]
        /home/claude/.ssh
    end
    W -->|-v ...:/workdir| /workdir
    G -->|-v G:G (rw)| G_same
    S -->|-v S:S :ro/rw| S_same
    SSH -->|-v ...:ro| /home/claude/.ssh
```

The unifying technique is that any path whose identity matters to tools inside the container is re-exposed at its original absolute location.

## Limitations and Edge Cases

- Docker Desktop on macOS only permits bind mounts under its configured File Sharing list; targets outside `/Users`, `/tmp`, `/private`, etc. produce "mounts denied" errors at `docker run` time.
- The symlink scan walks the entire worktree on every fresh launch; very large trees can be skipped with `CLAUDE_SANDBOX_MOUNT_SYMLINKS=0`.
- New external symlinks created while a container is running are invisible until the container exits and is relaunched.
- Granting read-write access to a symlinked directory also grants the agent the ability to modify that host location; the same caution that applies to any bind mount applies here.
- Non-standard `GIT_DIR` layouts or custom worktree configurations may require manual extension of the pointer-resolution block.

Sources: [README.md:197-200](), [.superset/launch.sh:223]()

The four strategies together let the container behave as a transparent execution sandbox while the host worktree remains the authoritative source of code, git history, and project data.

Sources: [.superset/launch.sh:1-15](), [README.md:1-20]()

---

## 05. Container Image and Entrypoint

> The Dockerfile that builds an Ubuntu 24.04 image with Claude Code, uv, Node 22, ripgrep, and a UID-1000 claude user; the minimal entrypoint.sh that sources /workdir/.env before exec.

- Page Markdown: https://grok-wiki.com/public/wiki/hans-claude-container-cf30219c8958/pages/05-container-image-and-entrypoint.md
- Generated: 2026-05-18T19:25:06.660Z

### Source Files

- `Dockerfile`
- `entrypoint.sh`
- `.dockerignore`

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:
- [Dockerfile](Dockerfile)
- [entrypoint.sh](entrypoint.sh)
- [.dockerignore](.dockerignore)
- [README.md](README.md)
- [.superset/launch.sh](.superset/launch.sh)
- [SETUP.md](SETUP.md)
</details>

# Container Image and Entrypoint

The `claude-sandbox` repository provides a minimal, reproducible Docker image for running Anthropic Claude Code inside isolated containers. Superset launches these containers per workspace so that the agent's file operations occur against a bind-mounted worktree while all Claude Code state remains visible to Superset's diff viewer and git tooling on the host.

This page documents the `Dockerfile` that assembles an Ubuntu 24.04 image containing Claude Code, the `uv` Python manager, Node 22, `ripgrep`, and supporting tools, plus the tiny `entrypoint.sh` that injects per-project environment variables before executing the requested command. The design deliberately keeps the image generic and the entrypoint minimal so that project-specific needs are handled either by ad-hoc `docker exec` or by thin child Dockerfiles.

## Purpose and Usage Context

The image exists to give Claude Code a consistent Linux execution environment (Node runtime, search tools, Python package manager) without polluting the host or requiring every workspace to ship its own full toolchain image. The worktree is always bind-mounted at `/workdir`; the container never becomes the source of truth for code or git history.

Superset invokes `.superset/launch.sh`, which performs preflight checks, prepares host credentials, and ultimately runs:

```bash
docker run --rm -it \
  -v "$PWD:/workdir" \
  -v "$HOME/.claude:/home/claude/.claude" \
  ... \
  -u "$(id -u):$(id -g)" \
  claude-sandbox:latest claude ...
```

On subsequent attachments the same script uses `docker exec` but explicitly calls the entrypoint again so that `/workdir/.env` is re-sourced.

Sources: [README.md:1-15](), [.superset/launch.sh:111-114](), [.superset/launch.sh:250]()

## Dockerfile Structure

The Dockerfile is intentionally linear and comment-heavy. It proceeds through five clear stages.

### Base Image and Core Toolchain

```dockerfile
FROM ubuntu:24.04

ENV DEBIAN_FRONTEND=noninteractive \
    LANG=C.UTF-8 \
    LC_ALL=C.UTF-8 \
    PATH="/home/claude/.local/bin:$PATH"

RUN apt-get update && apt-get install -y --no-install-recommends \
        ca-certificates curl gnupg \
        git openssh-client \
        build-essential pkg-config \
        less vim-tiny \
        ripgrep jq fd-find \
        python3 python3-pip python3-venv \
    && ln -s "$(command -v fdfind)" /usr/local/bin/fd \
    && rm -rf /var/lib/apt/lists/*
```

`ripgrep` (`rg`) and `fd` are installed because Claude Code's bash and file-search tools rely on them. The `fd` symlink preserves the conventional `fd` command name.

Sources: [Dockerfile:1-22]()

### uv and Node + Claude Code Installation

`uv` is installed via its official bootstrap script directly into `/usr/local/bin` so it is available to every user without shell-profile modifications:

```dockerfile
RUN curl -fsSL https://astral.sh/uv/install.sh \
    | env UV_INSTALL_DIR=/usr/local/bin UV_NO_MODIFY_PATH=1 sh
```

Node 22 is obtained from NodeSource and Claude Code is installed globally:

```dockerfile
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
    && apt-get install -y --no-install-recommends nodejs \
    && rm -rf /var/lib/apt/lists/* \
    && npm install -g @anthropic-ai/claude-code
```

An optional scientific Python stack is left commented out to keep the default image small.

Sources: [Dockerfile:24-36]()

### Non-root User and Runtime Configuration

The image creates a dedicated `claude` user at UID 1000:

```dockerfile
RUN userdel -r ubuntu 2>/dev/null || true \
    && useradd --create-home --uid 1000 --shell /bin/bash claude \
    && chmod 0777 /home/claude

RUN git config --system --add safe.directory /workdir

WORKDIR /workdir
```

The 0777 mode on `/home/claude` and the explicit `safe.directory` entry allow the container to be started with the host user's UID/GID (via `-u $(id -u):$(id -g)`) while still permitting writes into bind-mounted locations that carry host ownership. The stock `ubuntu` user is removed so that UID 1000 can be claimed cleanly.

Sources: [Dockerfile:45-58]()

### Entrypoint Wiring

The final stage copies and registers the entrypoint:

```dockerfile
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod 0755 /usr/local/bin/entrypoint.sh

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["claude"]
```

Sources: [Dockerfile:60-65]()

## Entrypoint Script

The script is deliberately minimal (14 lines):

```sh
#!/bin/sh
set -eu

if [ -f /workdir/.env ]; then
    set -a
    . /workdir/.env
    set +a
fi

exec "$@"
```

It exports every variable defined in `/workdir/.env` (typically `ANTHROPIC_API_KEY`, model overrides, project-specific secrets) into the environment of the process that follows. Using `exec` replaces the shell so that signals and exit status are passed through cleanly. The `set -eu` guarantees that a missing `.env` file or a later failure aborts early.

Because `docker exec` bypasses the image `ENTRYPOINT`, `.superset/launch.sh` explicitly invokes `/usr/local/bin/entrypoint.sh` on re-attach paths as well.

Sources: [entrypoint.sh:1-14](), [.superset/launch.sh:111-114]()

## Build Context Hygiene

`.dockerignore` excludes everything that should never enter the image layers:

```
.git
.gitignore
.superset/
README.md
node_modules
__pycache__
*.pyc
.venv
venv
.env
.env.*
!.env.example
...
```

Only `entrypoint.sh` is ever `COPY`ed. This keeps builds fast and prevents accidental leakage of secrets or host-specific files into the published image.

Sources: [.dockerignore:1-21]()

## Key Runtime Behaviors

| Aspect                  | Implementation                                                                 | Reason |
|-------------------------|--------------------------------------------------------------------------------|--------|
| Worktree location       | `-v "$PWD:/workdir"`                                                           | Single source of truth for Superset diff viewer |
| User identity           | `-u "$(id -u):$(id -g)"`                                                       | Host UID/GID on bind mounts; no permission surprises |
| Environment injection   | `entrypoint.sh` sources `/workdir/.env` on both `run` and `exec`               | Per-project secrets without image rebuilds |
| Re-attachment           | `docker exec ... /usr/local/bin/entrypoint.sh ...`                             | Re-source `.env` and keep interactive session |
| Default command         | `CMD ["claude"]`                                                               | `launch.sh` can omit arguments for plain interactive start |
| Image extension         | Child `Dockerfile` starting `FROM claude-sandbox:latest`                       | Project-specific packages without forking the base |

Sources: [README.md:113-129](), [.superset/launch.sh:118-128]()

## Container Startup Flow

```mermaid
sequenceDiagram
    participant S as Superset / launch.sh
    participant D as Docker
    participant E as entrypoint.sh
    participant C as claude

    S->>D: docker run ... -v $PWD:/workdir -u hostuid ... claude-sandbox claude [prompt]
    D->>E: ENTRYPOINT /usr/local/bin/entrypoint.sh
    E->>E: if /workdir/.env exists: set -a; source; set +a
    E->>C: exec claude [prompt]
    Note over C: Claude Code runs with project env vars

    S->>D: docker exec ... /usr/local/bin/entrypoint.sh claude ...
    D->>E: entrypoint.sh (re-invoked)
    E->>E: re-source /workdir/.env
    E->>C: exec claude ...
```

This diagram shows why the entrypoint must be re-invoked on `docker exec` and why `/workdir/.env` is always read from the bind mount rather than baked into the image.

## Design Rationale and Limitations

- The image stays intentionally generic; heavy dependencies (large Python stacks, custom CLIs) belong in per-project `Dockerfile.project` files that extend the base.
- No secrets or host configuration ever enter the image layers because `.dockerignore` and the bind-mount strategy keep them out.
- The UID-1000 + world-writable home trick trades a small amount of image surface for seamless host-container UID interoperability; the actual runtime user is the host identity thanks to the `-u` flag.
- Rebuilding is only required when the base toolchain changes; most day-to-day customization happens via `docker exec` (ephemeral) or child images (persistent).

Sources: [Dockerfile:45-58](), [README.md:62-87](), [SETUP.md:132-151]()

## Summary

The combination of a single, well-documented `Dockerfile` and a 14-line entrypoint that reliably sources `/workdir/.env` gives Superset workspaces a reproducible Claude Code sandbox while preserving host-side file ownership, git history, and per-project secrets. All behavior is directly verifiable from the five core files listed at the top of this page.

Sources: [Dockerfile:1-65](), [entrypoint.sh:1-14]()

---

## 06. Environment Variables, Customization, and Troubleshooting

> All CLAUDE_SANDBOX_* host and container variables, ANTHROPIC_* forwarding, network modes, per-project Dockerfile extension, and the documented failure modes with exact fixes.

- Page Markdown: https://grok-wiki.com/public/wiki/hans-claude-container-cf30219c8958/pages/06-environment-variables-customization-and-troubleshooting.md
- Generated: 2026-05-18T19:25:52.285Z

### Source Files

- `README.md`
- `.superset/launch.sh`
- `SETUP.md`

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:
- [README.md](README.md)
- [.superset/launch.sh](.superset/launch.sh)
- [SETUP.md](SETUP.md)
- [Dockerfile](Dockerfile)
- [entrypoint.sh](entrypoint.sh)
- [.superset/setup.sh](.superset/setup.sh)
- [.dockerignore](.dockerignore)
</details>

# Environment Variables, Customization, and Troubleshooting

This page documents every `CLAUDE_SANDBOX_*` variable that controls the sandbox launcher, how `ANTHROPIC_*` variables are forwarded from the host into the container, supported Docker network modes, the two supported patterns for per-project Dockerfile extension, and all documented failure modes with their exact fixes. These controls are implemented in the launch script and supporting files; they determine what the Claude Code agent running inside the container can see, reach, and modify.

The variables are split into host-side settings (consumed by the launcher before `docker run`) and container-side settings (sourced from a worktree `.env` file on every launch and reattach). Correct use of these options lets teams keep a generic base image while safely exposing only the files, credentials, and network paths each project actually needs.

## Host-side CLAUDE_SANDBOX_* Variables

These variables are read by `.superset/launch.sh` before the container starts. They are typically set in Superset's per-agent **Environment** field (one `KEY=value` per line) or exported in the shell before launching Superset for a host-wide default.

| Variable                          | Purpose                                                                 | Default                  |
|-----------------------------------|-------------------------------------------------------------------------|--------------------------|
| `CLAUDE_SANDBOX_IMAGE`            | Docker image name to run                                                | `claude-sandbox:latest`  |
| `CLAUDE_SANDBOX_NETWORK`          | Docker network mode (`bridge`, `host`, `none`, or custom name)          | `bridge`                 |
| `CLAUDE_SANDBOX_MOUNT_SSH`        | Set to `1` to bind-mount `~/.ssh` read-only (enables git+SSH)           | unset (off)              |
| `CLAUDE_SANDBOX_MOUNT_SYMLINKS`   | Set to `0` to disable the external-symlink scan entirely                | `1` (on)                 |
| `CLAUDE_SANDBOX_SYMLINK_MOUNTS_RW`| Set to `1` to mount every external symlink target read-write            | `0` (read-only)          |
| `CLAUDE_SANDBOX_SYMLINK_RW_PATHS` | Colon-delimited list of path prefixes (absolute or relative to worktree) that should be mounted read-write | `results_scratch` (example) |
| `CLAUDE_SANDBOX_PROMPT`           | Manual prompt override (used when invoking the script directly)         | unset                    |
| `CLAUDE_SANDBOX_SKIP_PERMISSIONS` | Set to `1` to pass `--dangerously-skip-permissions` to Claude Code      | `1` (on)                 |

The symlink-related variables drive the automatic bind-mount logic that makes symlinks pointing outside the worktree resolve correctly inside the container. The scan walks the worktree once on the initial `docker run`, skips `.git`, `node_modules`, `.venv`/`venv`, deduplicates targets, and logs each mount to stderr.

Sources: [README.md:103-110](README.md), [.superset/launch.sh:19-20,103,150,166-191](.superset/launch.sh)

## Container-side Variables and .env Handling

Project-specific secrets and configuration live in a `.env` file at the root of the worktree. The container entrypoint sources this file on every launch and every `docker exec` reattach:

```sh
# entrypoint.sh:8
if [ -f /workdir/.env ]; then
    set -a
    . /workdir/.env
    set +a
fi
```

`ANTHROPIC_API_KEY`, `ANTHROPIC_BASE_URL`, model overrides, and any other keys the agent needs can be placed here. `.superset/setup.sh` copies `../.env` into a newly created worktree when the parent directory already contains one, so a single source of secrets can propagate automatically.

Sources: [entrypoint.sh:8-15](entrypoint.sh), [.superset/setup.sh:10-14](.superset/setup.sh), [README.md:112-119](README.md)

## ANTHROPIC_* Forwarding

Any environment variable whose name begins with `ANTHROPIC_` is automatically forwarded from the host shell into the container. The launcher extracts the names at runtime and passes them with `-e`:

```sh
# .superset/launch.sh:236
while IFS= read -r var; do
    [ -n "$var" ] && docker_args+=(-e "$var")
done < <(env | awk -F= '/^ANTHROPIC_/ {print $1}')
```

This mechanism works even when the values are not present in the worktree `.env` file, giving operators a choice between host-level injection and per-worktree files.

Sources: [.superset/launch.sh:236-239](.superset/launch.sh), [README.md:127](README.md)

## Network Modes

`CLAUDE_SANDBOX_NETWORK` controls the `--network` argument passed to `docker run`.

- `bridge` (default): The container receives an isolated NAT network. Outbound internet works; the agent cannot reach services bound to `localhost` on the host.
- `host`: The container shares the host's network stack. The agent can contact `localhost` services running on the host (useful for local development APIs) but loses network isolation from the host.
- `none`: The container has no network interfaces. Use when a fully hermetic sandbox is required.

Sources: [README.md:131-137](README.md), [.superset/launch.sh:20,130](.superset/launch.sh)

## Per-project Dockerfile Extension

The base image (`claude-sandbox:latest`) is intentionally generic. Two supported extension patterns exist:

**Quick / ephemeral**: Use `docker exec` inside a running container to install tools. Changes live only in the container's writable layer and disappear when the container is removed (`--rm`).

**Persistent / reproducible**: Create a project-local `Dockerfile.project`:

```dockerfile
FROM claude-sandbox:latest
RUN pip install --break-system-packages mne nibabel
```

Build and point the launcher at the new image:

```sh
docker build -t claude-sandbox-myproj -f Dockerfile.project .
export CLAUDE_SANDBOX_IMAGE=claude-sandbox-myproj
```

The same variable can be placed in Superset's agent Environment field for per-agent images.

Sources: [README.md:65-87](README.md), [SETUP.md:140-151](SETUP.md)

## Documented Failure Modes and Exact Fixes

**`docker: command not found`** or daemon unreachable  
Docker is not on PATH or Docker Desktop / engine is not running.  
Fix: Install and start Docker Desktop (macOS) or the Docker engine (Linux) before launching the agent.

**`claude-sandbox: image '...' not found`**  
The requested image has never been built.  
Fix: From the repository root, run `docker build -t claude-sandbox:latest .` (or the project-specific tag).

**`permission denied` writing into the worktree or `~/.claude`**  
The container runs as `-u $(id -u):$(id -g)`. Hard-coded UID mismatches (e.g., forcing `1000:1000`) break bind-mount writes.  
Fix: Remove any manual `-u` override; the default in `launch.sh` matches the host user.

**`credentials not found` / Claude Code login prompt inside the container**  
Host `~/.claude` and `~/.claude.json` are missing, or on macOS the keychain entry was never created.  
Fix: Run `claude /login` on the host first. On macOS the launcher also stages `~/.claude/.credentials.json` from the keychain on every launch. After a container-side token refresh you may need to re-login on the host.

**`Claude configuration file not found at: /home/claude/.claude.json` after editing the host file**  
Bind-mounted regular files are pinned to the original inode. Atomic replacement (used by some editors and Claude's own backup logic) breaks the mount.  
Fix: Exit the container completely and relaunch; the new file is re-mounted.

**`fatal: not a git repository` inside the container**  
The worktree's `.git` file contains an absolute `gitdir:` pointer that points outside the `/workdir` bind mount.  
Fix: Ensure `launch.sh` is current; it automatically detects the pointer and bind-mounts the parent `.git` directory at the same absolute path.

**Container name collision (`Conflict. The container name ... is already in use`)**  
An older container for the same worktree is still present.  
Fix: `docker rm -f claude-sandbox-<basename>-<hash>` (the name appears in the error).

**Closing the laptop (or losing the TTY) ends the session**  
The launcher uses `--rm`; the container is removed as soon as the `claude` process exits. Reattach only works while the container is still running.  
Fix: For sessions that must survive sleep or disconnect, run a long-lived `tmux` (or equivalent) inside the container as a workaround.

**macOS "mounts denied" when a symlink target is outside Docker Desktop's allowed paths**  
Docker Desktop restricts bind mounts to a configured allowlist (`/Users`, `/tmp`, `/private`, `/var/folders`, `/Volumes` by default).  
Fix: Add the target directory under Docker Desktop → Settings → Resources → File sharing, or move the symlink target under an already-shared prefix.

Additional first-launch symptoms and fixes are summarized in the setup guide.

Sources: [README.md:210-252](README.md), [SETUP.md:160-180](SETUP.md), [.superset/launch.sh:40-80,142-149](.superset/launch.sh)

## Summary

All sandbox behavior—image choice, network exposure, SSH and symlink mounts, credential forwarding, and per-project package additions—is driven by a small set of documented `CLAUDE_SANDBOX_*` variables plus the conventional `.env` file at the worktree root. The documented failure modes above cover every error path that the launcher and setup scripts explicitly check or that users have reported in the project documentation. Keeping the base image generic and using the `Dockerfile.project` + `CLAUDE_SANDBOX_IMAGE` pattern for customization keeps maintenance cost low while still giving each workspace exactly the tools and secrets it needs.

Sources: [.superset/launch.sh:1-250](.superset/launch.sh)

---
