--- name: concurrent-safe-git allowed-tools: Read, Grep, Glob, Bash, PowerShell description: Commit safely in a worktree you do NOT have to yourself — several agents, a teammate, and a maintenance loop all editing and committing at once. The pathspec commit form, the four commands that silently destroy someone else's work, and the stat-dirty refresh that unblocks a merge git only THINKS is unsafe. Written from real incidents where a 10-line fix committed 270 lines and a reset --hard put a fixed security bug back into production. argument-hint: [commit | unblock-merge | diverged] --- # Concurrent-safe git **You do not have this working tree to yourself.** The moment more than one agent — or an agent plus a human, or an agent plus a cron/maintenance loop — edits the same checkout, ordinary git commands become destructive. Not "risky." Destructive. HEAD advances under you, files appear and vanish between the moment you survey and the moment you stage, and a bare `git commit` ships whatever *somebody else* happened to have staged. If a CD loop deploys from your commits, this stops being a local problem. A careless git operation in a shared tree **ships**. --- ## Two incidents, because the abstract version doesn't land **A `reset --hard` re-deployed a fixed security bug.** One session ran `git reset --hard` to clean up its own mess. That wiped hours of another session's uncommitted work on *tracked* files — untracked files survived, which is the signature that tells you this is what happened. The reverted source then flowed through the deploy loop, putting an already-fixed cross-tenant data leak **back into production**. Nobody typed "undo the security fix." **A 10-line fix committed 270 lines.** A bare `git commit` in a file another session had in-flight hunks in. The commit message described ten lines. The diff was two hundred and seventy, including a half-finished change to an API contract. Both were routine commands. Neither operator did anything they'd call careless. --- ## The rules ### 1. Commit with an explicit pathspec, atomically, in ONE command ```bash git add && git commit -m "msg" -- ``` The trailing `-- ` is the part people drop, and it's the part that matters: it makes the commit ignore foreign staged content in *both* directions. Same paths in both halves. Never a bare `git commit`. Never `git add -A` or `git add .`. ### 2. Never a bare `reset`, `checkout .`, `stash`, or `reset --hard` These operate on the **whole tree** and clobber whoever else is working in it. If you must unstage, scope it: `git reset -- `. If you think you need `reset --hard`: **stop.** That is the exact command from the first incident. Find another way, or ask a human. ### 3. Re-check `git status --porcelain` immediately before each commit group Not at the start of your task — *immediately* before. Skip files another session already landed. Leave their fresh in-flight modifications alone. ### 4. Guard against the same-file sweep Before committing a file someone else may be editing: ```bash git diff --stat -- ``` Confirm the line count matches **your** edit. If it's larger, foreign hunks are riding along. Read the full diff. If they changed one end of a contract — a backend gate and its frontend field, a schema and its consumer — verify *both* ends before you let a deploy loop have it. ### 5. Verify after committing ```bash git show --stat ``` Broad pathspecs sweep more than you intended. A directory pathspec like `tests/` will happily collect three other people's new test files. Confirm only your files landed — after the fact, not on faith. ### 6. Private index ⇒ `git read-tree HEAD` immediately before the commit When several agents share one tree, each usually commits through its own index (`GIT_INDEX_FILE= git add … && git commit …`) so nobody sweeps anyone else's staged files. That index is a **snapshot of the HEAD it was read from**. If a peer commits after you read it, your next commit records the old blobs for every file the peer touched — it silently **reverts their whole commit**. That happened: a commit from a stale private index deleted all 22 files of a peer's commit, and nothing in the diff looked unusual to the agent that made it. The rule, every time, with nothing in between: ```bash export GIT_INDEX_FILE=/tmp/my-agent.index git read-tree HEAD # refresh from the CURRENT HEAD, right now git add -- git commit -m "msg" git show --stat HEAD # only your paths? no deletions you didn't make? ``` awgit's `vcs-stale-revert-guard` pre-commit hook refuses a commit whose staged content equals the state *before* a recent commit (override for a deliberate revert: `AITHER_ALLOW_REVERT=1`). The awsh plugin installs it automatically (`awgit hooks ensure`) before your first commit in a repo — but the hook is the backstop, not the method. Read the tree, then commit. ### 7. Commit early, in small groups Uncommitted work in a shared tree is **not durable**. Someone else's routine hygiene can eat it. Don't let a large edit sit unstaged while you keep going. --- ## "Your local changes would be overwritten by merge" is usually a lie With many writers, index entries go **stat-dirty**: mtime changed, content identical. Git then refuses to touch files to protect changes that *do not exist* — and the obvious escape (`git stash`) is exactly what rule 2 forbids. In a shared tree the stash is repo-global: it is shared by every worktree of the repository, and a stash/pop takes other sessions' work off disk with yours. Do this instead: ```bash git diff --stat -- # which are REALLY modified? git update-index --refresh # re-stat; clears false-dirty entries ``` Measured on a real tree: a merge blocked by **7** files. `git diff --stat` showed exactly **one** had real changes; the other six were stat-only. After `--refresh` all seven read clean and kept their content — no stash, no reset, nothing lost. If a file *is* genuinely modified and still blocks, copy it to a scratch directory first — a filesystem backup git cannot lose — and only then restore that one path: ```bash mkdir -p ../scratch && cp -p ../scratch/ git restore -- # that path only, never the tree ``` **Do the merge itself in your own worktree on your own branch, never in the shared tree.** A merge, rebase or pull rewrites files every other session is reading; the pack's git guard hook blocks them in a shared checkout for that reason. ```bash awgit wt new my-merge --at --branch my/merge-branch # retire with `awgit wt done my-merge` git -C ../my-merge merge origin/ ``` --- ## Don't fight a divergence that resolves itself When every session shares ONE worktree and ONE HEAD, another session's `git push` carries **your** commits too, and their update clears your `behind` count. If a push is rejected: re-`fetch`, re-check, wait. Observed on a real tree: a state that looked like "ahead 6 / behind 6" became "ahead 5 / behind 0" on its own within minutes, with no intervention. Your commits are not at risk while they sit on the shared HEAD. **The risk comes from forcing the issue.** Push as its own command, never chained — a force-push guard can false-positive on a compound `a && git push`. --- ## Never - `git reset --hard` (any form) - bare `git reset`, `git checkout .`, `git stash` (any mutating form) - `git merge` / `rebase` / `pull` in the shared tree (do it in your own worktree) - `git add -A` / `git add .` / bare `git commit` - chained `git push` - a commit from a private `GIT_INDEX_FILE` you did not `git read-tree HEAD` into immediately before ## The one-line version *Name your paths, check twice, and never run a command whose blast radius is "everything."*