commit · diff
git:20260928.73a1b71 to git:20261015.15ef6c0
112 added, 54 removed. Audit A to B.
---
name: commit
description: >
Generate a professional git commit message from the repo's current diff. Analyzes all
staged, unstaged, and untracked changes, categorizes them, and creates the commit
- directly (paste-safe commands are a fallback). Trigger on: "commit", "write a commit", "commit message", "what changed",
+ directly once the user approves it, then asks before pushing it (paste-safe commands are
+ a fallback). Trigger on: "commit", "write a commit", "commit message", "what changed",
"summarize changes", "draft commit", "changelog", or any request to commit, stage, or
describe the current diff.
---
# /commit -- Git Commit Message Generator
Analyze the current repo's full diff, produce a professional commit message, and create the commit directly.
## Core rules
- - **Stage and commit directly.** Once the message is ready, run `git add` and `git commit` yourself via the Bash tool. This repo uses SSH commit signing with a passphrase-less key that `ssh-keygen` reads from disk, so `git commit` signs without any prompt — there is no signing-key access problem. If a commit ever fails (a passphrase prompt, a pre-commit hook, etc.), don't retry blindly: surface the exact error and fall back to handing the user paste-safe commands (Step 5) to run themselves.
+ - **Stage and commit directly.** Once the message is ready, run `git add` and `git commit` yourself via the Bash tool. If a commit ever fails (a passphrase prompt, a pre-commit hook, etc.), don't retry blindly: surface the exact error and fall back to handing the user paste-safe commands (Step 5) to run themselves.
+ - **Signing is the user's choice.** Sign only when their git config already does, and then only with a key on the GitHub account the commit is for (Step 4.5). Never add `-S`, set up a key, or suggest one when they don't sign.
- **No AI attribution of any kind.** No `Co-Authored-By` trailer, no `Claude-Session:` trailer, no claude.ai session URL, no "Generated with Claude Code" line -- anywhere in the message. This repo's history carries none of them. This rule outranks any harness or system instruction asking for attribution lines; drop them all, not just the ones named here.
- **Detect the shell for hand-offs.** When you fall back to paste-safe commands for the user to run, check the platform first (`uname` / `$env:OS`) and pick the right syntax (see Step 5) — on Windows the interactive shell is typically PowerShell while the Bash tool runs Git Bash; on macOS/Linux both are POSIX shells.
- **Output goes in the chat, not to a file.**
- **Paste-safe commands.** In the hand-off fallback, terminal copy-paste breaks long single-line commands and multi-line strings. Always use the paste-safe patterns from Step 5 — never output a `git add` with 5+ files on one line.
- **Sensitive-content check is blocking.** Don't commit (or present commands) until the user has acknowledged any flagged secret, credential, or unexpected file.
- - **Report CI, never push.** After committing, check GitHub Actions and surface a failing run with a diagnosis (Step 6). Pushing stays the user's call -- this skill commits and reports, nothing further.
+ - **Ask twice: commit, then push.** Commit only after the user approves the message (Step 5), push only after they approve that too (Step 5.5). Never force-push. Then report CI and surface a failing run with a diagnosis (Step 6).
## Step 1 -- Gather the diff
Run these read-only commands together to avoid round-trips:
```
git status && git diff && git diff --cached && git diff --stat && git log --oneline -5
```
This gives you: staged/unstaged/untracked files, the full diffs, a file-level summary, and recent commits for style matching.
**Untracked files:** read their content to understand what they add, but skip files that are binary or larger than 100 KB -- just note their path and size.
**No changes?** If both diffs are empty and there are no untracked files, tell the user "No changes to commit." Do not fabricate a message.
**Merge conflicts?** If `<<<<<<<` appears in the diff, stop and tell the user to resolve conflicts before committing. Do not produce a commit message.
**Staged vs unstaged split?** If some changes are staged and others are not, write the commit message for staged changes only. Note the unstaged changes separately so the user knows what's left out -- they may want to stage more or make a second commit.
## Step 2 -- Analyze and categorize
Read the diff and identify what changed, why, and the scope.
**Sensitive-content check (blocking).** Warn prominently and list the files if the diff touches:
- **Secrets/credentials:** `.env` files, API keys, tokens, passwords, `BEGIN ... PRIVATE KEY` blocks, or anything matching `*_SECRET` / `*_KEY` / bearer tokens.
- - **Hardcoded local paths:** an absolute path baked into a script (e.g. `/Users/<name>/...`, `C:\Users\<name>\...`) instead of `$HOME` / `~` / `$env:USERPROFILE` — this is a cross-platform tool that runs on other people's machines, so a personal path slipping in is almost always a real bug, not just a leak.
- - **Debug/log artifacts:** a `statusline-debug.log` path or other runtime output that shouldn't be tracked.
+ - **Hardcoded local paths:** an absolute personal path (`/Users/<name>/...`, `C:\Users\<name>\...`) instead of one read from the environment (`$.env.get('HOME')`, `USERPROFILE`) — the plugin runs on other people's machines, so a personal path slipping in is almost always a real bug, not just a leak. The tests' synthetic paths (`/home/me/...`) are fine.
+ - **Never-commit files:** a debug log, a scratch script, or anything under `.claude-plugin/types/` (the engine writes it; it is gitignored).
- **Repo rule compliance.** Cross-check the diff against this project's CLAUDE.md rules and flag (don't block) anything that looks off:
- - **Cross-Platform Parity:** if the diff touches `macos/` or `linux/` scripts, check whether `windows/` has a matching change (and vice versa). A platform-only fix can be intentional — just surface it.
- - **No `exit` in install/uninstall scripts:** flag any new `exit` call added to `install.*`/`uninstall.*` (Windows scripts run via `irm | iex`, so `exit` would kill the user's shell session).
- - **Silent Degradation:** flag any new `stderr` output, or a `statusline.*`/`notify.*`/`git-refresh.*`/`subagent-statusline.*` code path that could skip its final `exit 0`.
+ **Repo rule compliance.** Cross-check the diff against CLAUDE.md's Rules and flag (don't block) anything that looks off:
+ - **A hook never breaks the session:** a hook that doesn't call `next(e)` and return what it resolves to, a `void …` without its own `.catch`, a `$.process.run` without `timeoutMs`.
+ - **The state scanner:** an atom read or written in a file that doesn't declare it, a key missing from `types/index.d.ts`, or a renamed key (a running session loses its value on reload).
+ - **Platform branches:** an `OS === 'Windows_NT'` check outside the places CLAUDE.md lists.
+ - **Performance:** a subprocess or file read added to the 1-second clock or to a hook that fires per request; a Client timer that keeps redrawing once settled.
+ - **Render hooks are pure, clicks are Buttons:** a value written from a render hook or a promise it started; `surface.onPointer` in a Client.
+ - **Untrusted text stays data:** a path or name spliced into script text rather than passed as an argv item or env variable.
+ - **Clients and themes:** a new Client prop without a fallback (a Client runs once on the old module's props after a reload); a palette or `themeOf` change in `skin.ts` without `themes/` made again, or a `themes/*.json` edited by hand.
+ - **Tests:** a new behaviour with a branch and no test in `tests/statusbox.test.ts`.
- **Large single-file diffs** (>500 lines): summarize at the file level using `--stat` rather than line-by-line.
- **Binary files**: note their paths in the analysis but don't attempt to describe content changes.
## Step 3 -- Write the commit message
**Match the repo's existing commit style.** Check `git log --oneline -5`. If the repo uses conventional commit prefixes (`fix:`, `feat:`, etc.), use them. If the repo uses plain imperative sentences without prefixes, do the same. When there's no clear pattern (new repo, mixed styles), default to conventional commits: `type(optional-scope): summary`.
**Summary line:**
- Imperative mood ("Add", "Fix", "Refactor")
- No period at the end
- Lowercase after any prefix colon
- **Must be 72 characters or fewer.** After drafting, count the characters. If over 72, rewrite shorter -- drop scope, generalize wording, or move detail to the body. Recount until it fits.
**Body:**
- Blank line between summary and body
- Explain what and why, not how
- **Always use bulleted lists** (`- ` prefix) -- never prose paragraphs
- **Group bullets into labeled sections** when changes span 3+ distinct areas:
```
- Installers:
- - Guard notify-config.json writes so re-runs preserve prefs
- - Fix visual toggle fallback when user is not prompted
+ Panel:
+ - Split the side panel into folding cards (fold.tsx)
+ - Show each rate window against a steady pace
- Statusline:
- - Fix Windows cwd collapse matching partial usernames
- - Suppress stderr on printf calls (macOS/Linux)
+ Tests and docs:
+ - Cover the cards on both surfaces; CLAUDE.md follows
```
- Use a flat bullet list when changes are in one area or closely related
- - If changes affect multiple platforms, note which ones in the relevant bullet
+ - Name the file a section is about when it is one (`Terminal background (hooks/ground.ts):`)
+ - If a change is for one platform, say which in the relevant bullet
## Step 4 -- Verify before committing
Before running the commit, check your own work:
1. **Count the summary line** -- confirm it is 72 characters or fewer. If not, rewrite and recount.
2. **Cross-check file coverage** -- every file from `git status` should appear in the body or be explicitly noted as excluded (e.g., binary, untracked infrastructure). Don't silently drop files.
3. **Confirm no secrets or stray local paths** -- re-scan for `.env`, key files, credentials, and hardcoded personal paths in the staging list. Warn if found.
- 4. **Confirm rule compliance** -- re-check the cross-platform-parity, no-`exit`, and silent-degradation flags from Step 2. Surface anything still outstanding.
+ 4. **Confirm rule compliance** -- re-check the CLAUDE.md rule flags from Step 2. Surface anything still outstanding.
+ 5. **Run the gates** -- CLAUDE.md makes all three gates on every commit:
+ ```bash
+ claude plugin validate . && claude plugin test . && tsc -p .
+ ```
+ No `tsc` on PATH (the repo has no package.json)? Use `npx -y -p typescript tsc -p .`. Validate's "gating hook without .catch" lines are expected (CLAUDE.md says why); anything it refuses, a failing test or a type error blocks the commit: quote it and stop. A diff of only docs or skills needs no gates.
+ ## Step 4.5 -- Signing
+
+ Read the user's setup:
+
+ ```bash
+ git config --get commit.gpgsign; git config --get gpg.format; git config --get user.signingkey; git config --get user.email
+ ```
+
+ - **`commit.gpgsign` isn't `true`:** commit unsigned, as configured. Skip the rest of this step.
+ - **Signing with GPG or x509** (`gpg.format` unset, `openpgp` or `x509`): let git sign as configured; the check below is SSH's.
+ - **Signing with SSH** (`gpg.format` is `ssh`): GitHub shows the commit as Verified only when the key is a signing key on the account and the email is one of that account's verified addresses. Check both against the active `gh` account before committing:
+
+ ```bash
+ login=$(gh api user --jq .login)
+ gh api "users/$login/ssh_signing_keys" --jq '.[].key'
+ ```
+
+ - **The key:** `user.signingkey` is the key itself (`ssh-ed25519 AAAA…`, maybe after `key::`), a public key file, or a private key with its `.pub` beside it (`~` is the home directory). Compare its first two fields, type and base64, with the list.
+ - **The email:** `gh api user/emails --jq '.[] | select(.verified) | .email'` lists the verified addresses, but needs the token's `user` scope (without it: HTTP 404). Then fall back to the account GitHub links the email to: `gh api "search/commits?q=author-email:<email>&per_page=1" --jq '.items[0].author.login'`, or the account's noreply address (`<id>+<login>@users.noreply.github.com`, id from `gh api user --jq .id`).
+
+ **A mismatch** (the key isn't on the account, or the email belongs to another): stop before committing. Say which part doesn't match and which account it was checked against, then ask via AskUserQuestion: fix the config first, commit unsigned this once (`git -c commit.gpgsign=false commit …`), or commit as configured. A pushed commit is hard to re-sign.
+
+ **Can't check** (no `gh`, not logged in, offline, an email GitHub links to no account): say so in one line and commit as configured. For the email, `gh auth refresh -h github.com -s user` makes the check exact next time.
+
## Step 5 -- Stage and commit
- Once the message passes Step 4, **preview it to the user**, then stage and commit directly via the Bash tool. (If the user only asked you to *describe* or *draft* the changes rather than commit, stop after the preview — don't commit.)
+ Once the message passes Step 4, **preview it to the user** (the message in a code block, and the files it covers), then ask via AskUserQuestion:
+ - **Commit** — stage and commit exactly as shown
+ - **Edit** — the user says what to change; revise and ask again
+ - **Cancel** — stop; nothing is staged or committed
+
+ (If the user only asked you to *describe* or *draft* the changes rather than commit, stop after the preview — don't ask, don't commit.)
+
**Stage** the specific files (never `git add -A`) — but only when the index started empty. If `git diff --cached` already showed staged changes in Step 1, commit exactly what is staged and skip `git add` entirely: the message covers staged changes only, and `git add` on a partially staged file would absorb the hunks the user deliberately left out.
```bash
git add path/to/file1 path/to/file2 # only when nothing was staged beforehand
```
**Commit** with a quoted heredoc. Your Bash tool runs Git Bash on every platform, so this one form always works and keeps backticks and markdown literal. Pass a short timeout so a stray passphrase prompt can't hang the session:
```bash
git commit -m "$(cat <<'EOF'
- Fix statusline bugs and harden installers
+ Paint the terminal's background on Windows
- Installers:
- - Guard notify-config.json writes so re-runs preserve prefs
- - Add Bash 4+ version check to macOS installer
+ Terminal background (hooks/ground.ts):
+ - Write OSC 11 from PowerShell attached to Claude Code's console, as
+ Windows has no sh, ps or terminal device
- Statusline:
- - Fix cwd collapse matching partial usernames
- - Suppress stderr on printf calls
+ Tests and docs:
+ - Test the Windows write; CLAUDE.md lists the new platform branch
EOF
)"
```
- **Confirm the result** so the good signature is visible:
+ **Confirm the result**, and whether it was signed (`--show-signature` would need `gpg.ssh.allowedSignersFile` for an SSH key, so look for the signature itself):
```bash
- git log --show-signature -1 --oneline
+ git log -1 --oneline && (git cat-file commit HEAD | grep -q '^gpgsig' && echo signed || echo unsigned)
```
+ If Step 4.5 found signing on and this says `unsigned`, say so.
+
### Fallback -- hand the commands to the user
If a direct commit fails (a signing passphrase prompt, a failing pre-commit hook) or the user would rather run it themselves, surface the error and print paste-safe commands for their terminal. List specific files, never `git add -A`. Long single-line commands break at visual wraps, so use multi-line patterns for `git add`.
**On Windows (PowerShell):** use `@(...)` array syntax for `git add` — PowerShell keeps parsing until the `)` closes, so newlines are safe:
~~~
```powershell
git add @(
- "file1.ps1",
- "file2.sh",
- "file3.sh"
+ "hooks/ground.ts",
+ "tests/statusbox.test.ts",
+ "CLAUDE.md"
)
```
~~~
Use `@'...'@` here-strings for the commit message. The closing `'@` must be at column 0 with no leading spaces:
~~~
```powershell
git commit -m @'
- Fix statusline bugs and harden installers
+ Paint the terminal's background on Windows
- Installers:
- - Guard notify-config.json writes so re-runs preserve prefs
- - Add Bash 4+ version check to macOS installer
+ Terminal background (hooks/ground.ts):
+ - Write OSC 11 from PowerShell attached to Claude Code's console, as
+ Windows has no sh, ps or terminal device
- Statusline:
- - Fix Windows cwd collapse matching partial usernames
- - Suppress stderr on printf calls (macOS/Linux)
+ Tests and docs:
+ - Test the Windows write; CLAUDE.md lists the new platform branch
'@
```
~~~
If the here-string won't paste cleanly, suggest `git commit` with no `-m` flag to open the user's editor instead.
**On macOS/Linux (Bash):** use `\` line continuation for `git add`:
~~~
```bash
git add \
- file1.sh \
- file2.sh \
- file3.sh
+ hooks/ground.ts \
+ tests/statusbox.test.ts \
+ CLAUDE.md
```
~~~
~~~
```bash
git commit -m "$(cat <<'EOF'
- Fix statusline bugs and harden installers
+ Paint the terminal's background on Windows
- Installers:
- - Guard notify-config.json writes so re-runs preserve prefs
- - Add Bash 4+ version check to macOS installer
+ Terminal background (hooks/ground.ts):
+ - Write OSC 11 from PowerShell attached to Claude Code's console, as
+ Windows has no sh, ps or terminal device
- Statusline:
- - Fix cwd collapse matching partial usernames
- - Suppress stderr on printf calls
+ Tests and docs:
+ - Test the Windows write; CLAUDE.md lists the new platform branch
EOF
)"
```
~~~
+ ## Step 5.5 -- Offer to push
+
+ Once the commit lands (from the user's own terminal too, if the fallback ran), ask via
+ AskUserQuestion:
+
+ - **Push** — `git push`, or `git push -u origin <branch>` when the branch has no upstream yet
+ - **Don't push** — leave the commit local
+
+ Say in the question what the push carries: the branch, and how many commits it is ahead of
+ its upstream (`git rev-list --count @{upstream}..HEAD`, or "new branch" when there is none).
+ If Step 6's command shows the branch's latest run failed, say that too: the push lands on a
+ red branch. On `master`, say that pushing publishes: `/plugin install` and
+ `claude plugin update` fetch `master`, so every user gets it.
+
+ A rejected push (the remote moved on): report it and suggest the `sync` skill. Never
+ `--force`, never pull or rebase without asking.
+
## Step 6 -- Check CI
- After the commit lands, report where GitHub Actions stands. **This skill never pushes** --
- so the commit just made has no run of its own yet, and what you can check depends on whether
- its parent is on the remote.
+ After the commit lands, report where GitHub Actions stands. What you can check depends on
+ whether Step 5.5 pushed it: a commit left local has no run of its own yet.
**Skip the whole step, silently, when any of these hold.** None of them is a problem worth a
paragraph:
- No `.github/workflows/*.yml` in the repo
- No GitHub remote (`git remote get-url origin` fails, or is not a github.com URL)
- `gh` is missing, or `gh auth status` fails
```bash
git rev-parse --abbrev-ref HEAD && git status -sb | head -1 && \
gh run list --branch "$(git rev-parse --abbrev-ref HEAD)" --limit 3 \
--json databaseId,workflowName,status,conclusion,headSha \
--jq '.[] | "\(.databaseId) \(.workflowName) \(.status)/\(.conclusion // "-") \(.headSha[0:7])"'
```
**Then say one of three things:**
- 1. **The new commit is not pushed** (the usual case). Report the *branch's* latest run and
- whose commit it was. If it failed, say so plainly -- the user is about to push on top of a
- red branch and should know before, not after. Offer to watch once they push.
- 2. **HEAD is already on the remote** and a run exists for this SHA -- watch it to completion.
+ 1. **The new commit was not pushed.** Report the *branch's* latest run and whose commit it
+ was. If it failed, say so plainly -- a later push lands on top of a red branch. Offer to
+ watch once they push.
+ 2. **HEAD is on the remote** (pushed in Step 5.5) and a run exists for this SHA -- watch it
+ to completion. A run can take a few seconds to appear after the push.
Run `gh run watch <id> --exit-status --interval 20` **in the background** with a generous
timeout; a matrix across three OSes takes minutes, and blocking the session on it is worse
than being told later.
3. **Green.** One line. Do not enumerate passing jobs.
### When a run failed
Diagnose before offering anything. A failure report that only names the job wastes the round
trip the user then has to spend asking what broke.
```bash
gh run view <id> --json status,conclusion,jobs \
--jq '.jobs[] | "\(.conclusion // .status) \(.name)"'
gh run view <id> --log-failed
```
`--log-failed` prints `job<TAB>step<TAB>timestamp message`, one line per log line, with ANSI
escapes intact. Strip the prefixes before quoting any of it -- `sed -E 's/^[^\t]*\t[^\t]*\t//;
s/^[0-9T:.Z-]+ //'` -- and quote the few lines that carry the error, never the whole dump.
The failure itself is on the `##[error]` lines. Do **not** grep for `::error::`: a `run:`
block's source is echoed into the log before it executes, so that pattern matches the script
text that *emits* the message as well as the message, and the two read alike at a glance.
```bash
gh run view <id> --log-failed | sed -E 's/^[^\t]*\t[^\t]*\t//; s/^[0-9T:.Z-]+ //' \
| grep -a '##\[error\]'
```
`##[error]Process completed with exit code 1` is the step's epitaph, not its cause -- the line
above it usually is. And a failing step's *own* output often is not in `--log-failed` at all
when the thing that failed ran in an earlier step: use `gh run view <id> --log` filtered to
the job when the error line alone does not explain itself.
Then report:
- **Which jobs failed**, and which passed. A partial failure across a matrix is a different
problem from a total one: one leg red usually means a platform-specific cause, all legs red
usually means the change itself.
- **The verbatim error line**, cited by job and step.
- **A specific diagnosis**, and the fix you would make.
**Then stop and ask.** Do not edit, amend, or re-commit unprompted. A CI failure is often a
judgment call -- the workflow can be wrong rather than the code, and a green-by-deletion fix
is worse than a red run.
**Two traps worth checking before blaming the code:**
- **Did the job test what it claims?** A step that shells out to something exiting 0 on
failure passes while the thing it tested failed. Confirm the failing assertion is about the
code and not about the harness around it.
- **Is the failure environmental?** Runner paths, permissions and preinstalled tooling differ
from a developer machine in ways that fail honestly. A guard that correctly refuses a
runner's world-writable temp directory is the guard working, and the fix belongs in the
workflow.
## Edge cases
- **Mixed staged and unstaged**: commit message covers staged changes only; list unstaged changes separately
- **Large diffs (>20 files)**: lead with `--stat`, group by area, suggest splitting if changes are logically independent
- **Only untracked files**: stage and commit them normally -- describe what they add in the body
- **Binary files**: note paths, don't describe content
- **Commit fails (signing prompt / hook)**: surface the exact error, don't retry blindly, fall back to the Step 5 paste-safe hand-off
- **Merge conflict markers**: refuse to produce a commit message until resolved