Squash Commits
Combine all commits on the current branch into a single commit with a meaningful, synthesized commit message. This cleans up branch history before merging so reviewers see one clear commit instead of dozens of WIP saves.
How it works
The helper script at {{SKILL_DIR}}/scripts/squash.sh handles git operations (pre-flight
checks, base branch detection, backup, soft-reset). You handle the commit
message synthesis and user interaction.
Workflow
Step 1: Gather information
Run the helper script in dry-run mode to collect commit data without modifying anything:
{{SKILL_DIR}}/scripts/squash.sh --dry-run
The output contains:
BASE_BRANCH— the detected base branchCOMMIT_COUNT— number of commits to squashHAS_MERGE_COMMITS— whether merge commits are presentHAS_UPSTREAM— whether the branch has been pushedCOMMITSsection — each commit (short hash + message)COMMIT MESSAGESsection — full message body of each commitTRAILERSsection — deduplicatedCo-authored-byandSigned-off-bytrailers
If the script exits with an error, relay the error message to the user and stop. Common errors: dirty working tree, detached HEAD, only one commit ahead.
Step 2: Synthesize the commit message
Read all the commit messages from the dry-run output. Your job is to understand the overall intent of the changes and write a single, coherent commit message — not a mechanical list or concatenation of the originals.
Good synthesis means:
- Capture what the combined changes accomplish as a whole
- Remove noise (WIP, fixup, typo fixes, "oops") that doesn't add meaning
- Use the same voice and style as the project's existing commits
- Keep it concise — a short summary line, optionally followed by a body
- Do NOT add your own name, Co-Authored-By trailer, or any other attribution identifying you as an AI tool to the commit message. The commit message should contain only the synthesized summary and human-authored trailers.
If the TRAILERS section contains any trailers, append them at the end of the commit message (after a blank line). These carry attribution and compliance information that should not be dropped.
Step 3: Show the preview
Present the user with a clear preview before any changes are made. Because squashing rewrites history, showing exactly what will happen reduces mistakes and builds confidence. Display:
- Base branch: the detected base branch name and merge-base commit
- Commits to squash: count and list (short hash + original message for each)
- Proposed message: the full synthesized commit message you wrote
- Force-push needed: yes/no based on whether the branch has been pushed
- Merge commits warning: if merge commits are present, warn that squashing will flatten them
Then ask the user to confirm or decline. If they decline, stop — no changes are made.
Step 4: Handle pushed branches
If HAS_UPSTREAM=true, the branch has been pushed to a remote. After
squashing, the user will need to force-push (git push --force-with-lease)
because the branch history has been rewritten. Mention this in the preview so
the user is aware before confirming the squash. The actual force-push
confirmation happens in Step 6 after the squash completes.
Step 5: Execute the squash
Run the helper script in execute mode (without --dry-run):
{{SKILL_DIR}}/scripts/squash.sh
This creates a backup ref and runs git reset --soft to the merge-base. All
changes remain staged.
Then create the commit with the synthesized message:
git commit -m "<synthesized message with trailers>"
Step 6: Confirm success and handle force-push
After the commit, show the user:
- The new single commit (run
git log --oneline -1) - The backup ref location (from the script output)
- The recovery command:
git reset --hard <backup-ref>
If HAS_UPSTREAM=false, the squash is complete — no further action needed.
If HAS_UPSTREAM=true, the branch was pushed before squashing and the remote
history now diverges. Offer to force-push:
-
Warn that collaborators who have pulled this branch will see diverged history after the force-push.
-
Ask the user to confirm whether they want to force-push now.
-
If the user confirms, run:
git push --force-with-lease- If the push succeeds, show the output and confirm the remote branch is updated.
- If the push fails, show the full error output. Reassure the user that
the local squash commit is intact and the backup ref is still available
for recovery. Provide the manual command for retry:
git push --force-with-lease
-
If the user declines, skip pushing and show the manual command as a reference:
git push --force-with-lease
Specifying a different base branch
If the script cannot auto-detect the base branch (none of main, master,
develop exist), it will exit with an error asking you to specify one. Ask
the user which branch to use, then re-run with:
{{SKILL_DIR}}/scripts/squash.sh --dry-run --base-branch <name>
And for execution:
{{SKILL_DIR}}/scripts/squash.sh --base-branch <name>
Always confirm the detected base branch in the preview so the user can correct it before proceeding.
Example: branch off develop
If a user is on feature/login branched off develop:
{{SKILL_DIR}}/scripts/squash.sh --dry-run --base-branch develop
The script finds the merge-base between develop and HEAD, lists only the
commits on feature/login, and the rest of the workflow proceeds identically.
Edge cases
- Dirty working tree: The script refuses to run. Tell the user to commit or stash their changes first — a reset with uncommitted changes can lose work.
- Detached HEAD: The script refuses. Tell the user that squashing requires being on a branch.
- Single commit ahead: The script reports nothing to squash. Inform the user and stop.
- Merge commits in history: The script detects and reports them. Warn the user that squashing will flatten merge commits into a single linear commit. Ask for confirmation before proceeding.
- Already pushed: Covered in Steps 4 and 6. Step 4 mentions the force-push
requirement in the preview. Step 6 asks the user to confirm and executes
git push --force-with-leaseon confirmation. If the push fails, the error is shown with reassurance that the local squash is intact. - Diverged base (base branch has new commits since the branch was created): The soft-reset approach handles this correctly by design. It only affects the branch's own commits — the merge-base stays the same regardless of new commits on the base branch. No rebase is performed.
Example
A developer has a branch feature/add-search with 4 commits off main.
The branch has been pushed to the remote. Some commits were made with an AI
coding tool that added its own Co-Authored-By trailer:
abc1234 WIP: search endpoint skeleton
def5678 Add query parsing and validation
ghi9012 Fix typo in search query parser
jkl3456 Add pagination to search results
Co-authored-by: Alice <alice@example.com>
Co-authored-by: Claude <noreply@anthropic.com>
The helper script automatically filters out known LLM/bot trailers, so only
the human trailer (Alice) appears in the TRAILERS section of the dry-run
output.
Running the skill:
-
{{SKILL_DIR}}/scripts/squash.sh --dry-runshows 4 commits, base branchmain, no merge commits, has upstream. -
You synthesize: "Add search endpoint with query parsing, validation, and paginated results"
-
Preview shown to user:
Base branch: main (merge-base: 789abcd) Commits to squash: 4 abc1234 WIP: search endpoint skeleton def5678 Add query parsing and validation ghi9012 Fix typo in search query parser jkl3456 Add pagination to search results Proposed message: Add search endpoint with query parsing, validation, and paginated results Co-authored-by: Alice <alice@example.com> Force-push needed: Yes (branch has been pushed) -
User confirms the squash.
-
{{SKILL_DIR}}/scripts/squash.shcreates backup atrefs/backup/squash-commits/feature/add-searchand soft-resets to merge-base. -
git commit -m "Add search endpoint with query parsing, validation, and paginated results\n\nCo-authored-by: Alice <alice@example.com>"creates the single commit. -
Result shown: 1 commit ahead of
main, backup ref available for recovery. -
Since the branch was pushed, ask the user to confirm force-push. User confirms,
git push --force-with-leaseruns, remote branch is updated.