Soft reset and recommit
Rebuild a branch's history so the commits explain the change, without changing a byte of the result.
Argument: $ARGUMENTS — a PR number, a base ref, a hint about how to split, or nothing.
Four references carry the parts that must not drift:
../../references/branch-safety.md— the gates, the backup, recovery, and the push.../../references/split-mechanics.md— base resolution, the collapse, staging, splitting, verification.../../references/commit-messages.md— style discovery, intent recovery, and the privacy gate.../../references/rebase-toolkit.md— interactive rebase with no editor, plusreferences/rebase-todo.sh.
Core principle
The tree is the invariant. The history is the deliverable.
A recommit that changes what the branch produces is a bug, no matter how good the commits look. Every decision below is subordinate to proving the final tree is identical to the one it replaced.
Phase 1 — Gather intent, before anything destructive
The collapse destroys the original commit boundaries. Everything worth knowing has to be read first.
Read every original message whole, the trailers, the pull request body
and its review threads, the linked tickets, and — when the repository
does not explain itself — the session that wrote the code. Follow
commit-messages.md Part 2, including its privacy gate: extract
claims and re-state them, never copy a span out of a transcript.
Record what each original commit was for. That mapping is the raw material for every message written later.
Phase 2 — Resolve the base and run the gates
Resolve the base to a SHA once, per split-mechanics.md: fork point
first, plain merge-base as fallback, and refuse unless exactly one
merge base exists. On a stacked branch the base is the parent branch
tip, never trunk.
Then run every refusal in branch-safety.md: dirty tree, operation in
progress, merge commits in the range, branch held by another worktree,
shallow clone. Detect a pushed branch and whether anyone else has
pushed to it — without git fetch.
A refusal is a report and a stop, not a warning to proceed past.
Phase 3 — Discover the conventions
The commit format, per commit-messages.md Part 1: declared first,
mined from history at the fork point otherwise, and reported as
mixed with a question when the repository has no single style.
The project's quality gates, from AGENTS.md, CLAUDE.md, or
CONTRIBUTING.md — the format, lint, typecheck, and test commands as
the project defines them. Never invent one. These become the
per-commit gate in Phase 6, so copy any harness outside the repository
and call it by absolute path.
Phase 4 — Plan the series, and get it approved
Enter plan mode before any destructive step. In Claude Code that is
EnterPlanMode; in other hosts /plan or Shift+Tab. If plan mode is
unavailable, present the same plan as text and wait for an explicit
go-ahead.
The plan states:
- The resolved base SHA and how it was resolved.
- The backup branch name.
- The proposed commit series, in order: for each, its subject, the paths or hunks it takes, and the intent it came from.
- Which commits are split out of a single original, and which originals are merged together.
- The discovered commit format and the gate commands.
- Anything the gates flagged but did not halt on.
Order the series so it builds: a refactor before the behavior that depends on it, a fixture before the test that uses it. Declare any dependency between commits.
Split when a commit does two things. The signal is the message — if the body needs to describe two concerns, it is two commits.
Wait for approval. Exit plan mode before executing.
Phase 5 — Back up, then collapse
Create the backup branch first, and report its name:
git branch "backup/<branch>-$(date -u +%Y%m%dT%H%M%SZ)" <branch>
Then collapse:
git reset --soft <base-sha>
Phase 6 — Rebuild, one commit at a time
Per split-mechanics.md: whole files with git commit -m "..." --only -- <paths> (message flags before the --), a single file split
across commits through the regenerate-filter-apply loop, and the
staged set asserted before every commit.
Write each message from the intent gathered in Phase 1, in the format
discovered in Phase 3, preserving the original author identity and
date. Carry Co-authored-by and ticket trailers forward.
<toolkit> is the absolute path to references/rebase-todo.sh, which ships
with this skill. A shell runs with your project as its working directory, not
this skill's, so substitute the full path before invoking it.
Then gate the whole series in place:
sh <toolkit> verify <base-sha> '<test command>'
A failure stops the rebase and leaves it in progress; the script says so and prints the abort command. Fix the offending commit and re-run rather than pressing on.
Phase 7 — Prove it, then hand back
The gate, not a formality:
git diff --quiet backup/<branch>-<ts> HEAD
A non-zero exit means the recommit changed the result. Restore from the backup and report — do not try to patch the difference.
Then show the user how the history was regrouped:
git range-diff <base-sha> backup/<branch>-<ts> HEAD
Report the new series, the backup branch name, the gate results, and what was deferred.
Phase 8 — Pushing is a separate decision
Never push as part of the recommit. Offer it, with the flags from
branch-safety.md, and let the user decide:
git push --force-with-lease --force-if-includes origin <branch>
Rules
- Prove tree equality before reporting success. Without it there is no claim to make.
- Never rewrite a pushed branch without explicit consent, and never when someone else has pushed to it.
- Never
git add -Aorgit add .— explicit paths only. - Never delete the backup branch. The user does that when they are satisfied.
- Never bare
--force, and nevergit fetchwhile a lease is being relied on. - Never invent a test command, a version, or a ticket ID.
- Language-agnostic: every gate command comes from the project's own conventions files.
Common mistakes
Collapsing before reading the original messages. They are the primary evidence of intent, and after the soft reset they are only reachable through the backup ref.
Recutting a stacked branch against trunk. It absorbs the entire parent pull request into the child. The base is the parent tip.
Trusting a clean git rebase --exec run that had no --keep-base.
Without it the "verification" rebases onto upstream and silently drops
commits that became empty, exiting 0.
Putting -m after the -- in git commit --only. Everything
after -- is a pathspec, so git looks for a file named -m.
Gating on git range-diff being clean. Heavy regrouping shows as
drop-and-add pairs. It explains the recommit; tree equality gates it.
Leaving a stopped rebase in place. A failed --exec leaves a
detached HEAD that poisons every later git command.
Copying a span out of a transcript into a message. Transcripts are unredacted. Re-state the claim in your own words.
Adding a (#N) suffix because the merge commits have one. Merge
style and branch style legitimately differ in the same repository.