Sandboxed worktree
Creates a git worktree that a Claude Code session can work in autonomously: the
worktree gets its own .claude/settings.local.json that turns on the OS-level
sandbox, so sandboxed bash commands (including git commit) run without
permission prompts while staying confined to the worktree.
Where the worktree goes (and why not .claude/worktrees/)
The built-in EnterWorktree tool creates worktrees under <repo>/.claude/worktrees/.
That nesting causes two problems we deliberately avoid here:
- Every file in the worktree then matches
.claude/**, so the file-editing tools (Write/Edit) get denied by the standard.claude/settings-protection rules. - It makes settings resolution ambiguous (is the worktree its own project root,
or does it inherit the main repo's
.claude/?).
So worktrees are created outside any repo, in one central tree grouped per
repo: ~/projects/worktrees/<repo>/<name> (override the base dir with the
WORKTREE_BASE env var). Each is an unambiguous, separate git top-level with its
own .claude/; the main repo's settings stay untouched; and the projects dir
isn't cluttered with a <repo>-worktrees/ sibling per repo.
How to use
Run the helper script from anywhere inside the repo, passing a worktree name (optionally
--base <ref>; defaults toorigin's default branch):python3 ~/.claude/skills/worktree/create-sandboxed-worktree.py create <name> [--base <ref>] [--launch "<task>"]It will:
- resolve the shared
.gitdir and main repo root, - create
git worktree add -b wt/<name> ~/projects/worktrees/<repo>/<name>, - write
<worktree>/.claude/settings.local.jsonwith the sandbox enabled and the shared.gitpath pinned intofilesystem.allowWrite(sogit commitworks), plus~/.gradle/~/.m2writes and thek1maven01artifact host allow-listed for network.
- resolve the shared
Settings load at process start, so tell the user to open a fresh session in the printed path:
cd ~/projects/worktrees/<repo>/<name> && claudeSanity-check the sandbox engaged. This is documented behavior, not a load-bearing guess: Claude Code reads settings (including sandbox mode) from the project directory it starts in, and the worktree is its own project root with its own
.claude/settings.local.json(sandboxing docs). So just confirm it once per Claude version (settings resolution could shift across releases):- a write inside the worktree runs WITHOUT a permission prompt, and
git commitsucceeds (proves the pinned.gitwrite grant works). If sandbox did NOT engage, the worktree-local settings weren't loaded — fall back to enabling sandbox via the main repo's settings instead.
Launching an unattended agent (--launch)
Passing --launch "<task>" creates the worktree and immediately spawns a
detached, sandboxed headless agent on that task — for fire-and-forget work you
check back on later:
- Fresh session id per launch — each agent gets its own id, written to
<worktree>/.claude/agent-session(the breadcrumb), which is rewritten on every launch to point at the current run. Stop the agent withpkill -f <id>using the id from the breadcrumb. (Earlier versions derived the id deterministically from the worktree path; that made it single-use —claude --session-idrejects an id that already exists — so a second agent in the same worktree silently diverged onto a different id while the breadcrumb went stale. The breadcrumb is the source of truth, so the id no longer needs to be reconstructable.) - Per-run log — each run writes its own newline-delimited stream-json log at
<worktree>/.claude/agents/<UTC-timestamp>-<shortid>.log(--output-format stream-json --verbose): one JSON event per line, written as the run progresses. One file per run means sequential agents never clobber or interleave, and each run is analysable on its own. The breadcrumb'slog=points at the current run. - Permissions — the agent runs with
--dangerously-skip-permissions. The sandbox is the real containment boundary (it confines filesystem + network regardless of permission mode); skip-permissions only removes prompts so an unattended run can't stall on one it has no human to answer.
The script prints, on launch:
log: <worktree>/.claude/agents/<timestamp>-<shortid>.log (stream-json)
Tail: tail -f <log>
Stop: pkill -f <session-id> # SIGTERM the agent + its sandbox helpers
Running a second/third agent in an existing worktree
Use the launch verb — the supported way to run another agent where one already
ran. It mints a fresh session id, writes a new per-run log, and refreshes the
breadcrumb, so the monitor (hive) tracks the new agent instead of the old one:
python3 ~/.claude/skills/worktree/create-sandboxed-worktree.py launch <worktree-path> "<task>"
Do not attach to a running agent. claude --resume <session-id> on a live
session puts two clients on one conversation: the agent keeps working but the
interactive session is left broken/corrupted. To take over an in-flight run, Stop
it first (pkill -f <session-id> with the id from the breadcrumb), then either
start a fresh session (cd <worktree> && claude) or launch another agent.
Security note: because the agent skips permission prompts, the
writepaths inallowlist.jsonare effectively trusted-execution surfaces (e.g. a write to~/.gradle/init.gradlewould run on the next host Gradle invocation). Keepwriteentries to genuine caches/scratch you trust.
Tunables — allowlist.json
What the sandboxed agent can reach and write lives in allowlist.json next
to the script (this is the file to edit; the script just consumes it):
network— hostnames the sandbox may reach. Network is deny-all by default when the sandbox is on, and there is no*wildcard, but subdomain wildcards like*.kardium.localwork. Unattended agents hard-fail on a non-listed domain, so list everything they need up front.write— paths outside the worktree the agent may write to (build caches, scratch).~= home.
The worktree dir and the repo's shared .git are added automatically, so they
are not in allowlist.json. The script combines the allowlist with the computed
.git path to produce the worktree's .claude/settings.local.json. To refresh an
existing worktree after editing the allowlist, re-render its settings:
python3 ~/.claude/skills/worktree/create-sandboxed-worktree.py render <worktree-path>
Notes:
allowUnsandboxedCommandsisfalse(strict): a command that can't be sandboxed fails rather than silently escaping or prompting — appropriate for unattended agents.- The template denies writes to
<.git>/hooksand<.git>/config; deny-vs-allow precedence in the sandbox is undocumented, so treat that as best-effort.