Local git traps where a green command is wrong. Use when branching after a squash-merge, cutting a PR branch, after a clean merge or rebase, when a staged file vanishes, or before reset --hard.
Scanned 9/27/2026
npx -y skills add laurigates/claude-plugins --skill git-local-hazards --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Git Local Hazards?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/laurigates-git-local-hazards)More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.
---
name: git-local-hazards
description: Local git traps where a green command is wrong. Use when branching after a squash-merge, cutting a PR branch, after a clean merge or rebase, when a staged file vanishes, or before reset --hard.
allowed-tools: Read, Grep, Glob, Bash(git *), Bash(gh pr list *), Bash(grep *), TodoWrite
created: 2026-09-24
modified: 2026-09-26
reviewed: 2026-09-24
---
# Git Local Hazards — Verify the Content, Not the Exit Code
Promoted from the always-loaded `~/.claude/rules/git-hazards.md` rule, whose
stub keeps the gate lines. The body below is that rule's text. Sibling:
`git-plugin:git-merge-hazards` (GitHub PR/merge machinery), which follows the
same promotion pattern.
Six local-git traps, one law: **a green git command is not proof the result is
correct** — exit 0 is a claim about mechanics, not content. Each: the trap, the
5-second check, the fix.
## When to Use This Skill
| Use this skill when... | Use something else when... |
|---|---|
| Starting follow-up work after a branch's PR squash-merged | Deciding whether a branch really landed on GitHub → `git-merge-hazards` |
| Cutting a new PR branch | Another agent may be moving HEAD in this clone → `git-coworker-check` |
| A merge or rebase reported success over sibling work | Resolving an actual conflict → `git-conflicts` |
| A staged file vanished or `git status` flaps | Deleting a whole checkout → `git-repo-delete-check` |
| About to `reset --hard` / `checkout` / `restore` / `clean`, or recovering after one | |
## 1. Commits added after a squash-merge are orphaned
Anything committed to a branch **after** its squash-merge is in neither `main`
nor the squash commit. A fresh branch off `origin/main` silently lacks that
work; the first symptom is an ImportError far downstream.
- **Check** before follow-up work: `git grep <symbol> origin/main -- <path>` —
don't trust "the PR merged". The symbol must be **unique to the change**: a
grep for `tier == "deliver"` reported a change as landed when the hit came
from a pre-existing line that merely contained the same text (2026-08-19).
Prefer `git cherry origin/main <branch>` — `+` means not upstream, and it
survives SHA rewriting (rebase, cherry-pick), which plain ancestry checks do
not. A squash-merge defeats it two ways: a **multi-commit** squash marks
every commit `+`, landed or orphaned (no single commit's patch matches the
combined squash commit), and even a single-commit squash reads `+` when
`main` edited lines inside the diff's context before the merge (the patch-id
cherry compares hashes context lines too). After a squash, take the
boundary from the PR's frozen head instead:
`gh pr list --head <branch> --state all --json state,headRefOid`, then
`git log --oneline <headRefOid>..<branch>` lists exactly the commits the
merge did not include. The head SHA is the boundary, not commit dates: a
commit made before the merge but pushed after it is dated earlier than
`mergedAt`.
- **Fix**: replay only the orphans: `git rebase --onto origin/main <squash-point> <branch>`,
then verify `git log --oneline origin/main..HEAD` shows only the orphaned + new commits.
Confirm nothing was lost by comparing **trees**, not diffs:
`git rev-parse HEAD^{tree} <old-tip>^{tree}` must match.
### The variant with no symptom at all: the PR merges WHILE you are still on the branch
The framing above assumes you come back later and branch afresh. The quieter
case is that you **never leave**: the PR is merged mid-session — by CI, by a
teammate, by the user in another window — and you keep committing to the same
branch. Every `git push` succeeds, `git status` is clean, and nothing says the
branch's PR is closed. Observed 2026-08-19: two commits, and the source that
produced an hour-long delivery render, sat on a branch whose PR had already
merged. It surfaced only because an unrelated `git switch main` failed on local
changes.
- **The tell is on the remote, not locally.** `gh pr list --head <branch>`
returns nothing (and `--state all` shows MERGED) while you still have commits
in `origin/main..HEAD`.
- **Do not reuse the merged branch for the new PR.** A merged PR cannot take new
commits; push the rebased work to a **new** branch name so the fresh PR is not
tangled with the closed one.
- Cheap habit: before the *second* push to any branch in a long session, run
`gh pr list --head "$(git branch --show-current)" --json number,state`.
## 2. Unpushed commits on local `main` ride into new branches
Branching off local `main` inherits whatever it is ahead of `origin/main` by;
the PR then bundles stray commits under an unrelated title (squash hides it —
visible only in the file list).
- **Rule**: cut PR branches from the remote, always:
`git fetch origin && git switch -c <branch> origin/main`.
- **Check** when unsure: `git log --oneline origin/main..main` — empty means clean.
## 3. A clean textual merge can duplicate identical additions
When two branches each add the **same** helper/import/enum arm in non-adjacent
spots, `git merge` sees no overlapping hunk, reports success, and keeps **both
copies** — a duplicate-definition build break (or worse, silent shadowing in
lax languages).
- **Check**: build/test the merged tree before committing the merge; when
siblings solved related problems, `grep -c '<symbol>' <file>` — expect 1.
- **Fix**: hand-resolve to a single combined definition; never trust
"Automatic merge went well" as a verdict on content.
**The rebase variant: zero conflicts, broken result, no textual overlap.** Same
law, and easier to miss because a rebase that replays cleanly *feels* verified.
When `main` gains a file that depends on state your branch **removed**, the two
never touch the same lines, so there is nothing to conflict on — and the merged
tree is still broken.
> Observed 2026-08 (a data-pipeline repo): a branch removed TimescaleDB by
> rewriting the three `0001_initial` migrations. Meanwhile `main` gained
> `measurements/0004`, calling `decompress_chunk()`/`show_chunks()`
> unconditionally. `git rebase origin/main` reported **success with no
> conflicts** — different files — and produced a migration tree that cannot
> apply on the plain PostgreSQL the branch exists to target
> (`function show_chunks(unknown) does not exist`). Caught by reading `main`'s
> new commits, not by any gate.
- **Check after any rebase that spans many upstream commits**: list what `main`
added while you were away (`git log --oneline --name-only <old-base>..origin/main`)
and ask whether any of it depends on something your branch deletes — an
extension, a column, a helper, a config key. Then run the suite *on the target
environment*, not just the one you develop in.
- The durable fix is a **CI matrix over both environments**; a hand-run recorded
in a PR description is what let the original divergence through.
## 4. `git add` aborts atomically on a bad pathspec
`git add fileA nonexistent` stages **nothing** — not "fileA plus a warning".
Classic trip: `git mv old new`, edit `new`, then `git add new old` → the stale
`old` aborts the add, and the commit ships the rename with pre-edit content.
- **Rules**: one pathspec per `git add`, or only confirmed-present paths.
After `git mv` + edit, `git add <newname>` alone.
- **Check**: `git status --short` before committing — the index column must
show the change you intend.
- **Recovery**: the edit is still unstaged in the working tree; add and
commit/amend — don't redo the work.
## 5. A "vanished" staged file in a shared checkout was probably committed by a coworker
Sibling of the push-by-SHA HEAD race in `git-plugin:git-merge-hazards` (the
stacked-chain protocol and its three async races): the **index and HEAD are
process-global**, so a coworker session's commit lands between two of your Bash
calls with no warning. Observed 2026-07 (dotfiles): a file another session had
staged (`A `) disappeared from `git status`, then `ls` said it didn't exist,
then status flapped `A ` → `M ` across consecutive calls. The wrong theory
("pre-commit's stash dance ate it") was nearly acted on; the truth was the
coworker had committed the file to `main` mid-flight — every observation was a
stale read of state the coworker kept moving.
- **Check first, before any recovery**: `git log --oneline -3` — did HEAD
move? — and `git log -1 -- <path>`; a fresh commit touching the path is the
discriminator between "lost" and "landed".
- **Don't blind-restore.** Re-adding your own copy of a "lost" file can
silently **downgrade** the coworker's version (observed: the restored copy
lacked frontmatter the coworker had added before committing). Diff your
candidate against `HEAD:<path>` and keep the committed version unless yours
is genuinely newer.
- **Status flapping between consecutive calls is itself the tell** that a
coworker is active — stop mutating shared state (index, HEAD, branch
switches) until the flapping stops; re-read state fresh in the same command
that acts on it (same instinct as push-by-SHA).
## 6. Work destroyed by `reset --hard` is often still in a pre-commit stash
Unstaged content has no reflog entry, so this looks unrecoverable — but
`pre-commit` stashes unstaged changes around every run, and those stash
**commits** go dangling and survive until `gc`.
- **Prevent**: `git status` before `reset --hard`/`checkout`/`restore`/`clean`;
`git stash -u` anything present.
- **Recover**: `git fsck --lost-found`, then per dangling *commit* (not blob)
`git show <c>:<path>`; rank by `git diff --numstat HEAD <c> -- <path>`
against the diff shape you remember.
## Related
- `git-plugin:git-merge-hazards` — GitHub-side traps: squash-merge detection,
stacked-PR auto-close, push-by-SHA races, merging over red
- `git-plugin:git-coworker-check` — detection before destructive ops, and the
shared-checkout branch-isolation check before `git push -u`
- `git-plugin:git-pr-sync-check` — is the current PR branch still live
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!