We've all been there. You're working on a feature branch, making small incremental commits with messages like "wip", "fix typo", "update imports". Before you know it, you have 15 commits for what should logically be one clean PR. Squashing these commits manually is tedious and error-prone.
Enter git commit --fixup — a powerful flag that, combined with git rebase --autosquash, lets Git automatically arrange and squash your messy commits into a clean history. Here's how to use it.
What is git commit --fixup?
git commit --fixup creates a new commit that is explicitly marked as a fixup for an earlier commit. The key difference from --amend:
git commit --amend: rewrites the most recent commit in place (changes its hash).git commit --fixup: creates a new commit prefixed withfixup!, whichrebase --autosquashwill later merge into its target automatically.
When combined with git rebase -i --autosquash, you can:
Add forgotten files to an earlier commit
Fix typos or bugs that belong to a previous commit
Squash multiple related commits into one — automatically, without manual reordering
The Traditional Problem
Consider this common scenario:
git commit -m "wip: add authentication"
git commit -m "fix: typo in auth"
git commit -m "add tests for auth"
git commit -m "update auth logic"
git commit -m "fix off-by-one error"
git commit -m "wip: add authorization"
git commit -m "refactor auth module"Seven commits for what should be one cohesive "Add authentication" feature. Without --fixup, you'd open git rebase -i and manually mark each commit as squash or fixup — repetitive and easy to get wrong.
The Basic Fixup Workflow
Here's the pattern:
# 1. Make your original commit
git commit -m "feat: add user authentication"
# 2. Later, realise you forgot something — add it as a fixup
git add password-reset.ts
git commit --fixup=HEAD
# 3. When ready to clean up, run autosquash
git rebase -i --autosquash HEAD~2The --fixup=HEAD flag creates a commit named fixup! feat: add user authentication. When you run rebase --autosquash, Git automatically moves that commit directly after its target and marks it fixup in the interactive editor — you just save and close.
Step-by-Step: Fixup with autosquash
Say your log looks like this:
abc1234 feat: implement login
def5678 fixup! feat: implement login
ghi9012 wip: update docs
jkl3456 fixup! feat: implement loginRun:
git rebase -i --autosquash HEAD~4Git automatically opens the editor pre-arranged as:
pick abc1234 feat: implement login
fixup def5678 fixup! feat: implement login
fixup jkl3456 fixup! feat: implement login
pick ghi9012 wip: update docsSave and close — Git squashes all three login commits into one and leaves the docs commit untouched. No manual reordering needed.
Real-World Example: Bug Fix with Multiple Passes
# Initial fix
git commit -m "fix: handle null pointer in user service"
# Test reveals another case
vim user.service.ts
git add user.service.ts
git commit --fixup=HEAD
# Add a regression test
git add user.test.ts
git commit --fixup=HEAD
# Log now shows:
# abc1234 fix: handle null pointer in user service
# def5678 fixup! fix: handle null pointer in user service
# ghi9012 fixup! fix: handle null pointer in user service
# Clean up
git rebase -i --autosquash HEAD~3
# All three combined into one clean commit automaticallyReal-World Example: Multi-Step Feature
You want this final history:
feat: add authentication systemrefactor: clean up auth flow
But your current log is:
f47ac8 refactor: clean up auth flow
ba3d92 wip: add logout
8c7e1a wip: add token refresh
4a5b6c fix: handle edge case
9c8d7e wip: add login form
2a3b4c wip: setup auth contextRun git rebase -i HEAD~6 and arrange:
pick 2a3b4c wip: setup auth context
squash 9c8d7e wip: add login form
fixup 4a5b6c fix: handle edge case
fixup 8c7e1a wip: add token refresh
squash ba3d92 wip: add logout
pick f47ac8 refactor: clean up auth flowGit squashes the first five into one, opens the editor so you can write a clean message like feat: add authentication system, then keeps the refactor commit separate.
Best Practices
Set autosquash globally
Save yourself from typing --autosquash every time:
git config --global rebase.autoSquash trueNow git rebase -i HEAD~N automatically honours fixup! commits.
Write good original messages
Since fixup commits inherit the target's message, a descriptive original message means you won't need to edit anything when squashing:
# Bad — you'll need to rewrite this during squash
git commit -m "fix"
# Good — already describes the final state
git commit -m "fix: handle null pointer in auth service when user not found"Test after rebasing
git rebase -i --autosquash HEAD~5
npm test
# If tests fail, recover from reflog:
git reflog
git reset --hard HEAD@{1}Back up before large rebases
git branch backup-before-cleanup
git rebase -i HEAD~10
# If something goes wrong:
git reset --hard backup-before-cleanupCommon Pitfalls
Don't rebase public history. If commits are already on a shared branch, use merge instead. Check first:
git log origin/main..HEAD. If the output is empty, don't rebase.Force-push safely. After rebasing a feature branch, use
git push --force-with-leaseinstead of--forceto avoid overwriting a teammate's push.Don't squash too aggressively. Three focused commits reviewable on their own are better than one giant commit that's impossible to review.
Resolve conflicts carefully. If a conflict arises mid-rebase, edit the file, stage it with
git add, thengit rebase --continue. If it gets messy,git rebase --abortreturns you to where you started.
Troubleshooting
Git doesn't recognise --fixup: ensure you're on Git 2.24 or later (git --version).
Squashed commit has the wrong message:
git commit --amend # if it's the most recent commit
# or
git rebase -i HEAD~2 # to edit an earlier commit interactivelyUnexpected conflicts during rebase: abort, review what changed between the commits you're squashing, and try again with a tighter range.
Summary
git commit --fixup combined with rebase --autosquash is the fastest way to keep your commit history clean without the manual overhead of interactive rebase every time. The workflow is simple: commit normally, mark corrections with --fixup as you go, then run one rebase --autosquash command before opening your PR. Git does the reordering and squashing for you.

