Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .claude/rules/match-existing-doc-callout-conventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
title: Match Existing In-Document Callout Conventions
paths:
- "**/*.md"
---

# Match Existing In-Document Callout Conventions

When adding a new note, warning, or trade-off callout to an existing document, reuse the document's own established callout format instead of introducing a different generic Markdown pattern (e.g., blockquotes). Mixing conventions within one file signals unaudited editing and degrades scanability for readers who learned the document's visual language.

## Incorrect

The document consistently marks callouts as plain bold-lead paragraphs (`**Trade-off.** ...`, `**Prerequisite for X.** ...`), but a new section introduces a blockquote for its warning — a different convention never used elsewhere in the file.

```markdown
**Trade-off.** Mounting `~/.claude*` binds the container to your host machine's profile...

...

> **Warning.** This configuration only works when the workspace folder is a git repository...
```

## Correct

Survey existing callouts in the file first (`grep -n '^\*\*' file.md` or similar), then match their exact format for the new content.

```markdown
**Trade-off.** Mounting `~/.claude*` binds the container to your host machine's profile...

...

**Warning.** This configuration only works when the workspace folder is a git repository...
```

## Reference

- Observed in README.md: every existing callout ("**MCP registration.**", "**Prerequisite for `~/.claude.json`.**", "**Trade-off.**") uses a plain bold-lead paragraph with no blockquote; a newly added "**Warning.**" callout was wrapped in `>` blockquote syntax, breaking the pattern.
91 changes: 89 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ The details:

---

## Mounting multiple project directories
## Mounting multiple project directories (optional)

Mount each project under a sibling path inside `/workspaces/`:

Expand Down Expand Up @@ -272,6 +272,47 @@ docker run -it --rm \

---

## Within Git Worktree (optional)

