mcpField
G
git-commit

git-commit

github.com/channDevelopment051 installsUpdated 2d ago
Repository

About git-commit

A skill for grouping working-tree changes into Conventional Commits or rewriting recent non-conformant commit subjects. It separates commit creation, pushing, and history rewriting into distinct workflows with explicit safety checks.

Key features

  1. 01Group changes into logical commits
  2. 02Format subjects as Conventional Commits
  3. 03Route clean trees to rewrite checks
  4. 04Inspect secrets and show a commit plan
  5. 05Push or rewrite history with safety checks

Use cases

  1. 01Commit unrelated changes separately
  2. 02Standardize recent commit subjects
  3. 03Push commits without force
  4. 04Review files and commit messages before changes

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/chann/skills --skill git-commit

After installing, type/git-commitin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

agents/
openai.yaml
evals/
evals.json
scripts/
rewrite_msg.py

Details

From the project README

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

CommandSkillActionOperates on
/git-commitgit-commitGroup working-tree changes into Conventional CommitsUncommitted (staged + unstaged) changes
/git-commit-pushgit-commit-pushSame as above, then git push (no --force)Uncommitted changes + remote
/git-commit-rewritegit-commit-rewriteRewrite recent non-Conformant commit subjectsExisting 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?
SituationCommand
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 messyFirst 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

TypeMeaning
featNew feature (MINOR semver bump)
fixBug fix (PATCH semver bump)
docsDocumentation only
styleFormatting / whitespace; no logic change
refactorCode change that neither fixes a bug nor adds a feature
perfPerformance improvement
testAdding or fixing tests
buildBuild system or external dependencies
ciCI configuration files and scripts
choreOther changes that don't modify src or tests
revertReverts 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 feat from fix — never combine
  • Separate docs from 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 refactor from feat
  • 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 . or git add -A — always explicit paths
  • Never --no-verify, --no-gpg-sign, --amend (unless the user explicitly asks)
  • Never add emoji or Co-Authored-By: Claude unless requested
  • HEREDOC for multi-line messages so quoting stays correct
  • After each commit, run git status --short to 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-branch refuses 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 --force is 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:

  1. Inspect: git show --stat <sha> and the diff body if needed
  2. Pick a type from the table above based on the change content
  3. Pick an optional scope (module / file area) — omit if unsure
  4. Write a new subject: imperative mood, lowercase, ≤72 chars, no trailing period
  5. 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:
    git update-ref -d refs/original/refs/heads/<branch>
    
    Do not delete it automatically.
  • 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 patternLikely Conventional rewrite
Added X / Add Xfeat: add X (or feat(scope): add X)
Fixed X / Fix X / Bugfix: Xfix: <imperative>
Updated X (cosmetic/dep bump)chore: update X or build(deps): bump X
Updated X (real refactor)refactor: <imperative>
Cleanup / Clean up Xrefactor: <imperative> or chore: <imperative>
Renamed X to Yrefactor: rename X to Y
Removed X / Delete Xrefactor: remove X (or feat!: if breaking)
WIP / wip / tmpchore: work-in-progress checkpoint
Initial commitchore: initial commit
Merge branch 'X'Skip (merge commits are excluded)
[TICKET-123] Added Xfeat: add X + footer Refs: TICKET-123
proj #N: Added Xfeat(scope): add X + footer Refs: proj#N
Tests for Xtest: 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, and style, prefer chore for tooling/config, refactor for code-shape changes, style only for whitespace/formatting

Quick Reference

When stuck, ask:

SymptomAction
No working changes + messy historySuggest /git-commit-rewrite
Working changes + messy historyCommit first, then /git-commit-rewrite
Pushed history + want to fixUse Step 7 (branch-based rewrite)
Pre-commit hook failsFix the underlying issue; never --no-verify
Push rejected (non-FF)Stop, surface error, never --force without explicit consent
git filter-branch warns about deprecationFILTER_BRANCH_SQUELCH_WARNING=1 is already set; ignore

Common Mistakes

Combining feat + fix in one commit

  • Problem: feat(auth): add OAuth and fix null response mixes 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#43 linkage
  • Fix: Move ticket to a Refs: gnhf#43 footer; don't lose it

Rewriting merge commits

  • Problem: Filter-branch on merges scrambles parent linkage
  • Fix: git rev-list --no-merges always

Red Flags

Never:

  • git add . or git add -A (use explicit paths)
  • --no-verify, --no-gpg-sign, --amend without explicit user consent
  • --force push — except /git-commit-rewrite force mode (the force keyword is explicit consent); otherwise --force-with-lease only after explicit consent
  • Commit suspected .env* files (except the exact basename .env.example), credentials.*, *_rsa, *.pem, *.key, or *.p12 without explicit user override
  • Combine feat and fix in one commit
  • Rewrite pushed commits without showing the 3-option menu first — except in force mode, where the force keyword 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 --short after each commit to verify
  • Preserve the original commit body verbatim during rewrite
  • Use --no-merges when listing commits to rewrite
  • Default to "Cancel" when prompting on pushed commits
  • Match the repo's existing scope conventions (git log -20 --oneline first)

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-review before 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.

Comments

Markdown supported