| name | git-squash |
| description | MANDATORY: Use instead of `git rebase -i` for squashing - unified commit messages |
Git Squash Skill
Purpose: Safely squash multiple commits into one with automatic backup, verification, and cleanup.
Parallel Initial Investigation
OPTIMIZATION: Run initial git commands in parallel to reduce round-trips.
Before starting any squash workflow, gather information concurrently:
git rev-parse HEAD &
git status --porcelain &
git log --oneline <base>..HEAD &
git diff --stat <base>..HEAD &
wait
This reduces the initial investigation from 4+ sequential commands to a single parallel batch.
Safety Pattern: Backup-Verify-Cleanup
ALWAYS follow this pattern:
- Create timestamped backup branch
- Execute the squash
- Verify immediately - no changes lost or added
- Cleanup backup only after verification passes
Read PROJECT.md Squash Policy
Check PROJECT.md for configured squash preferences before proceeding.
SQUASH_POLICY=$(grep -A10 "### Squash Policy" .claude/cat/PROJECT.md 2>/dev/null | grep "Strategy:" | sed 's/.*Strategy:\s*//' | head -1)
if [[ "$SQUASH_POLICY" == *"keep all"* || "$SQUASH_POLICY" == *"Keep all"* || "$SQUASH_POLICY" == *"keep-all"* ]]; then
echo "โ ๏ธ PROJECT.md configured for 'Keep all commits'"
echo "Squashing will override this preference."
echo ""
echo "Options:"
echo " 1. Proceed with squash (override PROJECT.md preference)"
echo " 2. Cancel to preserve commits as configured"
echo ""
echo "To proceed, continue with this skill."
echo "To honor PROJECT.md preference, abort the squash operation."
fi
if [[ "$SQUASH_POLICY" == *"single"* || "$SQUASH_POLICY" == *"Single"* ]]; then
echo "๐ PROJECT.md configured for 'Single commit' squashing"
echo "All commits will be squashed into one (not by type)."
fi
if [[ "$SQUASH_POLICY" == *"by-type"* || "$SQUASH_POLICY" == *"by type"* ]]; then
echo "๐ PROJECT.md configured for 'Squash by type'"
echo "Commits will be grouped by type prefix."
fi
Workflow Selection
CRITICAL: Choose workflow based on commit position.
LAST_COMMIT="<last-commit-to-squash>"
BRANCH_TIP=$(git rev-parse HEAD)
if [ "$(git rev-parse $LAST_COMMIT)" = "$BRANCH_TIP" ]; then
echo "Commits at tip โ Use Quick Workflow (soft reset)"
else
echo "Commits in middle of history โ Use Interactive Rebase Workflow"
fi
Planning Commit Pattern Detection
Detect common "feature + planning STATE.md update" pattern.
Before squashing, check if the commit sequence follows this pattern:
- Implementation commit(s):
feature:, bugfix:, refactor:, etc.
- Final commit(s):
planning: or config: with only .claude/cat/issues/ changes
Detection logic:
LAST_COMMIT=$(git log -1 --format="%s" HEAD)
LAST_FILES=$(git diff-tree --no-commit-id --name-only -r HEAD)
if [[ "$LAST_COMMIT" =~ ^planning: ]] && \
[[ "$LAST_FILES" =~ \.claude/cat/issues/ ]] && \
! echo "$LAST_FILES" | grep -qv "\.claude/cat/"; then
echo "PATTERN DETECTED: Final commit is planning-only STATE.md update"
fi
When pattern detected:
- Extract final STATE.md content before squash
- After squash, ensure STATE.md reflects final state (not intermediate)
- Include planning changes in implementation commit per M076
Quick Workflow (Commits at Branch Tip Only)
Use ONLY when squashing the most recent commits on a branch.
git log --oneline -1
BACKUP="backup-before-squash-$(date +%Y%m%d-%H%M%S)"
git branch "$BACKUP"
git status --porcelain
echo "Files on base but not in branch (will be DELETED if you proceed):"
git diff --name-status <base-commit>..HEAD | grep "^D" | cut -f2
git reset --soft <base-commit>
git diff --stat "$BACKUP"
git diff --name-status HEAD | grep "^D"
git commit -m "Unified message describing what code does"
git diff "$BACKUP"
git rev-list --count <base-commit>..HEAD
git branch -D "$BACKUP"
Interactive Rebase Workflow (Commits in Middle of History)
Use when commits to squash have other commits after them.
BACKUP="backup-before-squash-$(date +%Y%m%d-%H%M%S)"
git branch "$BACKUP"
FIRST_COMMIT="<first-commit-to-squash>"
COMMITS_TO_SQUASH="<second-commit> <third-commit> ..."
cat > /tmp/squash-editor.sh << EOF
#!/bin/bash
$(for c in $COMMITS_TO_SQUASH; do echo "sed -i 's/^pick $c/squash $c/' \"\$1\""; done)
EOF
chmod +x /tmp/squash-editor.sh
cat > /tmp/msg-editor.sh << 'EOF'
cat > "$1" << 'MSG'
<your unified commit message here>
MSG
EOF
chmod +x /tmp/msg-editor.sh
BASE_COMMIT="<parent-of-first-commit>"
GIT_SEQUENCE_EDITOR=/tmp/squash-editor.sh GIT_EDITOR=/tmp/msg-editor.sh git rebase -i $BASE_COMMIT
git diff "$BACKUP"
git branch -D "$BACKUP"
rm /tmp/squash-editor.sh /tmp/msg-editor.sh
Critical Rules
Check for Unintended Deletions (M238)
CRITICAL: Worktrees may be out of sync with base branch updates.
When using git reset --soft <base>, the index reflects your working tree state. If the base
branch has files your branch never received (e.g., new files added to base after your branch
diverged), the soft reset will stage those files as DELETIONS.
Before committing after soft reset:
git diff --name-status HEAD | grep "^D"
git checkout <base> -- <path-to-unexpected-deleted-file>
git commit --amend --no-edit
Why this happens:
- Branch created from older base commit
- New files added to base branch later
- Worktree never received these files (no merge/rebase from base)
- Soft reset stages "delete files that exist on base but not in working tree"
Preserve Commit Type Boundaries When Squashing
CRITICAL: Follow commit grouping rules from commit-types.md.
Key rules when squashing:
- Task STATE.md โ same commit as implementation (M076)
- Different commit types (
feature: vs docs:) โ keep separate
- Related same-type commits โ can combine
Before squashing, analyze commit types:
git log --oneline <base>..HEAD | while read hash msg; do
type=$(echo "$msg" | cut -d: -f1)
echo "$type: $hash ${msg#*: }"
done | sort -t: -k1
git log --format="%s" <base>..HEAD | cut -d: -f1 | sort | uniq -c
Automatic STATE.md Preservation
CRITICAL: Preserve final STATE.md state when squashing planning commits.
When squashing commits that include STATE.md updates:
-
Before squash: Record the final STATE.md content
TASK_STATE=".claude/cat/issues/v*/v*.*/*/STATE.md"
git show HEAD:$TASK_STATE > /tmp/final-state.md 2>/dev/null || true
-
After squash: Verify STATE.md wasn't reverted to intermediate state
if [[ -f /tmp/final-state.md ]]; then
if ! diff -q "$TASK_STATE" /tmp/final-state.md >/dev/null 2>&1; then
echo "โ ๏ธ STATE.md reverted to intermediate state - restoring final state"
cp /tmp/final-state.md "$TASK_STATE"
git add "$TASK_STATE"
git commit --amend --no-edit
fi
fi
Why this matters:
- Squashing can revert STATE.md to earlier commit's version
- Final state (status: completed, progress: 100%) must be preserved
- Per M076: STATE.md belongs in same commit as implementation
Use Correct Workflow for Commit Position
git checkout <mid-history-commit>
git reset --soft <base>
git branch -f main HEAD
GIT_SEQUENCE_EDITOR=... git rebase -i <base>
Position HEAD First (Quick Workflow Only)
git reset --soft <base>
git checkout <last-commit>
git reset --soft <base>
Write Meaningful Commit Messages with Task ID
feature(auth): add login
feature(auth): add validation
bugfix(auth): fix typo
feature: add login form with validation
- Email/password form with client-side validation
- Server-side validation with descriptive messages
Task ID: v1.1-implement-user-auth
MANDATORY: Include Task ID: v{major}.{minor}-{task-name} as the last line.
See git-commit skill for detailed message guidance.
Verify Immediately After Commit
git diff "$BACKUP"
git rev-list --count <base>..HEAD
Squash vs Fixup
| Command | Message Behavior | When to Use |
|---|
squash | Combines all messages | Different features being combined |
fixup | Discards secondary messages | Trivial fixes (typos, forgotten files) |
When in doubt, use squash - you can edit the combined message.
Non-Adjacent Commits
For commits separated by others, use interactive rebase:
git rebase -i <base-commit>
Conflict Pre-Computation for Non-Adjacent Commits
Before attempting to squash non-adjacent commits, analyze potential conflicts:
echo "Conflict risk analysis:"
git diff --name-only <base>..HEAD | while read file; do
commits=$(git log --oneline <base>..HEAD -- "$file" | wc -l)
if [[ $commits -gt 1 ]]; then
echo " โ ๏ธ $file: modified by $commits commits"
fi
done
for file in $(git diff --name-only <base>..HEAD); do
git log -p <base>..HEAD -- "$file" | grep -E "^@@" | sort | uniq -d && \
echo " ๐ด $file: same lines modified by multiple commits - HIGH RISK"
done
Risk Levels:
| Risk | Pattern | Recommendation |
|---|
| LOW | Different files per commit | Safe to squash |
| MEDIUM | Same file, different lines | Usually safe |
| HIGH | Same file, same lines | Manual resolution likely needed |
Error Recovery
git reset --hard $BACKUP
git reflog
git reset --hard HEAD@{N}
Success Criteria