From 753250f28d0100161ad3288d414c2bd8a6fd6dd4 Mon Sep 17 00:00:00 2001 From: leovs09 Date: Mon, 21 Sep 2026 22:22:57 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=93=9D=20docs:=20add=20git=20worktree?= =?UTF-8?q?=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add "Within Git Worktree" and "With Git Worktree support" sections to README, covering both plain `docker run` and devcontainer flows for mounting the main repo's git common dir into a worktree-based container - Add a rule documenting the project's existing bold-lead callout convention so future edits don't introduce blockquote-style callouts --- .../match-existing-doc-callout-conventions.md | 37 ++++++++ README.md | 87 ++++++++++++++++++- 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 .claude/rules/match-existing-doc-callout-conventions.md diff --git a/.claude/rules/match-existing-doc-callout-conventions.md b/.claude/rules/match-existing-doc-callout-conventions.md new file mode 100644 index 0000000..a18a0fd --- /dev/null +++ b/.claude/rules/match-existing-doc-callout-conventions.md @@ -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. diff --git a/README.md b/README.md index 2117d5d..50f5b30 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,7 @@ The `:ro` flag prevents the container from modifying your host keys or config. --- -## Mounting multiple project directories +## Mounting multiple project directories (optional) Mount each project under a sibling path inside `/workspaces/`: @@ -231,6 +231,46 @@ docker run -it --rm \ **Trade-off.** A single shared container is convenient but reduces isolation: a runaway process in one project can affect the others. For full isolation, run a separate container per project, each with its own `~/.claude*` mounts. +### 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 `/.git/worktrees/`. 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/`, 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/"` | 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: '' 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 @@ -416,6 +456,51 @@ 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" + ], + // 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. + --- From 0a3f6b969fd882b8dde3493a6381e0f54db1741d Mon Sep 17 00:00:00 2001 From: leovs09 Date: Mon, 21 Sep 2026 22:27:09 +0200 Subject: [PATCH 2/2] fix: merge git worktreee support and reusable claude instalation --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b502d1f..0873203 100644 --- a/README.md +++ b/README.md @@ -270,7 +270,9 @@ docker run -it --rm \ **Trade-off.** A single shared container is convenient but reduces isolation: a runaway process in one project can affect the others. For full isolation, run a separate container per project, each with its own `~/.claude*` mounts. -### Within Git Worktree (optional) +--- + +## 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 `/.git/worktrees/`. 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"). @@ -309,7 +311,6 @@ This `docker run` flow itself writes nothing into your working tree. If this rep .sandbox-gitcommon ``` - --- ## Enviroment variables @@ -437,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 @@ -520,7 +520,9 @@ For projects that want MCP servers proxied from the host's [Docker MCP Catalog]( // 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=${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.