Git Commit Skill
Overview
Group working-tree changes into Conventional Commits 1.0.0 units (one commit per logical unit), or rewrite recent non-conformant commit subjects in place.
Core principle: Match user intent to the right workflow — new commits vs. history rewrite — and never bypass safety with destructive flags unless the user explicitly opts in (e.g. the /git-commit-rewrite force keyword).
Announce at start: "I'm using the git-commit skill to <commit / push / rewrite> these changes."
Commands
| Command | Skill | Action | Operates on |
|---|---|---|---|
/git-commit |
git-commit |
Group working-tree changes into Conventional Commits | Uncommitted (staged + unstaged) changes |
/git-commit-push |
git-commit-push |
Same as above, then git push (no --force) |
Uncommitted changes + remote |
/git-commit-rewrite |
git-commit-rewrite |
Rewrite recent non-Conformant commit subjects | Existing local history |
Choosing the Right Command
Run this check before doing anything:
git status --short # Are there working-tree changes?
git log --oneline -10 # Are recent messages already Conventional?
| Situation | Command |
|---|---|
| Working tree has changes, history is fine | /git-commit |
| Working tree has changes + want to push | /git-commit-push |
| Working tree clean, recent history is messy ("Added X", "WIP", "Fixed bug") | /git-commit-rewrite |
| Both: working tree dirty AND history messy | First commit (/git-commit), then /git-commit-rewrite separately |
Auto-routing: If the user runs bare /git-commit but the working tree is clean, look at recent history. If recent commits are non-conformant, surface this and ask: "No working changes to commit. Recent history has N non-Conformant subjects — do you want to rewrite them with /git-commit-rewrite?"
Conventional Commits Format
<type>[optional scope][!]: <description>
[optional body]
[optional footer(s)]
Allowed types
| Type | Meaning |
|---|---|
feat |
New feature (MINOR semver bump) |
fix |
Bug fix (PATCH semver bump) |
docs |
Documentation only |
style |
Formatting / whitespace; no logic change |
refactor |
Code change that neither fixes a bug nor adds a feature |
perf |
Performance improvement |
test |
Adding or fixing tests |
build |
Build system or external dependencies |
ci |
CI configuration files and scripts |
chore |
Other changes that don't modify src or tests |
revert |
Reverts a previous commit |
Breaking changes — append ! after type/scope (e.g. feat(api)!: drop v1 endpoints) and/or include a BREAKING CHANGE: ... footer.
Subject rules
- Imperative mood: "add", not "added" / "adds"
- Lowercase first word of description
- No trailing period
- Aim for ≤72 characters in the subject line
- Scope is optional but recommended when it scopes to a clear module/area
Examples
feat(parser): add ability to parse arrays
fix(ui): correct button alignment
docs: update README with usage instructions
refactor(auth): extract token validation
chore: update dependencies
feat!: send email on registration
BREAKING CHANGE: email service is now required at boot
Workflow: /git-commit (default)
Step 1: Inspect the working tree
git status --short
git diff
git diff --cached
If there are no changes → tell the user, run the auto-routing check (above), and stop.
Step 2: Group changes into logical units
Read each modified file's diff and group by intent. Each group maps to one Conventional Commit. Ignore the existing staging state — staged + unstaged changes are merged and re-grouped from scratch.
Grouping rules:
- Separate
featfromfix— never combine - Separate
docsfrom code changes (unless the doc is a docstring inside the same change) - Tests for a new feature/fix may go with that commit, or stand alone if unrelated
- Separate
build/ci/ dependency updates from feature work - Separate
refactorfromfeat - Fixes in unrelated modules → separate commits per module
- Trivial whitespace inside a feature file can fold into the feature commit
Don't over-split (a one-line trivial cleanup with the feature is fine) and don't over-combine (a feat and fix must be separate commits).
Secrets check — flag any of these and exclude by default unless the user explicitly confirms: .env*, credentials.*, *_rsa, *.pem, *.key, *.p12, files containing the word secret in the path. Do not flag the exact basename .env.example solely because it matches .env*; it is a public template. Every other secret-path rule still applies, including sensitive parent directories or other matching filename patterns. Warn the user about files that remain suspected secrets; they should not be committed.
Step 3: Show the commit plan
Before creating any commit, print a plan and let the user object:
Plan: 3 commits
1. feat(auth): add OAuth login flow
- src/auth/oauth.ts
- src/auth/index.ts
2. fix(api): handle null response in user fetch
- src/api/users.ts
3. docs: update README with auth setup
- README.md
Step 4: Create commits one at a time
For each unit, in plan order:
git add path/one path/two # explicit paths only
git commit -m "$(cat <<'EOF'
type(scope): description
optional body explaining the why
EOF
)"
Rules:
- Never
git add .orgit add -A— always explicit paths - Never
--no-verify,--no-gpg-sign,--amend(unless the user explicitly asks) - Never add emoji or
Co-Authored-By: Claudeunless requested - HEREDOC for multi-line messages so quoting stays correct
- After each commit, run
git status --shortto confirm only the intended files moved
If a pre-commit hook fails: do not retry with --no-verify. Investigate, fix the underlying issue (or report it), and create a fresh commit.
Step 5: Final summary
git log --oneline -<N>
(where N is the number of commits made) and show it to the user.
Worked Example
User: 변경사항 의미 단위로 커밋해줘
git status --short →
M src/auth/oauth.ts
M src/auth/index.ts
M src/api/users.ts
M README.md
?? src/api/.env
→ Detect .env (secret) — exclude, warn
→ Group:
1. feat(auth): add OAuth login flow (oauth.ts, auth/index.ts)
2. fix(api): handle null response in user fetch (users.ts)
3. docs: update README with auth setup (README.md)
→ Show plan, wait for confirmation
→ For each: git add <paths> && git commit -m "..."
→ git log --oneline -3
Workflow: /git-commit-push
Run the default workflow above. After every commit succeeds:
git push
If the current branch has no upstream configured:
git push -u origin "$(git branch --show-current)"
Never use --force or --force-with-lease. If the push is rejected (non-fast-forward), stop and report — do not auto-resolve. If the push fails for any reason, surface the error and let the user decide.
Worked Example
User: /git-commit-push
(Same workflow as default, then:)
→ git push
error: failed to push some refs (non-fast-forward)
→ STOP. Report:
"Push rejected: branch is behind origin. Run `git pull --rebase`
first, then re-push manually. I will not auto-resolve."
Workflow: /git-commit-rewrite
Rewrites non-Conformant commit messages in recent history. This is destructive — it changes commit SHAs. The default policy refuses to touch any commit that already exists on a remote.
Force mode (force keyword)
If the invocation contains the standalone token force or --force (case-insensitive) — e.g. /git-commit-rewrite force, "force rewrite these commits", "force로 다시 써줘" — run in force mode. The keyword is explicit, pre-granted consent for a destructive in-place rewrite, so skip every interactive gate and apply directly. (The trigger is the English token force/--force; the Korean word 강제로 by itself does not trigger it.)
- Skip safety Checks A & B (dirty tree / detached HEAD) — do not pre-emptively stop. Git still enforces its own rules:
git filter-branchrefuses a genuinely dirty tree ("Cannot rewrite branches: You have unstaged changes"). If git errors, surface it verbatim — do not work around it. - Skip the Step 2 pushed-commits menu — auto-select the in-place rewrite even if commits already exist on a remote. Do not prompt.
- Skip the Step 5 wait — still show the old → new plan for transparency, then apply immediately without waiting.
- Push: after the rewrite, if any rewritten commit was already on the remote, finish with
git push --force(bare). This is the only place bare--forceis permitted, and only because the user opted in with the keyword. If nothing was pushed, the rewrite stays local — do not push. - Cleanup is unchanged — keep the
refs/original/backup ref and report it (Step 8).
Without the force keyword, follow the full cautious workflow below.
Step 1: Determine the rewrite range
Default base:
upstream=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || true)
if [ -n "$upstream" ]; then
base=$(git merge-base HEAD "$upstream")
elif git rev-parse --verify main >/dev/null 2>&1; then
base=$(git merge-base HEAD main)
elif git rev-parse --verify master >/dev/null 2>&1; then
base=$(git merge-base HEAD master)
else
echo "Could not determine a base. Ask the user for an explicit range." >&2
exit 1
fi
If the user specifies a count (e.g. "rewrite last 5"), use HEAD~5 as the base instead. Never use --root.
Step 2: Safety checks
Force mode skips this entire step (see "Force mode" above) — go straight to Step 3.
Three checks must pass before rewriting in place:
Check A — Working tree clean:
[ -z "$(git status --porcelain)" ]
Fail → tell user to commit/stash first; stop.
Check B — HEAD attached:
git symbolic-ref -q HEAD >/dev/null
Fail → tell user to checkout a branch; stop.
Check C — No commit in range is on a remote:
for sha in $(git rev-list "$base..HEAD"); do
if [ -n "$(git branch -r --contains "$sha")" ]; then
pushed_commits+=("$sha")
fi
done
If pushed_commits is non-empty: do not silently refuse. Show the user this menu:
Found N commits in <base>..HEAD whose subjects are non-Conformant.
M of them are already pushed to a remote:
abc1234 origin/main "Added User-Agent Parser"
def5678 origin/main "Fixed bug"
...
Rewriting published history breaks pulls for collaborators. Options:
1. Cancel — keep history as-is
2. Rewrite locally + force-push (destructive; coordinate with team first)
3. Cherry-pick onto a NEW branch with rewritten messages (safe; original branch untouched)
Which option? (default: 1)
- Option 1 → exit cleanly
- Option 2 → require explicit phrase like "yes force push" before continuing; after rewrite, run
git push --force-with-lease(NEVER--force) - Option 3 → see "Branch-based rewrite" below
Step 3: Identify non-conformant commits
For each commit in <base>..HEAD, check the subject against:
^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?: .+
Skip commits that already match. Skip merge commits (use git rev-list --no-merges).
Step 4: Generate new messages
For each non-conformant commit:
- Inspect:
git show --stat <sha>and the diff body if needed - Pick a
typefrom the table above based on the change content - Pick an optional
scope(module / file area) — omit if unsure - Write a new subject: imperative mood, lowercase, ≤72 chars, no trailing period
- Preserve the original commit body verbatim. Only the subject line is rewritten. If the original body is empty, leave it empty.
See the "Mapping Common Non-Conformant Patterns" section below for typical translations.
Step 5: Show old → new plan
Rewrite plan: 3 commits
abc1234 added login feature
→ feat(auth): add login feature
def5678 Fixed bug
→ fix(api): handle null response in user fetch
ghi9012 WIP
→ chore: work-in-progress checkpoint
Wait for explicit confirmation before applying. (Force mode shows this plan but does not wait — it applies immediately.)
Step 6: Apply the rewrites (in-place)
Build the mapping file at /tmp/cc-rewrite-map.tsv. Each non-conformant commit gets one line:
<full-40-char-sha>\t<new full message including preserved body>
The new full message is <new subject>\n\n<original body> (or just <new subject> if there is no body). Newlines inside the message are escaped as \n in the TSV; rewrite_msg.py un-escapes them.
Then run (use absolute path to the script):
script="$HOME/.claude/skills/git-commit/scripts/rewrite_msg.py"
# Plugin install path may differ; resolve via:
# script=$(find ~/.claude -name rewrite_msg.py -path '*git-commit*' | head -1)
FILTER_BRANCH_SQUELCH_WARNING=1 git filter-branch -f \
--msg-filter "python3 '$script'" \
"$base..HEAD"
The script reads $GIT_COMMIT, looks it up in /tmp/cc-rewrite-map.tsv, and prints the new message. Commits not in the map pass through unchanged.
If git filter-repo is available (preferred over filter-branch, which is deprecated): use it instead. But the bundled mapping flow currently relies on filter-branch; only switch if the user has filter-repo and prefers it.
Step 7: Branch-based rewrite (Option 3 from Step 2)
When commits are pushed and the user picks Option 3, do this instead of in-place rewrite:
# 1. Note current branch
src=$(git branch --show-current)
# 2. Create a fresh branch off the upstream/base
git checkout -b "${src}-cc" "$base"
# 3. Cherry-pick each commit, rewriting message as you go
for sha in $(git rev-list --reverse --no-merges "$base..$src"); do
new_msg=$(lookup_in_map "$sha") # from Step 4 plan; or compose interactively
git cherry-pick "$sha"
git commit --amend -m "$new_msg" # only the cherry-pick we just made
done
# 4. Verify, then push the new branch
git log --oneline "$base..HEAD"
git push -u origin "${src}-cc"
The original branch is untouched. The user can open a PR from ${src}-cc and abandon the original branch when ready. No force push, no destruction.
Step 8: Post-rewrite cleanup
After in-place rewrite (Step 6):
- Run
git log --oneline "$base..HEAD"and show the result. - Tell the user a backup ref was kept at
refs/original/refs/heads/<branch>and how to discard it once they're satisfied:
Do not delete it automatically.git update-ref -d refs/original/refs/heads/<branch> - Delete the temp file:
rm -f /tmp/cc-rewrite-map.tsv
After branch-based rewrite (Step 7):
- Tell the user how to switch back if they change their mind:
git checkout <original-branch> git branch -D <branch>-cc
Worked Example: messy history
User: 커밋 메시지 다시 써줘
(User shares git log:)
96b6be8 gnhf #43: Added User-Agent Parser ...
eda73c5 gnhf #42: Added JSON Schema Generator ...
e943794 gnhf #41: Added IPv4 Subnet Calculator ...
0a7e01e gnhf #40: Added Markdown TOC Generator ...
Step 1 (range):
upstream = origin/gnhf/prd-chann-tools-2026-8f2964
base = $(git merge-base HEAD origin/main)
Step 2 (safety):
✓ working tree clean
✓ HEAD attached
✗ all 4 commits exist on origin/<branch>
→ Show menu (Cancel / Force-push / New branch). User picks 3.
Step 3 (regex): all 4 fail the regex (start with "gnhf #N:")
Step 4 (mapping):
"gnhf #43: Added User-Agent Parser..." → feat(api-network): add user-agent parser
"gnhf #42: Added JSON Schema Generator" → feat(data-format): add JSON schema generator
"gnhf #41: Added IPv4 Subnet Calculator" → feat(api-network): add IPv4 subnet calculator
"gnhf #40: Added Markdown TOC Generator" → feat(text): add Markdown TOC generator
Drop the "gnhf #N" prefix (ticket-tracker noise; belongs in the body or a footer).
To preserve the ticket reference, add a footer:
Refs: gnhf#43
Step 5: show old → new plan, wait for confirmation.
Step 7 (branch-based):
git checkout -b feature-tools-cc origin/main
for sha in (cherry-pick each, --amend with new message)
git push -u origin feature-tools-cc
Mapping Common Non-Conformant Patterns
Use these translations when generating new subjects in Step 4.
| Original pattern | Likely Conventional rewrite |
|---|---|
Added X / Add X |
feat: add X (or feat(scope): add X) |
Fixed X / Fix X / Bugfix: X |
fix: <imperative> |
Updated X (cosmetic/dep bump) |
chore: update X or build(deps): bump X |
Updated X (real refactor) |
refactor: <imperative> |
Cleanup / Clean up X |
refactor: <imperative> or chore: <imperative> |
Renamed X to Y |
refactor: rename X to Y |
Removed X / Delete X |
refactor: remove X (or feat!: if breaking) |
WIP / wip / tmp |
chore: work-in-progress checkpoint |
Initial commit |
chore: initial commit |
Merge branch 'X' |
Skip (merge commits are excluded) |
[TICKET-123] Added X |
feat: add X + footer Refs: TICKET-123 |
proj #N: Added X |
feat(scope): add X + footer Refs: proj#N |
Tests for X |
test: add tests for X |
Docs: <anything> |
docs: <imperative> |
Refactor X (already imperative-ish) |
refactor: <imperative form> |
Rules of thumb:
- Past tense → imperative ("Added" → "add", "Fixed" → "fix")
- Capitalized → lowercase
- Strip trailing periods
- Ticket-tracker prefixes → move to a
Refs:footer (don't drop the linkage) - If the type is genuinely ambiguous between
chore,refactor, andstyle, preferchorefor tooling/config,refactorfor code-shape changes,styleonly for whitespace/formatting
Quick Reference
When stuck, ask:
| Symptom | Action |
|---|---|
| No working changes + messy history | Suggest /git-commit-rewrite |
| Working changes + messy history | Commit first, then /git-commit-rewrite |
| Pushed history + want to fix | Use Step 7 (branch-based rewrite) |
| Pre-commit hook fails | Fix the underlying issue; never --no-verify |
| Push rejected (non-FF) | Stop, surface error, never --force without explicit consent |
git filter-branch warns about deprecation |
FILTER_BRANCH_SQUELCH_WARNING=1 is already set; ignore |
Common Mistakes
Combining feat + fix in one commit
- Problem:
feat(auth): add OAuth and fix null responsemixes types - Fix: Two separate commits, one feat one fix
Past-tense subjects
- Problem: "added login flow", "fixed bug"
- Fix: Imperative — "add login flow", "handle null response"
git add . to stage everything
- Problem: Accidentally stages secrets or unrelated changes
- Fix: Always
git add <explicit-paths>per group
Bypassing hooks with --no-verify
- Problem: Hides real issues; commit "works" locally but fails CI
- Fix: Investigate the hook failure; fix root cause; create fresh commit
Silently refusing rewrite of pushed history
- Problem: Says "I can't" with no path forward
- Fix: Present the 3-option menu in Step 2 (cancel / force-push / branch-based)
Dropping ticket references during rewrite
- Problem: "gnhf #43: Added X" → "feat: add X" loses the
gnhf#43linkage - Fix: Move ticket to a
Refs: gnhf#43footer; don't lose it
Rewriting merge commits
- Problem: Filter-branch on merges scrambles parent linkage
- Fix:
git rev-list --no-mergesalways
Red Flags
Never:
git add .orgit add -A(use explicit paths)--no-verify,--no-gpg-sign,--amendwithout explicit user consent--forcepush — except/git-commit-rewriteforce mode (theforcekeyword is explicit consent); otherwise--force-with-leaseonly after explicit consent- Commit suspected
.env*files (except the exact basename.env.example),credentials.*,*_rsa,*.pem,*.key, or*.p12without explicit user override - Combine
featandfixin one commit - Rewrite pushed commits without showing the 3-option menu first — except in force mode, where the
forcekeyword is the pre-consent - Use
git filter-branch --root - Drop ticket references during rewrite (move them to
Refs:footer)
Always:
- Show a commit plan before creating commits
- Run
git status --shortafter each commit to verify - Preserve the original commit body verbatim during rewrite
- Use
--no-mergeswhen listing commits to rewrite - Default to "Cancel" when prompting on pushed commits
- Match the repo's existing scope conventions (
git log -20 --onelinefirst)
Behavior Notes
Match the repo's existing style. Inspect git log -20 --oneline first. If the repo uses Conventional Commits, mirror its scope conventions. If existing messages are mixed, default to lowercase descriptions and concise scopes.
Narration vs. commit-message language. Conversation updates: user's prompt language. Commit messages: language already dominant in git log (English when ambiguous).
Be conservative with scopes. A scope must name a real module/area. If unsure, omit the scope rather than invent one.
Don't over-explain in bodies. A body should add context the subject can't carry (the why, a tricky tradeoff, related issue). If the subject is self-explanatory, no body.
Refuse, don't bypass. If a safety check fails (pushed commits without consent, dirty tree, detached HEAD, suspected secret), report and stop. Do not work around with destructive flags. Exception: /git-commit-rewrite force mode, which the user opts into explicitly with the force keyword (see Force mode).
Trust explicit user override. "Force push", "yes I know it's pushed, rewrite anyway", "include the .env this time" → proceed, but state the risk in one line first. For /git-commit-rewrite, the force keyword is the formalized form of this (see Force mode).
Integration
Pairs with:
- code-review — Run
/code-reviewbefore committing as a final quality gate - git-merge-to-main / git-merge-to-dev — After committing, integrate the branch
- Any plugin commit hooks (commitlint, husky) — fix violations rather than
--no-verify
Called by: Manual user invocation only. Never auto-run during another skill's workflow.