# Worktree

> Spin up an isolated, sandboxed git worktree for a long-running/autonomous agent. Creates the worktree in a central tree (~/projects/worktrees/<repo>/<name>, never under a repo's .claude/) and seeds it with a .claude/settings.local.json that enables the OS sandbox so bash + git run hands-off, confined to the worktree. Use when the user wants to "spin up a worktree for an agent", run unattended/long-running work, or avoid babysitting bash permission prompts.

- Skill: `ooshka/worktree` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add ooshka/worktree`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ooshka/worktree/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: ooshka (https://skillmd.com/u/ooshka)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/ooshka/worktree

---


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

1. Every file in the worktree then matches `.claude/**`, so the file-editing
   tools (Write/Edit) get denied by the standard `.claude/` settings-protection
   rules.
2. 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

1. Run the helper script from anywhere inside the repo, passing a worktree name
   (optionally `--base <ref>`; defaults to `origin`'s default branch):

   ```bash
   python3 ~/.claude/skills/worktree/create-sandboxed-worktree.py create <name> [--base <ref>] [--launch "<task>"]
   ```

   It will:
   - resolve the shared `.git` dir and main repo root,
   - create `git worktree add -b wt/<name> ~/projects/worktrees/<repo>/<name>`,
   - write `<worktree>/.claude/settings.local.json` with the sandbox enabled and
     the shared `.git` path pinned into `filesystem.allowWrite` (so `git commit`
     works), plus `~/.gradle`/`~/.m2` writes and the `k1maven01` artifact host
     allow-listed for network.

2. **Settings load at process start**, so tell the user to open a fresh session
   in the printed path:

   ```bash
   cd ~/projects/worktrees/<repo>/<name> && claude
   ```

3. **Sanity-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](https://code.claude.com/docs/en/sandboxing)). 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 commit` succeeds (proves the pinned `.git` write 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 with `pkill -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-id` rejects 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's `log=` 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:

```bash
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 `write` paths in
> `allowlist.json` are effectively trusted-execution surfaces (e.g. a write to
> `~/.gradle/init.gradle` would run on the next *host* Gradle invocation). Keep
> `write` entries 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.local` work. 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:

```bash
python3 ~/.claude/skills/worktree/create-sandboxed-worktree.py render <worktree-path>
```

Notes:
- `allowUnsandboxedCommands` is `false` (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>/hooks` and `<.git>/config`; deny-vs-allow
  precedence in the sandbox is undocumented, so treat that as best-effort.

