---
key: release.prepush-clean-worktree-guard
title: '2026-07-17 — Pre-push guard: don''t push a file whose working-tree copy is uncommitted (H13, #416 session)'
status: active
since: '2026-07-17'
supersedes: none
superseded-by: none
rule: A fail-open pre-push hook blocks a push when any file in the branch's diff vs `origin/main` also has uncommitted working-tree or index changes, since a stale-index commit (e.g. `git reset --soft` + `git add` over an edited-but-unstaged fix) can silently push, CI-test, and get reviewed a different tree than the one on disk. Scope is precise to pushed-diff files; escape hatch `ETV_ALLOW_DIRTY_PUSH=1`.
signals: 'working-tree vs committed-tree mismatch, stale index, pre-push hook, H13 · paths: `.claude/hooks/prepush-clean-worktree-check.sh`, `.husky/pre-push` · issues: #416'
mechanics: wired after the H11 rebase check in `.husky/pre-push`; same hook family as H11/H12 (#303)
---

A review fix (`--no-renames`) was edited into the working file and empirically verified there, but a
`git reset --soft` + `git add` then committed the *stale index*, leaving the fix as an uncommitted
working-tree diff. The push, the CI run, and a cold reviewer each saw a **different tree**: CI/push
had the OLD code; the reviewer read the working file and "confirmed" a fix that never shipped. A PR
went out still carrying the bug the review had cleared. Root cause: local build/test/review all
operate on the working tree, but what ships is the *committed* tree — nothing enforced that they
match.

Decision: a fail-open **pre-push hook** (`.claude/hooks/prepush-clean-worktree-check.sh`, wired into
`.husky/pre-push` after the H11 rebase check) blocks a push when a file that is part of the branch's
diff vs `origin/main` **also** has uncommitted working-tree or index changes. Scope is deliberately
precise — only files in the pushed diff, so unrelated uncommitted scratch (or untracked files) never
false-block. Fail-open on anything undecidable (not a repo, offline, no `origin/main`); deliberate
escape `ETV_ALLOW_DIRTY_PUSH=1`. This is the mechanized half of the working-tree-vs-committed lesson;
the review-process half (point reviewers at `git show <sha>:<file>`, never the bare working file) stays
guidance. Same hook family as H11 (rebase-not-merge) / H12 (issue-qualification), same "#303 make the
rule a hook, not prose to remember" throughline. Verified: dirty PR-file → block; clean tree → allow;
dirty non-PR file → allow; escape hatch → allow.
