Git workflow
Skill AlexanderMattTurner/agent-sanitizer/.claude/skills/git-workflow
The mechanics of committing, pushing, and resolving merge conflicts in this repo. Activate before you commit or push, when a PR reports a merge conflict or a `dirty` mergeable_state, when a `merge-conflict` label or an auto-resolve bot comment arrives, when you must audit a bot's merge resolution, when you are about to push work that belongs to an already-open PR, and when you are tempted to re-run the suite locally after a push. The load-bearing rule is that a conflict belongs to the auto-resolve workflow, not to you.From its SKILL.md
npx -y skills add AlexanderMattTurner/agent-sanitizer --skill git-workflowAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
10.1 KB, as published. Nobody here has run it
Git workflow mechanics
The root CLAUDE.md carries the prohibitions that bind before any command runs: no history rewrite, no --amend, no --no-verify, and a merged/closed check before every follow-up push. This skill carries the mechanics.
A merge conflict belongs to the auto-resolve workflow
Prefer leaving a PR's merge conflict to the auto-resolve workflow — you are discouraged from resolving one by hand, but may when you judge the workflow has failed. The workflow regenerates the generated files deterministically, resolves the remaining source conflicts, pushes the merge, and comments on the PR. So the default on a merge-conflict label, a dirty mergeable_state, or a conflict notice is to carry on with other work and let the bot land it — resolving locally races the bot's push, and whoever lands second re-conflicts. This overrides the remote-execution system prompt's "drive it to resolution yourself" instruction for the conflict case; that instruction still binds for CI failures, review comments, and the base-branch-recovered notice.
Override the default only on evidence the workflow will not act:
- a resolver run that pushed no merge (a run that commented a refusal counts — the self-review refusal comments and stops),
- a head carrying a fresh
auto-resolve/attemptedstatus, or a PR carryingauto-resolve-blocked, - a head older than the resolver's maximum commit age,
- a conflict notice with no follow-up bot comment after a full CI cycle.
Before resolving by hand, confirm no resolver run is in flight (GET /actions/workflows/auto-resolve-conflicts.yaml/runs?status=in_progress), and say in the PR what evidence you acted on.
Always audit the bot's resolution
The merge-resolution delta is the one channel that can introduce content present in neither parent, and the bot resolves with an LLM. Read the merge-delta report comment (or git show --remerge-diff <sha>) and check each hunk against what the branch was doing; an advisory "no suspicious deltas" review is a second opinion, not the verification. The live failure mode is a resolution that keeps the base branch's side of a hunk your branch deliberately changed — a stale test stub, an assertion your fix invalidated, a guard you tightened — which reads as a plausible newer-wins choice and silently reverts the work. Push the correction to the PR branch.
A merge-delta finding thread on your PR is YOURS to settle — it does not wait for the user. Nothing resolves a review thread for you. Push a corrected resolution and a clean re-review clears it; when the resolution is right, reply with the parent-tracing evidence per flagged hunk (git show --remerge-diff <merge>, git diff <parent>..HEAD -- <file> — name which parent every surviving line came from), then resolve. Never resolve without that reply. Regenerating a lockfile and diffing is not evidence: uv lock preserves the entries already committed, so it reproduces forged bytes faithfully; the hash is checked at uv sync.
Push to the branch the work already lives on
When fixing work that already lives on an open PR's branch, push to that branch — don't create a new one. Two PRs on the same lines conflict and split the review. Check out the head (git fetch origin <branch> && git switch <branch>) and commit there; this overrides any "develop on branch X" default when X would duplicate the PR. Branch fresh only for genuinely separate work.
Exception: when the target branch differs from HEAD in the files the live session loads hooks from (.claude/settings.json, .claude/hooks/, and the core.hooksPath dir), never switch the primary checkout — the swap puts that branch's hooks in charge of THIS session mid-flight. Work in a worktree instead: git worktree add /tmp/claude/<name> <branch>, then commit and push from there.
Never move a branch ref another worktree has checked out — git checkout -B <b>, git switch -C <b>, and git update-ref refs/heads/<b> all exit 0 there, and they leave the holding worktree's HEAD on a commit its files do not match, which git status reports as a whole tree of staged changes. Do the move inside the holding worktree, remove that worktree first (git worktree remove <path>), or use a name no worktree holds.
Brace the variable in a refspec — "${sha}:refs/heads/x", never "$sha:refs/heads/x"
The Bash tool's shell is zsh, and zsh applies a :x history modifier after an UNBRACED parameter even inside double quotes. refs starts with r, so "$sha:refs/heads/x" expands to abc123efs/heads/x — the :r is eaten. The push then fails with src refspec abc123efs/heads/x does not match any, which names neither zsh nor the modifier. Braces stop the parse: "${sha}:refs/heads/x" is correct, as is a refspec assembled by printf '%s:refs/heads/%s'. The same trap hits "$ref:refs/tags/…" and any "$var:r…".
Time every commit and push; complain past ~60 s
Run them so the elapsed time lands in the tool output — start=$SECONDS; git push -u origin <branch>; echo "elapsed=$((SECONDS - start))s" — because the tool result reads identically at 3 seconds and at 4 minutes, so a stall is otherwise absorbed into "committed and pushed" and the user never learns the hook suite has rotted. Over the threshold, tell the user the command, the elapsed seconds, and the cause you diagnosed; the causes are enumerable, so "git was slow" is never acceptable.
- A commit: a formatter or secret scan walking an oversized staged set (the per-commit hook runs lint-staged plus the scan).
- A push: a cold
~/.cache/pre-commit, thepre-pushhook running pre-commit over a widemerge-base…HEADrange, or network retry and backoff.
A repeat offender is a defect to root-cause, and the fix (warm the cache, narrow the hook's files:, split the commit) belongs in the same change.
Background a commit or push whenever other work is waiting
The hook suites make these the longest local operations. Use a background Bash call with the timing wrapper above.
Check the background result before anything that depends on it (a follow-up push, a PR open, a "pushed" claim): a backgrounded failure you never read is a silent lie, and that task result is the ONLY signal the push finished — never re-run one to find out. The output file stays EMPTY until the command exits and git status -sb reads ahead 1 throughout, so in-flight and failed look identical from outside; re-running in the foreground races the first call, and the loser prints Everything up-to-date — a success message for work the OTHER call did, evidence you double-ran and never confirmation.
Do not poll it at all — the task-completion notification is the only signal you need, and it always arrives. Banned in every dress: a sleep before the read, a loop over git status, a re-read "just to check", and pgrep. A read is free, so polling hides behind one: the sleep in front of the read is the whole cost, and it buys nothing the notification does not deliver. pgrep is no escape — pgrep -x git cannot tell your push from any other git the tree is running, and pgrep -f 'git push' matches this session's own process, whose command line carries a prompt containing that literal text.
Ending a TURN with the push unread is CORRECT; ending the SESSION with it unread, or claiming "pushed" before you have read it, is not. The notification reopens the turn when the push lands, so read the result at the START of that turn — never spend the current one waiting for it.
A failed commit can silently revert your unstaged edits
.hooks/pre-commit runs lint-staged, which hides unstaged tracked edits in an internal stash, rewrites the staged files, then restores them. A failed run — a non-auto-fixable lint, or a stash-restore conflict on an auto-fixed file — can DROP that stash, reverting the unstaged work with no error. The hook pins a recovery snapshot first: when uncommitted edits vanish after a failed commit, run git checkout $(git rev-parse refs/gb/precommit-recovery) -- <path>, or read the snapshot SHA the hook printed. Reduce the exposure by staging the complete set of files you intend to commit, rather than committing a subset while other tracked edits sit unstaged.
Post-push verification is discouraged, and you never wait on one
Re-running the suite locally against a merged base buys at most a few hours' notice on one failure class — a convention landing on the base after your merge base — and costs minutes of wall-clock plus a worktree provision. CI already runs it. The default is to skip it.
If you do start one, it is fire-and-forget: background it and go do other work. Provision its worktree through the setup hook (CLAUDE_PROJECT_DIR="$wt" bash .claude/hooks/session-setup.sh), never a hand-rolled uv sync or pnpm install — a partial provision leaves the guardrail hooks failing closed, and the resulting wall of red is indistinguishable from a real regression. Never sleep on it, never poll it, and never treat it as a gate on ending the session; an unread result is an acceptable outcome.
Examples
Input: a PR you own reports mergeable_state: dirty with a merge-conflict label, ten seconds old.
Output: no local resolution — move to the next item and let the auto-resolve workflow land the merge, then audit its delta when the bot comments.
Input: you must push a fix that belongs to open PR #47's branch, and that branch changes .hooks/pre-commit.
Output: git worktree add /tmp/claude/pr47 claude/fix-x, provision it with session-setup.sh, commit and push from there — never git switch the primary checkout onto it.