Inspect the working tree, confirm the branch is one the user will commit on, split the diff into atomic concerns, draft a strict conventional commit per concern, get approval, and commit each in order. Never push, never open a PR: that is /pr's job.
Arguments, if any, are user guidance: a scope, how to split concerns, or a branch name. Factor them into the plan before the approval gate.
-
Resolve base and current branch:
BASE=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's#^origin/##')
BRANCH=$(git branch --show-current)
If BASE is empty, origin/HEAD is unset. Populate it once with git remote set-head origin -a (plain git, works on any host), then re-read. With no origin remote at all, BASE stays empty: skip the default-branch comparison in Step 2 and treat the current branch as the working branch.
-
Branch gate:
BRANCH == BASE (on the default branch) → ask: commit into $BASE, or create a new branch?
BRANCH is a non-default branch:
- Matches the work at hand (named for this change, or the user pointed you at it) → use it, no prompt.
- Looks unrelated to the diff (a
chore/bump-*, a release branch, someone else's feature branch) → ask before reusing it; on "new branch", follow the same name prompt + validation.
-
Staging gate. Run git status --porcelain=v1. If nothing is staged or unstaged → stop ("no changes to commit"). Otherwise the candidate is the staged set; if there are unstaged changes not already staged, ask whether to fold them in (stage all, pick a subset, or leave out) and print what's left out. If leaving out empties the candidate set (nothing was staged), that's a cancel: stop with "no changes to commit". Never auto-stage.
-
Concern analysis: decide how many commits.
- Read the candidate diff (
git diff --cached if staged, else git diff HEAD). For the drafting view only, exclude noisy paths that add no signal:
**/package-lock.json, **/yarn.lock, **/pnpm-lock.yaml, **/bun.lock*,
**/*.min.js, **/*.min.css, **/*.map,
**/dist/**, **/build/**, **/.next/**, **/node_modules/**,
**/*.generated.*, **/*_generated.*, **/*.pb.ts
They still show in --stat, so commit them with the concern they belong to and mention them in the body if useful.
- Cluster files into concerns by intent, not directory. A concern is a self-contained change with one purpose.
- Separate: a fix + an unrelated config tweak; a feature + a dep bump; a refactor + a doc update.
- Same: a feature spanning N files; a rename across the repo; a fix and its test.
- Order so each commit leaves the build green: config / deps first, then features and fixes, cosmetic / cleanup last.
- Hunk-level splits: if one file mixes concerns, plan
git add -p <file> and note it in the plan.
- Don't force a split when the diff is cohesive: one feature touching ten files is one commit.
-
Draft the plan: one entry per concern.
- Header:
<type>(<scope>): <subject>, ≤ 100 chars, imperative, no leading capital, no trailing period. Types: feat | fix | chore | docs | refactor | test | perf | ci | build | style | revert. Derive scope from the touched area (monorepo package, top-level directory, role name in this dotfiles repo, feature area like auth/api); omit (<scope>) only when genuinely repo-wide, and surface the doubt at the gate if unclear.
- Body: only when the why isn't obvious from the subject. Blank line after subject, wrap at 100 cols, short
- bullets, no prose paragraphs. Skip rather than pad.
- Never write
Co-Authored-By: Claude … or 🤖 Generated with …. Attribution is handled by the attribution setting in settings.json; duplicating it in the message body produces the trailer even when attribution is configured to be empty. The git-skill-gate hook hard-blocks messages containing these lines.
-
Humanize: plain verbs (add, fix, remove, rename, move, drop, bump, split), no AI vocabulary (leverage, robust, seamless, comprehensive, enhance, streamline, foster), no promotional tone, no em dashes between clauses, no emojis. Be specific: fix(checkout): handle empty cart on /checkout beats fix: bug in checkout.
-
Approval gate (mandatory). Print the plan (per commit: number, subject, file group, any folded-in noisy files, body). Then call AskUserQuestion:
question: "Proceed with these commits?", header: "Commit plan", multiSelect: false
- options:
Go (commit each entry in order), Cancel (stop, commit nothing, unstage nothing).
- The structured question is the gate: don't wait for prose like
go / lgtm / ok. Free-form confirmations between the plan and the commits break attributionSkill in the transcript and cause git-skill-gate.sh to block.
- On
Other, integrate the redirect ("change commit 2's scope", "split commit 1", "drop the last"), replan from concern analysis if needed, and re-run this gate.
-
Commit each concern in order: