Skip to content

Git rules for many AI agents in one repo

A Claude Code hook and an AGENTS.md block that stop agents sweeping up each other's work.

The problem

Put several agents in one repo and they eat each other's work. Three git commands do almost all of it: git add -A, git commit -a and a bare git stash. Each one acts on every change in the working tree, and when four agents share a checkout, every change includes another agent's half-finished edit.

The rules and the hook

AGENTS.md
## Git, when other agents share this checkout

- Stage only files you changed, by path. Never `git add -A`, `git add .`, `git add -u` or `git commit -a`.
- Run `git diff --cached` before every commit and unstage anything that isn't yours.
- Never run a bare `git stash`. Use `git stash push -- <paths>` for your own files only.
  To recover someone else's stashed file: `git show 'stash@{0}:<path>'`. Don't pop it.
- Commit your own work before you hand off or switch branches.
- You don't approve your own work. Say what you changed and how to check it; someone else verifies it.
- A task is done when its commit is on main (`git branch --contains <sha>` lists main). Until then it isn't.
.claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-git.mjs\"" }]
      }
    ]
  }
}
.claude/hooks/guard-git.mjs
#!/usr/bin/env node
// guard-git.mjs: a Claude Code PreToolUse hook for repos where several agents share one checkout.
//
// Blocks the git commands that sweep up or throw away other agents' uncommitted work:
//   git add -A / --all / -u / . / :/      stages every change in the tree
//   git commit -a / --all                 stages every modified tracked file at commit time
//   git stash (with no "-- <paths>")      stashes every change in the tree
//   git stash clear                       deletes every stash, other agents' included
// Global options don't hide them (git -C dir, git -c key=value ...), and neither does sh -c "..." or eval.
//
// Quoted text is ignored, so a commit message that mentions "git add -A" still passes.
// Exit code 2 blocks the tool call, and Claude Code shows stderr to the agent so it can correct itself.
// No dependencies. Free to copy and change.

let raw = '';
for await (const chunk of process.stdin) raw += chunk;

let command = '';
try {
  const input = JSON.parse(raw);
  if (input.tool_name !== 'Bash') process.exit(0);
  command = String(input.tool_input?.command ?? '');
} catch {
  process.exit(0); // Not a hook payload we understand: don't get in the way.
}

// Blank out quoted text and heredoc bodies so words inside messages don't count. One pass, left to right, so an
// apostrophe inside "double quotes" (don't, won't) can't pair with one in a later string and hide the commands between.
// Text passed to `sh -c`, `bash -c`, `zsh -c` or `eval` is a command, so it's checked too.
const nested = [];
function blank(cmd) {
  let out = '', q = null, buf = '';
  const body = cmd.replace(/<<-?\s*['"]?(\w+)['"]?[^\n]*\n[\s\S]*?\n\s*\1\b/g, ' ');
  for (let i = 0; i < body.length; i++) {
    const ch = body[i];
    if (q) {
      if (ch === '\\' && q === '"') { buf += body[i + 1] ?? ''; i++; continue; }
      if (ch === q) { if (/(?:\b(?:ba|z|da)?sh\s+(?:-\w+\s+)*-c|\beval)\s*$/.test(out)) nested.push(buf); out += q + q; q = null; buf = ''; continue; }
      buf += ch;
      continue;
    }
    if (ch === '\\') { out += ch + (body[i + 1] ?? ''); i++; continue; }
    if (ch === "'" || ch === '"') { q = ch; continue; }
    out += ch;
  }
  return out;
}
let scan = blank(command);
for (let n = 0; n < nested.length && n < 20; n++) scan += '\n' + blank(nested[n]);

// The git subcommand after any global options: git -C dir, -c key=value, --git-dir=..., --no-pager and so on.
function sub(c, name) {
  const m = c.match(new RegExp(`\\bgit((?:\\s+(?:-[Cc]\\s+\\S+|--?[\\w-]+(?:=\\S+)?))*)\\s+${name}\\b(.*)$`));
  return m ? m[2] : null;
}

// Look at each simple command separately (split on ; & && || | and newlines).
const reasons = [];
for (const part of scan.split(/\n|;|&&?|\|\|?/)) {
  const c = part.trim();
  const add = sub(c, 'add');
  if (add !== null && /(^|\s)(-A|--all|-u|--update|\.|:\/|\*|--\s+\.)(\s|$)/.test(add)) {
    reasons.push('`git add -A` / `git add .` stages every change in the tree, including other agents\' work.');
  }
  const commit = sub(c, 'commit');
  if (commit !== null && /(^|\s)(--all|-[a-zA-Z]*a[a-zA-Z]*)(\s|$)/.test(commit)) {
    reasons.push('`git commit -a` stages every modified tracked file, including other agents\' edits.');
  }
  const stash = sub(c, 'stash');
  if (stash !== null) {
    const rest = stash.trim();
    if (/^clear\b/.test(rest)) {
      reasons.push('`git stash clear` deletes every stash in the repo, including other agents\' stashed work.');
    } else if (!/^(list|show|pop|apply|drop|branch|create|store)\b/.test(rest) && !/(^|\s)--(\s|$)/.test(rest)) {
      reasons.push('`git stash` without `-- <paths>` stashes every change in the tree, including other agents\' work.');
    }
  }
}

if (reasons.length) {
  process.stderr.write(
    `Blocked: ${reasons.join(' ')}\n` +
    'Other agents may have uncommitted work in this checkout. Stage only your own files by name ' +
    '(`git add path/to/file`), check `git diff --cached`, then commit. ' +
    'To stash, name the paths: `git stash push -- <paths>`.\n',
  );
  process.exit(2);
}
process.exit(0);
CommandResult
git add -ABlocked
git add .Blocked
git -C ../other add --allBlocked
git commit -am "wip"Blocked
git stashBlocked
git stash -m wipBlocked
git stash clearBlocked
git add src/app.ts && git commit -m "never use git add -A"Allowed
git commit --amend --no-editAllowed
git stash push -m "mine" -- src/a.tsAllowed
git stash popAllowed
git add ./src/file.tsAllowed

Every line run against guard-git.mjs on November 6, 2026

How to use it

  1. Paste the block into AGENTS.md or CLAUDE.md. Agents that don't run Claude Code hooks follow it too.
  2. Save guard-git.mjs as .claude/hooks/guard-git.mjs in your repo. It needs Node 18 or newer and nothing else.
  3. Add the hook to .claude/settings.json. A blocked command exits 2, and Claude Code shows the agent what to do instead.
  4. Give each agent its own worktree when you can: git worktree add ../repo-agent-a -b agent-a. The rules and the hook are the backstop for when two agents end up in one checkout anyway.

Where it came from

I've had several AI agents committing to one repository since June 3. By early November that was 11,635 commits on main. Almost every disaster in that time came from the same three commands, which are perfectly safe when one person types them.

The six rules: stage only your own files, by name. Never run a bare git stash. Commit your own work before you hand off. One worktree per agent when you can. The author doesn't approve its own work. And done means it's on main.

That last one came from finding ten tasks marked done whose work never reached main. For nine of them the commit was reachable from no branch or tag at all, one git gc away from gone.

The hook ignores quoted text, so a commit message that mentions git add -A still goes through. It is 89 lines with no dependencies.

Rule five is the one Excellent is built on: the agent that did the work doesn't approve it, and Excellent checks for itself. excellent.so