Parallel agents turn one working directory into a contested execution surface. A branch alone does not solve that problem. It records an intended line of history, which is essential, but it does not stop two local processes from rewriting the same files, sharing generated output, racing over a dev server, or quietly changing the environment underneath each other.
This becomes visible the first time two agents work in the same clone. One is tracing a checkout regression. Another is adding a billing endpoint. The first checks out a branch, installs a package, refreshes generated types, and runs a focused test. The second sees a different filesystem than the one it began with, then stages a commit containing some of the first agent's collateral changes. Nothing about that sequence is exotic. It is the ordinary failure mode of treating a shared working tree as if a branch were a private workspace.
The risk grows with capable coding agents because they make more small operational moves per hour. They inspect, edit, generate, install, format, test, rebase, and retry. The faster those moves happen, the less safe it is to depend on human memory to separate them. The answer is not to slow every agent down or to ban parallel work. It is to give each agent an execution lane and require evidence at the point where that lane becomes shared history.
This post lays out that operating model: worktrees for local isolation, small commit scopes for accountable handoffs, and proportional hooks for fast proof. If you're leading engineers or coordinating agents, it gives you a practical boundary model for deciding what should be separate locally and what must be proven before integration. It also explains what those controls do not solve, because false confidence is as dangerous as no control at all.
Thesis: Branches record intent; worktrees and scoped hooks make parallel execution isolated, reviewable, and safe enough to integrate.
Why now: Coding agents increase the number of local changes and verification attempts faster than a shared working directory can safely absorb.
Who should care: Engineers, tech leads, platform teams, and anyone running more than one coding agent against a repository.
Bottom line: Treat each agent's worktree as a lane, each commit as a bounded evidence packet, and CI as the final integration authority.
Key Ideas
- A branch is a history and review boundary. A worktree is an execution boundary. Parallel local agents need both.
- A commit should be small enough that its staged diff, allowed files, tests, and review claim all tell the same story.
- Line-count, complexity, and test hooks are useful when they are scoped to the staged change and backed by broader CI, not treated as a quality oracle.
The branch-only isolation fallacy breaks under local concurrency
A branch is still the right place to name an outcome: codex/checkout-fix, codex/billing-endpoint, or whatever vocabulary your team uses. It gives the work a review target, a merge relationship, and a durable record of intent. But a branch checked out in a shared directory does not give an agent a private filesystem, index, or process state.
That distinction is easy to miss because a single developer can often get away with it. The working tree feels personal. With two local agents, it becomes a mutable commons. One agent's generated client can appear in another agent's diff. One agent can consume a port, test database, cache, or credential profile that another agent assumes is its own. A checkout, stash, or rebase intended to unblock one task can interrupt the other task at precisely the moment it is preparing a commit.
Call this a shared-working-tree collision: independent tasks become coupled because they share local mutable state, even when their branches are different. The collision is not a Git merge conflict. It happens earlier, before Git has a clean artifact to compare.
Consider a fictional composite team, Northstar Commerce. Two agents have been asked to make low-risk changes on the same application. One agent investigates a checkout regression on codex/checkout-fix. The other adds an idempotency guard to a billing endpoint on codex/billing-endpoint. In one directory, the tasks repeatedly collide through branch checkouts, generated files, and test setup. The team spends time recovering context rather than producing evidence.
The branch names are correct. The execution model is not.
Field note: In production, I've seen the first sign of a shared-worktree failure arrive before any dramatic conflict. It is the quieter moment when nobody can explain why an unrelated file is staged.
Worktrees make the execution lane real
git worktree lets one repository host multiple working directories. Each working tree can have its own checked-out branch, HEAD, index, and file layout while sharing the underlying repository object store. In practice, that means the Northstar team can create ../northstar-checkout for the checkout branch and ../northstar-billing for the billing branch rather than taking turns inside a single directory.
That is not merely a convenience feature. It changes the failure model. A checkout investigation can install its own dependencies, run its own server on a dedicated port, and modify its own generated artifacts without rewriting the billing agent's working files. The billing agent can stage and inspect a diff without wondering whether it contains transient debris from the checkout investigation.
| Control surface | Branches alone | Separate worktrees | Separate clones |
|---|---|---|---|
| Review and merge lineage | Yes | Yes | Yes |
Independent files, index, and HEAD | No | Yes | Yes |
| Shared Git object database and common repository state | Yes | Yes | No |
| Setup cost | Low | Low to moderate | Higher |
| Best use | History and integration | Parallel local tasks | Stronger operational or credential isolation |
Worktrees are therefore a high-leverage default for parallel local code changes. They are not total isolation. Repository references and default configuration can still be shared. So can non-Git resources: ports, databases, queues, caches, artifact directories, cloud credentials, browser profiles, and upstream branches. A worktree isolates the workspace, not every dependency that a workspace touches.
At this point, the distinction is clear: worktrees make independent execution possible, but they do not prove that the resulting change stayed within its intended boundary. That proof begins with the commit.
Plain-language decode: A worktree is another checkout of the same repository. It gives an agent its own desk, not its own building.
That boundary matters operationally. Agents should still declare their branch, worktree path, service port, test fixture, and any shared external resource before they begin. Teams also need an integration owner or an explicit order for rebases and merges. Worktrees remove accidental file-level interference. They do not remove coordination.
flowchart LR
A[Task contract<br/>allowed paths and proof] --> B[Dedicated worktree<br/>branch + local state]
B --> C[Staged diff<br/>bounded commit scope]
C --> D[Local hooks<br/>scope, complexity, focused tests]
D --> E[Commit<br/>reviewable evidence packet]
E --> F[Review and CI<br/>integration proof]
F --> G[Merge or revise]
The decision implied by this model is simple: when two agents may change code concurrently, create separate worktrees before work starts. Do not wait for the first accidental checkout or polluted diff to prove that the working tree was shared.
Commit scope is where an agent's work becomes accountable
Worktrees prevent many local collisions, but they do not guarantee that an agent changes only what it was asked to change. That second problem is scope drift: the task starts as a billing guard and ends as a mixed commit containing a handler rewrite, a formatting sweep, a package update, and unrelated generated output.
Small commits are often presented as a review preference. In agentic workflows, they are more than that. A small scoped commit is the handoff contract between a fast local actor and the humans, agents, hooks, and CI systems that must evaluate its claim. If the task says "add an idempotency guard to this endpoint," the staged files, the diff, the tests, and the commit message should all describe that same bounded change.
Git gives that contract a precise surface: the staged snapshot. git diff --cached shows what the next ordinary commit will contain. That is the right object for a scope hook to inspect, not the entire dirty directory. The hook should ask questions such as: Are all staged paths allowed by the task? Did the staged change exceed the intended size? Did the agent modify a high-risk boundary without an explicit exception? Is the test evidence relevant to the files being committed?
Before input: a broad task becomes a broad, unverifiable commit
Task: Make billing safer.
Commit: "fix billing"
Changed: payment handler, checkout component, lockfile, 42 generated files,
formatting across six modules, and a test helper.
Proof: "tests passed locally"
The reviewer cannot tell which change matters, which test protects it, or whether the package update was intentional. The agent may have done useful work, but the resulting artifact does not make that work easy to trust.
After output: the task, diff, and proof agree
Task: Reject duplicate billing requests before the provider call.
Allowed paths: src/billing/idempotency.ts, src/billing/charge.ts, tests/billing/charge.spec.ts
Commit: "fix(billing): reject duplicate charge attempts"
Staged scope: 3 files, 96 added lines, 14 removed lines
Proof: focused billing test suite passes; changed-file complexity check passes
Exception: none
The second artifact is not bureaucratic overhead. It is machine-readable evidence. A reviewer can compare the declared paths to the staged paths. A hook can apply the same scope contract deterministically. Another agent can recover context without rereading a long chat transcript. CI can choose a focused test shard before running the full integration suite.
The commit is not a receipt for work performed. It is a claim that must carry its own evidence.
Local hooks should prove the claim, not police the entire repository
Commit hooks are most useful when they defend the boundary that the commit claims to represent. A pre-commit hook can abort a commit on a nonzero exit, which makes it a natural place for fast feedback. But a hook is not a substitute for CI, and it is not a universal measure of code quality. Developers can bypass local hooks, and a fast local check does not see the whole integration surface.
Here's what this means: use the hook to reject a bad local handoff quickly, then use CI to decide whether that handoff survives contact with the rest of the system.
The practical design is layered. The local hook checks the staged or affected change quickly. CI checks the branch in a clean, broader environment. Protected-branch policy enforces the non-negotiable result. Each layer has a different job.
| Verification gate | What it should catch | How to scope it | Why it matters for agents |
|---|---|---|---|
| Path and diff-size check | Unauthorized files or unexpectedly large changes | Staged file list and staged line count | Stops scope drift before review noise grows |
| Complexity check | A changed function becoming difficult to reason about | Changed source files, with a documented exception path | Keeps agents from hiding risky rewrites in a small task |
| Format, type, and lint check | Cheap correctness and consistency failures | Changed files or package-defined affected targets | Converts routine repair into immediate feedback |
| Focused test gate | Regressions in the behavior claimed by the commit | Tests mapped to staged paths or task label | Makes the proof relevant rather than merely available |
| CI integration suite | Cross-service, platform, and clean-environment failures | Entire branch or merge queue | Catches what local speed intentionally leaves out |
A minimal hook can make the layering concrete:
#!/usr/bin/env bash
set -euo pipefail
changed_files="$(git diff --cached --name-only --diff-filter=ACMR)"
scripts/check-commit-scope.sh "$changed_files"
scripts/check-changed-complexity.sh "$changed_files"
scripts/test-affected.sh "$changed_files"
The important design choice is not the shell syntax. It is the input. Those checks receive the staged file set, so their judgment is tied to the artifact about to enter history. If a repository wants to measure changed lines, it can inspect the staged diff. If it wants to measure complexity, it can analyze the changed source files. If it wants to run focused tests, it can map those files to a relevant test target.
There is one caveat worth treating seriously. Many test commands run against the current worktree files, not a reconstructed staged snapshot. Unstaged edits can therefore make a local test appear to validate code that is not actually in the commit. Teams should choose and document a policy: require the relevant worktree to be clean before the focused test, or build and test a temporary staged snapshot. The latter is stronger; the former is often a good initial control.
Verification debt: every change that reaches review without a relevant, inspectable proof leaves someone else to reconstruct why it is safe.
Line and complexity ceilings work when they shape decisions
Line-number and complexity hooks deserve more respect than they usually get, especially when agents can produce plausible large patches quickly. A staged line ceiling forces a useful question: is this still one reviewable decision, or has it quietly become three? A complexity ceiling asks a different question: did this small task turn a local function into a control-flow puzzle?
Neither metric is truth. A short diff can be dangerous. A long diff can be the cleanest possible generated migration. Complexity scores can be gamed by splitting a coherent function into opaque helpers. This is why hard global thresholds create performative compliance. Agents will optimize for the metric they can see.
Use the controls as a prompt for judgment instead. Make the default threshold low enough to surface a conversation, not so low that teams normalize bypassing it. Let an exception be explicit, scoped, and visible in the commit or task contract. The policy should reward a reviewer asking "why is this larger?" rather than reward an agent for scattering one hard problem across ten files.
For example, a repository might warn when a staged source file gains more than 250 lines, reject a changed function that crosses the agreed complexity ceiling, and require an exception note for generated code or a deliberately coupled refactor. The precise numbers are local choices. The durable principle is that the hook should measure the decision being reviewed, not manufacture a score that impersonates judgment.
This is also where teams must resist making every local hook expensive. If a hook takes several minutes, agents and humans will route around it. Fast checks create immediate feedback. Slower, broader checks belong in CI or a merge queue where their cost is expected and their environment is clean.
The staged snapshot is stricter than a green terminal
One subtle failure mode deserves explicit policy because it appears responsible at first glance. An agent runs the relevant test, sees it pass, makes one more unstaged experiment, and then commits the earlier staged version. The test result may be real, but it may no longer prove the commit. The terminal validated one filesystem state. Git recorded another.
This is why a commit hook should be precise about its supported workflow. A normal commit uses the index, but commands such as git commit -a, interactive patch selection, and path-limited commits choose or modify commit content differently. A repository does not need to ban every advanced Git workflow. It does need to decide whether its local evidence rules are built for all of them or only for a deliberate default path.
For most agent workflows, the default can be boring and strong: stage the intended files, inspect git diff --cached, keep relevant paths clean, run the focused test, then commit. If a task needs an interactive patch or an exception, make that visible in the task contract instead of allowing the proof trail to become ambiguous. The goal is not to deny capable operators their tools. It is to keep automation from silently assuming a snapshot it did not actually inspect.
This is also a reason to avoid a single giant "run everything" hook. It encourages people to treat a green command as a vague permission slip. Better hooks name the relationship between claim and evidence: these are the files, this is the staged size, this is the changed function, and this is the targeted behavior that passed. That specificity is what lets a reviewer, an agent, or a future incident responder challenge the result without replaying the entire development session.
Snapshot rule: A passing test is useful only when you can say which commit state it actually validated.
Branching still matters, but it cannot own the whole concurrency model
It would be a mistake to read this as an argument against branches. Branches are the integration vocabulary. They identify a unit of change, preserve alternate history, enable review, and make rollback tractable. A worktree without a branch discipline becomes a pile of directories. A hook without a branch and review policy becomes a local opinion.
So far, the model has separated three jobs that teams often collapse into one: history management, local execution, and proof. Branches own the first job. Worktrees own the second. Hooks and CI divide the third.
The problem is assigning branches a job they cannot do. A branch cannot prevent two agents from using the same port. It cannot protect a shared test database from conflicting fixtures. It cannot stop an agent from staging generated output left behind by another process. It cannot make a hook's test prove the staged snapshot if the worktree has unrelated unstaged edits. It cannot serialize a rebase or resolve the social decision about whose change integrates first.
That is the branch-only isolation fallacy: confusing history separation with execution separation. In a one-person workflow the distinction often feels academic. In a parallel local workflow, it becomes a daily reliability concern.
The stronger model assigns each control to its proper layer:
- Use a branch to name the outcome and define the merge target.
- Use a dedicated worktree to isolate files, index,
HEAD, dependencies, and local process setup. - Use a task contract to declare allowed paths, risk boundaries, and required proof.
- Use a scoped commit hook to examine the staged change and run fast, relevant gates.
- Use review and CI to validate the integrated branch in a clean, authoritative environment.
That sequence is not ceremony for its own sake. It lets parallelism increase throughput without making the repository's local state unknowable.
The first operating standard should be deliberately small
Do not begin by building an elaborate agent governance platform. Start with one repository and one high-value rule: no two active coding agents share a working directory. Create a worktree per task. Reserve a port or isolated test fixture when the task needs one. Require a staged-diff check that blocks unexpected paths and an affected-test command that is quick enough to run every time.
Then observe the failures. If agents routinely create large commits, add a staged line warning and an explicit exception format. If reviewers repeatedly find difficult conditionals, add a changed-function complexity check. If test evidence is vague, formalize the mapping from path or task label to focused test target. Let the hooks become a record of recurring failure modes, not a collection of fashionable checks.
Make ownership visible as well. One person or one integration agent should own the merge queue for a shared area, especially while the workflow is new. That owner does not need to approve every edit. They need to keep branch order, shared-resource reservations, and failed-check follow-up from becoming implicit. Parallel work becomes fragile when everybody can start a task but nobody owns the moment when independent lanes converge.
For the Northstar team, the next run looks different. The checkout agent works in its own worktree and can discard or amend the branch without disturbing billing. The billing agent's commit contains only the three declared files. The local hook rejects a surprise lockfile change, then passes the focused billing suite. CI later catches a cross-service contract failure that the local test was never intended to see. The team learns from a contained rejection instead of untangling a contaminated workspace.
For a skimmer returning here, the operating doctrine is simple: isolate before editing, bound before committing, and verify before integrating.
Trust is the output of constrained parallelism
Agentic coding will make branching more important, not less. But it will also make branch-only workflows less adequate. As local change volume rises, repositories need a way to separate execution, preserve evidence, and bring work back together without forcing every contributor to reconstruct a hidden chain of filesystem events.
Git worktrees are one of the simplest tools available for that separation. Scoped commit hooks make the next boundary explicit. Line and complexity checks make oversized decisions visible. Focused tests make local proof relevant. CI keeps the final word because integration remains larger than any single worktree or commit.
The goal is not a perfect guardrail system. It is a workflow where failures occur early, locally, and with enough context to fix them. That is what makes parallel work safer without making it timid.
Branching records intent. Isolation protects execution. Verification earns integration.
