DEV Community

Cover image for AI Agents And Branch Strategy: Safe Automation With Git
Nazar Boyko
Nazar Boyko

Posted on • Originally published at nazarboyko.com

AI Agents And Branch Strategy: Safe Automation With Git

There's a GitHub issue on the Claude Code repo with a title that should make anyone automating git nervous: "Claude repeatedly commits and pushes directly to main despite explicit instructions." The reporter had a rule in their CLAUDE.md saying, in plain English, never commit to main: everything goes through a feature branch and a PR. The agent read it, agreed with it, and then pushed to main anyway.

That's the whole problem with letting an agent touch git, compressed into one bug report. The agent isn't malicious and it isn't broken. It just doesn't treat your branch policy as a hard constraint the way a CI server does. It treats it as a strong suggestion competing with everything else in its context, and sometimes another instruction wins. If your safety model is "I told it not to," you don't have a safety model. You have a hope.

So the question isn't "how do I phrase the instruction better?" It's "how do I set up git so the agent physically can't do the dangerous thing, no matter what it decides?" That's a branch-strategy question, and it has good answers.

Why "just tell it not to" doesn't hold

The instinct is to write the rule more forcefully. All caps. Three exclamation marks. A CLAUDE.md section titled CRITICAL: READ THIS FIRST. It feels like it should work, and it does work most of the time, which is exactly what makes it dangerous: it fails rarely enough that you stop watching.

Here's the mechanism behind the failure. An agent's behavior comes from its whole context window: your project rules, the conversation so far, the tool descriptions, and, crucially, any system prompt the platform wraps around the session. There's a second Claude Code issue documenting this precisely: when you launch an agent through the web "task" flow, the platform injects a system-prompt block that instructs the agent to commit and push as part of its default workflow. That injected instruction can override the CLAUDE.md rule you wrote, because it sits closer to the model's notion of "what am I here to do." You can't out-shout a system prompt you can't see.

Even without a competing system prompt, natural-language rules are probabilistic. A guardrail that holds 99% of the time sounds great until you remember an agent might run twenty git operations in a session, across dozens of sessions a week. At that volume, 1% is not an edge case. It's Tuesday.

The fix is to stop relying on the agent's judgment for the part that has to be deterministic. Let the agent decide what to change. Don't let it decide whether it's allowed to write to main. Move that decision somewhere the agent's context can't reach it.

The isolation primitive: one worktree per agent

The single best structural move is to stop letting agents work in your main checkout at all. Give each agent its own working directory, on its own branch, with its own index, and let it make whatever mess it wants in there.

Git has had the tool for this since 2015: git worktree. A worktree is a second (or third, or tenth) working directory attached to the same repository. Each one has a private HEAD, a private index, and its own files on disk, but they all share a single object store, the one .git folder with all your commits and blobs. So you get full isolation at the working-directory level for almost no disk cost, because nothing is duplicated except the checked-out files.

# Spin up an isolated workspace for an agent, on a fresh branch
git worktree add ../agent-auth-refactor -b agent/auth-refactor

# The agent runs entirely inside ../agent-auth-refactor
# Your main checkout never moves, never gets dirty, never gets a stray commit

# When the branch is merged (or abandoned), tear it down
git worktree remove ../agent-auth-refactor
Enter fullscreen mode Exit fullscreen mode

The reason this matters for safety, not just tidiness: an agent working in its own worktree cannot commit to your main branch, because main isn't checked out there. The danger isn't blocked by a rule the agent might ignore. It's blocked by the fact that the dangerous target physically isn't present in that directory. Git won't let two worktrees check out the same branch at once, so main stays pinned in your primary checkout, untouched.

This is also why worktrees have quietly become the standard way to run multiple agents at once. Each agent gets a sealed sandbox; conflicts that used to happen silently while two processes fought over the same files now move to merge time, where normal git tooling catches them. One developer can have four or five agents building different features in parallel, each on its own branch, and review them one by one.

Tip
Worktrees share the object store but not the working directory, which includes node_modules, .env, and build artifacts. A fresh worktree starts without installed dependencies. Budget for an npm install (or your equivalent) per worktree, or symlink heavy, gitignored directories you trust to be identical across branches.

Architecture diagram: one shared .git object store feeds three isolated worktrees (main checkout, agent/auth-refactor, agent/fix-flaky-test), each with its own HEAD, index, and files; main lives only in the main checkout.

The major tools have caught up to this. Native worktree support landed across Claude Code, Cursor, and the GitHub Copilot CLI between late 2025 and early 2026. In Claude Code specifically, you can add isolation: worktree to a subagent's frontmatter and it'll create a fresh worktree under .claude/worktrees/ every time that agent runs, then auto-clean it if the agent made no changes:

.claude/agents/refactorer.md

---
name: refactorer
description: "Refactors a module in isolation, then opens a PR for review."
isolation: worktree
---

You work in an isolated worktree. Make your changes, run the tests,
and commit to your own branch. Never switch to or commit on main.
Enter fullscreen mode Exit fullscreen mode

Cursor 3.0 exposes the same idea through a /worktree slash command. The point across all of them is identical: the agent's blast radius is one directory and one branch.

Branch naming that survives a dozen agents

Once agents are creating branches on their own, naming stops being cosmetic. With one human developer, fix-thing is fine because there's one of you and you remember what it was. With agents opening branches across many sessions, you need names that tell you, at a glance, who made this, why, and whether it's safe to delete.

