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

- 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)
- [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]()
