CommunityCoding & Developmentgithub.com

roselletabuena/git-safe-history-rewrite

Rewrite git history without losing work or clobbering other people - amend, reset, squash, reorder, drop or edit commits, change a commit message, remove a commit from a branch, or force-push. Use this whenever the user asks to amend, squash, fixup, "undo the last commit", "get rid of that commit", "remove a commit from history", "clean up my commits before the PR", or to force-push; and whenever a task would end in `git push --force`. It decides whether rewriting is even appropriate (a commit others have pulled should be reverted, not erased), takes a recovery point first, avoids the interactive editors that hang in a non-interactive shell, and pushes with a lease instead of a bare force.

What is git-safe-history-rewrite?

git-safe-history-rewrite is a Claude Code agent skill that rewrite git history without losing work or clobbering other people - amend, reset, squash, reorder, drop or edit commits, change a commit message, remove a commit from a branch, or force-push. Use this whenever the user asks to amend, squash, fixup, "undo the last commit", "get rid of that commit", "remove a commit from history", "clean up my commits before the PR", or to force-push; and whenever a task would end in `git push --force`. It decides whether rewriting is even appropriate (a commit others have pulled should be reverted, not erased), takes a recovery point first, avoids the interactive editors that hang in a non-interactive shell, and pushes with a lease instead of a bare force.

Works with~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/roselletabuena/skills/tree/HEAD/plugins/git-workflows/skills/git-safe-history-rewrite

Installed? Explore more Coding & Development skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

Rewriting history safely

Every rewrite - commit --amend, reset, rebase, filter-repo - produces new commits and moves a branch pointer off the old ones. That is harmless while nobody else has the old commits, and destructive the moment someone does: their clone still has the old history, and the next time they push or pull it comes back as duplicates or conflicts. So the first question is never "how", it is "who else has these commits?"

1. Decide: rewrite or revert

git fetch --prune
git status --short --branch                  # ahead/behind, and dirty state
git branch -r --contains <commit>            # which remote branches already hold it
git log --oneline @{u}..HEAD                 # commits that exist only locally
Where the commit livesDo this
Local only, never pushedRewrite freely.
Pushed to a branch only this user works on (their PR branch)Rewrite, then push with a lease (step 5).
On a shared or protected branch: main, master, develop, release/*, or anything with other people's commits on topDo not rewrite. Use git revert.
Already released or deployedgit revert. A rewrite cannot un-deploy it.

When the user says "get rid of that commit from main", they usually want its effect gone, not its SHA. git revert <sha> does that, keeps history honest, and needs only an ordinary push. For a merge commit, git revert -m 1 <merge-sha>. Say why you chose revert, and if the user insists on erasing it from a shared branch, name who it will affect and get an explicit yes first.

A revert conflicts when later commits edited the same lines - which is exactly the case where commits were built on top of the bad one. Resolve it as any conflict (see git-conflict-resolution): remove the bad change, keep the later work, then git revert --continue.

One revert trap: after reverting a merge, merging the same branch again brings nothing back, because git thinks those commits already landed. To re-land the work, revert the revert first.

2. Take a recovery point before touching anything

git rev-parse HEAD                           # note it
git branch backup/<branch>-pre-rewrite       # a real ref, not just the reflog

The reflog also records where the branch was, but it is local to this clone, it expires (90 days for reachable entries, 30 for unreachable by default), and git gc after reflog expire ends it. A named backup branch costs nothing. Leave it in place until the user has confirmed the result.

3. Pick the least destructive command

GoalCommandNotes
Fix the last commit's message or add a forgotten filegit commit --amend-m "..." or --no-edit avoids opening an editor.
Uncommit but keep the changes stagedgit reset --soft HEAD~1
Uncommit and unstage, keep the filesgit reset --mixed HEAD~1 (the default)
Move a branch back, refusing to lose local editsgit reset --keep <commit>Aborts if it would overwrite uncommitted changes. Prefer this to --hard.
Throw away uncommitted work as wellgit reset --hardUncommitted work is not in the reflog and cannot be recovered. Only after confirming nothing there is wanted; git stash -u first if unsure.
Discard changes to one filegit restore <file>
Squash or fold fix-ups into earlier commitsgit commit --fixup=<sha> then autosquashSee below.
Drop or reorder commitsrebase -i with a supplied todoSee below.

A failed pre-commit hook means no commit was made. Running git commit --amend to "retry" then modifies the previous commit, folding the new changes into the wrong one. After a hook failure, fix the problem, re-stage, and make a normal new commit.

Interactive rebase without an interactive editor

git rebase -i opens an editor, which hangs in a non-interactive shell. Do not rely on it. Two ways that work:

# Fold fix-up commits into their targets - accept the todo git generates:
git commit --fixup=<target-sha>
GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash <base>

# Drop or reorder: write the todo yourself.
GIT_SEQUENCE_EDITOR="sed -i '/^pick <short-sha> /d'" git rebase -i <base>

<base> is the commit before the first one you want to change. If a rebase stops on a conflict, resolve it, git add, then GIT_EDITOR=true git rebase --continue. If it goes wrong, git rebase --abort returns to where it started.

4. Verify the rewrite changed what you meant it to

A message edit, reorder or squash should leave the final tree identical:

git diff backup/<branch>-pre-rewrite HEAD --stat      # empty = same content
git range-diff backup/<branch>-pre-rewrite...HEAD     # commit-by-commit comparison

If you meant to drop content, the diff should show exactly that content and nothing else. Anything unexpected means restore from the backup and redo it.

5. Publish - only with the user's go-ahead

Pushing a rewrite is the outward-facing, hard-to-reverse step. Confirm first unless the user already asked for the push.

git push --force-with-lease=<branch>:<expected-remote-sha> --force-if-includes origin <branch>
  • Never bare --force. It overwrites whatever is on the remote, including a teammate's push you have not seen.
  • Name the expected SHA. A plain --force-with-lease compares against your remote-tracking ref, and an IDE's background git fetch quietly updates that ref, so the lease then protects nothing. Use the SHA you saw in step 1 (git rev-parse origin/<branch>).
  • --force-if-includes (git 2.30+) additionally refuses if the remote moved to something your local branch never integrated.
  • Never force-push main/master/develop/release/*. If a hosting rule blocks it, that rule is correct; do not work around it.

After pushing, tell the user that anyone with the old branch checked out must reset to the new one rather than pull, and that an open PR's review comments may lose their anchors.

Guardrails

  • Signed commits lose their signatures when rewritten; re-sign with -S or tell the user.
  • Do not rewrite commits another person authored without asking, even on a feature branch.
  • Stacked branches above the rewritten one are now stale. Use git-stacked-pr-rebase to restack them rather than rebasing each by hand.
  • A committed secret is not fixed by a rewrite. Rotate the credential first; assume it is compromised.
  • Report plainly: what the old tip was, what the new one is, where the backup ref is, and whether anything is unpushed. If revert was the right call, say that is what you did.

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

Related Skills