Running the sandbox from inside a [git worktree](https://git-scm.com/docs/git-worktree) needs two extra mounts. A worktree's `.git` is not a real repository directory — it is a one-line *gitfile* holding an absolute host path to `<main-repo>/.git/worktrees/<id>`. That path does not exist inside the container, so git fails with `fatal: not a git repository: (null)`. A worktree holds no objects or refs of its own — they all live in the main repository's `.git` (the "common dir").

```bash
# once, from inside the worktree — protects the main repo's worktree metadata
git worktree lock "$PWD"

docker run -it --rm \
-v "$PWD:/workspaces/$(basename "$PWD")" \
-v "$(git rev-parse --path-format=absolute --git-common-dir):/git/common" \
-e GIT_DIR="/git/common/worktrees/$(basename "$(git rev-parse --git-dir)")" \
-e GIT_WORK_TREE="/workspaces/$(basename "$PWD")" \
-e CLAUDE_CODE_OAUTH_TOKEN \
-e ANTHROPIC_API_KEY \
-e CONTEXT7_API_KEY \
-w "/workspaces/$(basename "$PWD")" \
neolabhq/sandbox:latest \
bash
```

**What each flag does:**

| Flag | Purpose |
|------|---------|
| `-v "$(git rev-parse --path-format=absolute --git-common-dir):/git/common"` | Mounts the main repository's `.git` (the common dir, where all objects and refs actually live) into the container. Mounting just this directory is sufficient: the worktree's own admin directory lives *inside* the common dir, at `worktrees/<id>`, and its link back to the common dir is the relative string `../..` — so the mount works at any container path, and no shared parent directory between the repo and the worktree is required. |
| `-e GIT_DIR="/git/common/worktrees/<id>"` | Points git at this worktree's admin directory inside the mounted common dir, so it resolves the correct `HEAD`, index, and refs for this worktree. `GIT_COMMON_DIR` alone does not work — git also needs to know which worktree's admin directory applies. |
| `-e GIT_WORK_TREE="/workspaces/..."` | Points git at the checked-out files, since `GIT_DIR` no longer sits next to them once redirected into `/git/common`. |

**Why `git worktree lock` matters.** Inside the container, the worktree's recorded host path does not exist, so git reports the worktree as `prunable`. A subsequent `git worktree prune` or `git gc` — run on the host, or in another container sharing the same common dir — would then delete this worktree's admin directory from your real main repository. Run `git worktree lock "$PWD"` from the worktree before starting the container to prevent that, and `git worktree unlock "$PWD"` to reverse it once you no longer need the lock. Running `lock` again on a worktree that is already locked is harmless — git exits with `fatal: '<path>' is already locked` and the existing lock is left untouched.

**Trade-off.** `GIT_DIR` is global to the container's environment, so do not mount unrelated repositories into the same container — git inside them would resolve to this worktree's admin directory instead of their own.

This `docker run` flow itself writes nothing into your working tree. If this repository is also set up with the [devcontainer pattern below](#with-git-worktree-support), which does create a `.sandbox-gitcommon` symlink in the workspace folder, add that entry to `.gitignore`:

```gitignore
.sandbox-gitcommon
```

---

## Enviroment variables

### Passing CLAUDE_CODE_OAUTH_TOKEN
Expand Down Expand Up @@ -397,7 +438,6 @@ Override system-level CLIs for a specific project by dropping a `devbox.json` at

**(e) Role boundary.** `mise` owns language runtimes in the project file just as it does at the image level: Node, Python, Go, Java, Ruby, Deno, Bun, etc. `devbox` owns system CLIs and libraries a project pins via nixpkgs. They compose cleanly because devbox's nix profile entries land on PATH before the mise shims when `devbox shell` activates, but the mise shims still resolve language binaries because devbox does not install Node, Python, Go, or Java by default.


---

## Using as a devcontainer
Expand Down Expand Up @@ -461,6 +501,53 @@ For projects that want MCP servers proxied from the host's [Docker MCP Catalog](
}
```

### With Git Worktree support

`devcontainer.json` `mounts` entries can only substitute variables — they cannot read a file — so the main repository's absolute path cannot be written directly into the config. The workaround: `initializeCommand` runs on the **host** before the container is created and creates a symlink inside the workspace folder pointing at the common dir; Docker's daemon resolves a symlink used as a bind-mount source on the host.

`.devcontainer/devcontainer.json`:

```jsonc
{
"name": "Agent Sandbox",
"image": "neolabhq/sandbox:latest",
"features": {
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {
"moby": false
}
},
// Runs on the HOST before the container is created. Points .sandbox-gitcommon
// at the main repository's .git directory (or this repo's own, when not a worktree).
"initializeCommand": "bash -c 'ln -sfn \"$(git rev-parse --path-format=absolute --git-common-dir)\" .sandbox-gitcommon && git worktree lock \"$PWD\" 2>/dev/null || true'",
"mounts": [
"source=${localWorkspaceFolder}/.sandbox-gitcommon,target=/git/common,type=bind",
"source=sandbox-claude-runtime,target=/home/vscode/.local/share/claude,type=volume",
"source=sandbox-claude-plugins,target=/home/vscode/.claude/plugins,type=volume"
],
// Runs inside the container. Points git at the worktree's admin dir when this
// is a worktree; does nothing in a regular repository.
"postStartCommand": "bash -c 'if [ -f .git ]; then id=$(basename \"$(sed \"s|^gitdir: ||\" .git)\"); if [ -d \"/git/common/worktrees/$id\" ]; then printf \"export GIT_DIR=/git/common/worktrees/%s\\nexport GIT_WORK_TREE=%s\\n\" \"$id\" \"$PWD\" >> ~/.bashrc; fi; fi'",
"remoteUser": "vscode",
"containerEnv": {
"CLAUDE_CODE_OAUTH_TOKEN": "${localEnv:CLAUDE_CODE_OAUTH_TOKEN}",
"ANTHROPIC_API_KEY": "${localEnv:ANTHROPIC_API_KEY}",
"CONTEXT7_API_KEY": "${localEnv:CONTEXT7_API_KEY}"
}
}
```

- **`initializeCommand`** runs on the host, before the container is created, and (re)creates `.sandbox-gitcommon` as a symlink to the repository's common dir, then best-effort locks the worktree so a stray `prune` or `gc` cannot delete its admin directory. This also runs when using VS Code's "Reopen in Container", so the flow works from the IDE, not just the CLI.
- **`mounts`** bind-mounts `.sandbox-gitcommon` — resolved through the symlink by the Docker daemon on the host — to `/git/common` inside the container.
- **`postStartCommand`** runs inside the container on every start. When the workspace's `.git` is a worktree gitfile, it exports `GIT_DIR` and `GIT_WORK_TREE` into `~/.bashrc` so interactive shells resolve git correctly; in a regular repository it does nothing.

Add the symlink to `.gitignore`. It is created inside the working tree, so `git status` shows it as untracked — and if it were committed, it would point at one developer's absolute local path and be broken for everyone else:

```gitignore
.sandbox-gitcommon
```

**Warning.** This configuration only works when the workspace folder **is** a git repository, and — for a worktree — when its main repository is also present on the host. `initializeCommand` runs `git rev-parse`, which fails in a folder that is not a git repository: no symlink is created, the `mounts` source is then missing, and because devcontainer `mounts` entries are emitted as `--mount type=bind`, Docker **hard-errors and the container will not start** — unlike plain `docker run -v`, which silently creates a root-owned empty directory on the host when the source is missing. In a regular, non-worktree repository this is harmless: the symlink points at that repository's own `.git`, and `postStartCommand` does nothing.


---

Expand Down
Loading