# 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.

- Repository: hans/claude-container
- GitHub: https://github.com/hans/claude-container
- Human wiki: https://grok-wiki.com/public/wiki/hans-claude-container-cf30219c8958
- Complete Markdown: https://grok-wiki.com/public/wiki/hans-claude-container-cf30219c8958/llms-full.txt

## 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)
