# One task, one worktree: isolating AI coding agents with git worktree

> The cheapest way to shrink an agent's blast radius is to keep it out of your checkout. The details that make that actually safe.

- By: Djan Magno
- Published: 2026-09-19
- Updated: 2026-09-19
- URL: https://t25.io/en/blog/one-task-one-worktree-isolating-ai-coding-agents/
- Engineering · git worktree, ai coding agents, claude code, codex, isolation, security, parallel agents

> **TL;DR:** > - **`git worktree`** gives each task its own directory and branch while sharing one repository. It's the cheapest isolation for running coding agents in parallel.
> - In T25 every task gets a worktree at `.factory/worktrees/<project>/<TASK-ID>` on a branch like `feat/TASK-0042`. No implementation agent runs in the main checkout.
> - Creating the worktree is easy. The work is in the guards: path inside the root, recorded ownership, a per-task lock, a base synced with `origin`, and a cleanup that refuses to delete work.

## Why isolate coding agents

A coding agent working in the same directory as you creates three problems, and all of them get worse with more than one agent:

- **Collisions.** Two agents editing the same checkout overwrite each other, and `git status` becomes a mix nobody can untangle.
- **Contamination.** You're halfway through a change, the agent runs the tests, they fail because of your unfinished code, and it "fixes" something that wasn't its to fix.
- **Blast radius.** A careless `git checkout .` or `rm` from the agent hits your uncommitted work.

A container solves all three, but costs an image, volumes and mounted credentials. A separate clone solves them too, but duplicates `.git` and loses shared objects. `git worktree` sits in the middle: independent working directories, one repository behind them.

## The basics in one command

```bash
git worktree add -b feat/TASK-0042 .factory/worktrees/app/TASK-0042 main
```

That creates the directory, creates the branch from `main`, and checks it out. The agent gets that directory as its `cwd` and nothing else. When the task is done and the work is committed, `git worktree remove` takes the directory away.

If that were all, this post would end here. The rest is what goes wrong when a factory creates hundreds of these automatically.

## The guards that make it safe

T25's `WorkspaceManager` is treated as security-sensitive code. Every item below exists because the case without it is plausible.

### 1. Git without a shell

Every git call goes through `execFile` with an argument list, never a shell string. The task ID and branch name are never interpolated into a command, so a task title containing `; rm -rf` is just text. The process also has a hard timeout and is killed with `SIGKILL`, so a hung git doesn't leave the repo half-written while something else tries to clean up.

### 2. Validated IDs and branches

The task ID must match `^[A-Za-z0-9][A-Za-z0-9._-]*$` before it becomes part of a path. The base branch goes through `git check-ref-format --branch`. Nothing containing `..` or a slash makes it into the path.

### 3. The path has to stay inside the root

Before any worktree path is used, it's resolved with `realpath` and compared against the configured root. If the relative path starts with `..`, is absolute, or is empty, the operation fails. That catches the symlink case: a directory that looks like it's inside the root but points outside.

### 4. Recorded ownership, recorded twice

A directory existing in the right place doesn't prove the factory created it. Before reusing a worktree, T25 requires two things:

- a metadata file (`.factory-metadata/<TASK-ID>.json`) with the task ID, branch and canonical path, all three matching;
- the path listed in `git worktree list --porcelain` with the same branch.

Without both, the factory refuses to adopt the directory. That keeps it from "inheriting" a folder someone made by hand or another tool left behind.

### 5. A lock per task

Two processes preparing the same worktree at once is a classic race. The lock is a file created with the `wx` flag (fails if it already exists) holding the PID of whoever took it. If the lock exists and its owner is alive, the second attempt fails with the right message. If the owner died, the lock is treated as stale and taken over.

### 6. Sync the base before branching

This guard came from a real mistake. During a dogfooding round, a task was branched from a local `main` that was behind `origin`: a pull request had been merged directly on GitHub, outside the factory. The result was three rounds of QA and implementation chasing a test assertion that had already been fixed upstream.

Now, before creating a new worktree, T25 runs `git fetch origin main` followed by `git merge --ff-only origin/main`. The `--ff-only` is the important part: if the local clone diverged for any reason, the operation fails loudly instead of continuing from the wrong base.

### 7. Cleanup that doesn't delete work

`cleanup()` checks ownership again, runs `git status --porcelain --untracked-files=all`, and refuses to continue if anything changed, untracked files included. It never uses `--force`. Losing an agent's work to an automatic cleanup is worse than leaving a directory behind.

## Branch names that pass your repo's rules

The branch uses the task type's prefix, Conventional Commits style: `feat/`, `fix/`, `refactor/`, `docs/`, `chore/`, followed by the ID. It's a practical choice: many repos have pre-push hooks or protection rules that reject non-standard names, and a `factory/...` branch would die on push.

## Does a worktree solve everything?

No. A worktree isolates the **repository's files**, not the process. The agent still runs as your user, can see the rest of the machine, and uses its CLI's credentials. That's why T25 stacks another layer by risk: medium-risk tasks and above can run the agent inside a Docker sandbox, with a signed policy the worker validates before executing. Real network restriction for that sandbox is still on our list. A worktree is the first layer, not the only one.

| Option | Isolates repo files | Isolates the process | Setup cost | Shares git objects |
|---|---|---|---|---|
| Main checkout | No | No | None | Yes |
| `git worktree` | Yes | No | Low | Yes |
| Separate clone | Yes | No | Medium | No |
| Container + volume | Yes | Yes | High | Depends |

## A checklist to do the same

If you're building isolation for your own agents:

1. One worktree and one branch per task, created from a base synced with `--ff-only`.
2. Git called without a shell, with a timeout and `SIGKILL`.
3. Task IDs validated by regex before becoming a path; `realpath` and a root check on every use.
4. Ownership written to metadata and confirmed in `git worktree list`.
5. A per-task lock with a PID and stale-lock detection.
6. A cleanup that refuses dirty directories and never forces.

## FAQ

### Can I run several agents in parallel on the same repo?

Yes, as long as each one has its own worktree. They share git's object store, but each has its own working directory, index and branch.

### Is the worktree deleted when the task finishes?

Only if it's clean. T25's cleanup refuses to remove a worktree with uncommitted changes or untracked files, and never uses `--force`.

### Why not use a container for everything?

A container isolates more, but costs an image, volumes and mounted credentials for every task. A worktree handles collisions and contamination at almost no cost. T25 layers both: a worktree always, a Docker sandbox from medium risk up.

### Where does T25 keep its worktrees?

In a local, git-ignored folder, by default under `.factory/worktrees/`, split per project. None of it is committed.