A convention that holds up well is a three-part prefix: the actor, the type, and a short slug.

agent/feat/checkout-coupon-stacking
agent/fix/flaky-payment-webhook-test
agent/chore/bump-eslint-to-v9
human/feat/new-onboarding-flow
Enter fullscreen mode Exit fullscreen mode

The leading agent/ is doing real work. It lets you filter automated branches in one glob (git branch --list 'agent/*'), it makes a stale-branch cleanup job trivial to write safely, and it tells a human reviewer immediately that this code came from an agent and deserves the corresponding read. A flat namespace where agent branches look exactly like human branches is how you end up afraid to run git branch -d on anything.

Keep the slug derived from the task, not the timestamp. agent/fix/null-deref-in-cart-total is greppable and self-documenting six weeks later; agent/fix/2026-06-14-1 is noise. If you need uniqueness because two agents might tackle similar work, append a short ticket ID rather than a date, like agent/feat/SHOP-412-coupon-stacking.

Commits the agent makes vs commits you'd make

An agent left to its own devices tends toward two failure modes on commits, and they pull in opposite directions. Some agents commit obsessively, a commit per file touched, with messages like "update file", and you end up with a branch history that's forty commits of mush. Others do everything in one giant commit titled "implement feature" that's impossible to review hunk by hunk.

Neither is what you'd do by hand, so put the standard in the agent's instructions and, more importantly, squash at merge time so the agent's commit hygiene stops mattering for your main history. A reasonable contract:

  • One logical change per commit on the branch, but don't obsess. The branch is scratch space.
  • A real commit subject in the imperative mood, scoped if you use conventional commits: fix(cart): correct total when last coupon is removed.
  • The body explains why, not what. The diff already shows what.
  • Squash-merge the PR so main gets one clean commit per change, regardless of how the branch got there.

The squash is the safety valve. It means you can let the agent be sloppy on its own branch, where sloppiness is free, and still keep main readable. You're separating "the agent's working log" from "the project's permanent history," and only the second one has to be good.

Warning
Don't let an agent author commits that aren't attributable to it. Configure the agent's git identity (or a co-author trailer) so git log and git blame make clear which commits came from automation. When something breaks in production six months later and the blame points at agent/feat/..., you want to know that immediately, not discover it during the incident.

Guardrails that don't depend on the agent behaving

Isolation and naming are most of the battle, but you still want a backstop for the case where an agent (or a misconfigured one, or one running under a system prompt you didn't write) tries to commit to a protected branch anyway. The principle here is layered defense: a check the agent runs on itself, plus a check that runs regardless of the agent.

The agent-side check is cheap and worth adding to your instructions: run git branch --show-current before any commit, and if the result is main or master, stop and create a branch first. Treat it as a hard gate in the prompt, not a polite suggestion. But understand its limit: it lives in the same context that might get overridden, so it's a first line, not the line.

The check that doesn't depend on the agent is a local pre-commit hook. The pre-commit framework ships a no-commit-to-branch hook that blocks commits to main and master by default:

.pre-commit-config.yaml

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: no-commit-to-branch
        # blocks main and master by default; add more with args: [--branch, develop]
Enter fullscreen mode Exit fullscreen mode

After pre-commit install, every git commit runs the hook first. On a protected branch, the commit halts before it happens, with no rule-following required from whoever (or whatever) typed the command.

Here's the gotcha that catches people, though: a local hook is bypassable. Anyone, including an agent with shell access, can run git commit --no-verify, or simply uninstall the hook. A local guardrail protects against accidents, not against a process that's actively working around it. So the local hook is necessary but not sufficient.

The layer that actually holds is server-side, where the agent's shell can't reach: a branch protection rule (or, on GitHub, a ruleset) on the remote that requires a pull request before anything merges to main. That single setting, "require a pull request before merging", makes the agent structurally incapable of putting code on main without a human-approved PR, because the push to main is rejected by the server no matter what the local environment allows. Local hooks catch the honest mistakes fast; the server-side rule is the one you actually bet on.

Layered-defense diagram: a commit to main must pass an agent self-check (soft), a local pre-commit hook (bypassable with --no-verify), and server-side branch protection requiring a PR (the agent's shell cannot reach it).

The merge is where humans belong

Notice what all of this adds up to: the agent does the work autonomously, in isolation, and the human stays out of the loop during development, with no babysitting each edit. The human re-enters at exactly one point: the pull request. That's the right place. Reviewing a finished, isolated branch is a far better use of a senior engineer's attention than watching an agent type.

This is also where the parallelism pays off. Because each agent is sealed in its own worktree on its own branch, you can have several running at once and review their PRs as they land instead of as they're written. In practice, teams find a ceiling here: most cap somewhere around eight to ten concurrent worktrees before the overhead of tracking what each agent is doing eats the benefit of running them in parallel. That's a useful number to know: the constraint on parallel agents usually isn't your machine, it's your own ability to review what comes out the other end.

So the branch strategy for safe automation comes down to four moves, none of which trust the agent to behave: isolate each agent in its own worktree so it can't touch main, name branches so you can always tell automated work apart, squash at merge so messy commit history never reaches main, and put a required-PR rule on the remote so the one truly dangerous operation is impossible from the agent's shell. Get those in place and you can hand an agent real autonomy, not because you've convinced it to be careful, but because you've arranged things so that careless and careful produce the same safe outcome.


Originally published at nazarboyko.com.

Top comments (0)