What git workflow works when coding agents make most of your commits?
Agents commit to a work branch, each session in its own worktree, and main changes by one PR whose CI re-checks every commit message.
Senior Full-Stack Engineer- Published
- 10 minReading time
- EngineeringCategory
Listen to this post· AI-narrated
0:00 / 11:16Every git workflow written for solo developers assumes one person typing commits. Then you open a second terminal, start another agent session, and a few hours later one of them has, in a Reddit user's words (opens in a new tab), been "committing to the wrong branch". Another commit swept up files the other session was still editing, and the log reads "updated stuff" three times. The usual advice about small commits and good branch names doesn't stop any of that, because it relies on the committer remembering it. Agents commit whenever they're asked to, and a new session starts without your conventions unless something hands them over. So the workflow has to be enforced by something that runs whether anyone remembers it or not. Here's the setup I'd give a solo developer today. I ran every step below on a small sample repository, and the outputs are copied from it.
TL;DR: A git workflow for coding agents needs three rules. Give each agent session its own worktree, or have it commit only the paths it touched. Let main change only through one pull request from a work branch. Check every commit message in that pull request's CI, because a local hook can be skipped with --no-verify.
What changes in a solo git workflow when agents commit?
A solo git workflow used to fail in one way: you got sloppy. With agents it fails in three more ways, and each of them can happen while every session follows its instructions.
- Wrong branch. Two sessions in one folder share one checked-out branch. When one switches branches, the other follows without knowing.
- Someone else's files.
git add -Astages everything in the working tree, including what the other session is halfway through. - Unreadable history. An agent writes whatever subject seems reasonable in the moment. Repeat that across every commit and
git logstops answering "what changed and why".
The guides ranking for this question today (this one (opens in a new tab) or this one (opens in a new tab)) give good habits for a human: commit small, name branches, run a checklist before you push. Those habits live in the developer's head. An agent follows them only when they're in that session's context, and you skip them at the end of a long day. Each rule needs to run somewhere a skip is visible: a hook, a branch rule, a CI job.
Should each agent session get its own worktree?
Yes, whenever two sessions might touch the same files or switch branches. Git already supports it: a repository "can support multiple working trees, allowing you to check out more than one branch at a time" (git-worktree (opens in a new tab)). Each session gets its own folder and its own branch, and git refuses to check out a branch that another worktree already has, so two sessions can't silently share one. Claude Code builds this in: claude --worktree feature-auth starts a session in its own worktree, and the same command with another name starts a second one that can't collide with it (Claude Code docs (opens in a new tab)). The commenter who answered that Reddit thread put it plainly: it "sure beats multiple agents working on the same folder".
For small parallel jobs that never touch the same file, say one session on the API and one on a page, a lighter rule works in a single checkout: no session switches branches, and each one commits only the paths it touched. That rule is a convention nothing enforces, so keep it for jobs where a slip is cheap. When you pass paths to git commit, it commits those paths and ignores whatever is staged for others (git-commit, --only (opens in a new tab)). In the sample repo, session B had src/web.ts staged when session A committed:
git commit -m "refactor(api): name the email check result" -- src/api.ts
# 1 file changed: src/api.ts
git diff --cached --name-only
# src/web.ts <- still staged, still B's
One catch: a brand-new file has to be added first. git commit -- src/auth.ts on an untracked file fails with "pathspec 'src/auth.ts' did not match any file(s) known to git", so the rule for your agent's instructions file (CLAUDE.md, AGENTS.md) is git add <new files>, then git commit -- <paths>, and never git add -A.
Why should main change only through one pull request?
Main is what you deploy, so it should only change at a moment you chose. The simplest model that does that has two branches. Agents commit to a work branch (I call mine next) as often as they like. Main changes only when you open a pull request from next and merge it with a merge commit, usually when you're ready to release. Squash or rebase merges would leave next and main with different histories.
Worktree branches land on next without merge commits: rebase the branch onto next, then git merge --ff-only. That keeps next linear, and it avoids a trap I hit in the sample repo. Git runs the commit-msg hook on merges too (githooks (opens in a new tab)), so the default "Merge branch 'feature-auth' into next" subject was rejected by the rule in the next section.
The pull request into main is the one review a solo developer can keep up with. You won't read every agent commit one by one, but you will read one diff and one list of commit subjects before a release. It's also where CI runs on the whole range.
To make the rule real on GitHub, add a ruleset on main that requires a pull request and a passing status check, and leave its bypass list empty, since rulesets let you name people who skip them. One catch from GitHub's docs (opens in a new tab): rulesets work on public repositories with GitHub Free, but a private repository needs Pro, Team or Enterprise. On a free private repo nothing on GitHub enforces the rule, so it rests on your agent instructions and on you remembering.
How do you make agents write commit messages you can read later?
Give them a grammar small enough to check with one regular expression. Conventional Commits (opens in a new tab) is the common one: <type>[optional scope]: <description>, as in fix(api): reject empty email. I'd make two changes for agents. Require the scope, and close the list of scopes to the areas your repo actually has. An open list lets each session coin its own scope, and the log fills with near-duplicates.
The sample repo keeps the rule in one script, and both the hook and CI call it:
#!/bin/sh
# scripts/check-subject.sh: <type>(<scope>): <summary>
pattern='^(feat|fix|refactor|test|docs|chore|ci)\((api|web|db|ci|docs)\): [a-z].{1,70}$'
echo "$1" | grep -Eq "$pattern"
The hook reads the subject from the message file git passes it, and a non-zero exit aborts the commit (githooks (opens in a new tab)):
#!/bin/sh
# .githooks/commit-msg
subject=$(head -n1 "$1")
if ! sh scripts/check-subject.sh "$subject"; then
echo "commit-msg: rejected \"$subject\"" >&2
echo "expected: <type>(<scope>): <summary>, e.g. fix(api): reject empty email" >&2
exit 1
fi
Run git config core.hooksPath .githooks once per clone so git uses the tracked folder. Here's what the agent saw when it tried a vague subject:
$ git commit -m "updated stuff"
commit-msg: rejected "updated stuff"
expected: <type>(<scope>): <summary>, e.g. fix(api): reject empty email
The error message matters more than the regex. It's the only documentation the agent reads at that moment, so it should show a valid example. The retry, fix(api): return false for an empty email, went through.
Why isn't a commit-msg hook enough?
Git lets anyone skip it. The commit-msg hook "can be bypassed with the --no-verify option", git's own docs say, and so can pre-commit (githooks (opens in a new tab)). Deny that flag in your agents' settings where you can (Claude Code's permission rules (opens in a new tab), for example, can block Bash command patterns). A deny rule still only covers the agents you configured, and it doesn't cover you. When the flag gets through, the commit lands with whatever subject it had:
$ git commit -am "wip" --no-verify
4aff9ce wip
Keep the hook for fast feedback and let CI decide. The sample repo's CI step runs the same script over every commit the pull request would bring into main. It skips merge commits, because on a pull request GitHub Actions checks out a merge commit it creates itself:
#!/bin/sh
# scripts/check-range.sh main..next
status=0
for sha in $(git rev-list --no-merges "$1"); do
subject=$(git log -1 --format=%s "$sha")
if ! sh scripts/check-subject.sh "$subject"; then
echo "bad subject in $(git rev-parse --short "$sha"): $subject"; status=1
fi
done
exit $status
It caught the skipped commit and exited with code 1:
$ sh scripts/check-range.sh main..next
bad subject in 4aff9ce: wip
On GitHub Actions, check out the full history first. actions/checkout fetches a single commit by default, and fetch-depth: 0 fetches all of it (actions/checkout (opens in a new tab)), which the range needs:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- run: sh scripts/check-range.sh origin/main..HEAD
Make that job a required status check, and the pull request stays blocked until the bad commit is reworded. GitHub can match commit messages in a ruleset too, but its docs place that rule under Enterprise Cloud, so for most solo repos a few lines of shell in CI are the practical route.
What does this workflow cost?
The expensive part is fixing a commit after it lands. Rewording 4aff9ce means an interactive rebase on next and a force-push, and any worktree branched from the old next has to be rebased onto the new one before it can fast-forward. Pause the other sessions before you rewrite. The hook exists to make this rare: every rejection it catches locally is a rebase you don't do later.
The rest is upkeep. A new area of the repo has to go into the scope list before any commit touching it gets through. Worktrees mean a rebase before each merge, and two sessions that both changed the same file will still conflict there. Releases wait for a pull request and a green check, even when you're the only person who reads them. I'll take all of that over a log I can't read.
How to set it up
- Create a
nextbranch and tell your agents to commit there, with explicit paths and new files added first. - Use
claude --worktree <name>(orgit worktree add) whenever two sessions might touch the same files, and land each worktree branch with a rebase andgit merge --ff-only. - Add
scripts/check-subject.shwith your closed scope list, and acommit-msghook that calls it throughcore.hooksPath. - Add a CI job with
fetch-depth: 0that runs the range check on every pull request intomain, and make it a required status check with no bypass.
More posts on building with coding agents are on the gabs.app blog. I'm a full-stack engineer and my work is on gabs.app; if you're hiring, the contact form comes straight to me.