Safely create a git commit by validating repository state, staging intended changes, scanning for secrets/conflicts, generating a Conventional Commits message (repository convention first, else an English Angular default) from the staged diff, and committing without amend.
Installs into .claude/skills of the current project.
Are you the author of Git Commit?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/johnqtcg-git-commit)
---
name: git-commit
description: Safely create a git commit by validating repository state, staging intended changes, scanning for secrets/conflicts, generating a Conventional Commits message (repository convention first, else an English Angular default) from the staged diff, and committing without amend.
disable-model-invocation: true
allowed-tools: Read, Grep, Bash(git add*), Bash(git commit*), Bash(git status*), Bash(git diff*), Bash(git log*), Bash(git stash*), Bash(git rev-parse*), Bash(git show*), Bash(go list*), Bash(go build*), Bash(go vet*), Bash(go test*), Bash(golangci-lint*), Bash(pytest*), Bash(ruff check*), Bash(flake8*), Bash(mypy*), Bash(pyright*), Bash(cargo check*), Bash(cargo clippy*), Bash(cargo test*), Bash(mvn test*), Bash(./gradlew*), Bash(npm*), Bash(yarn*), Bash(pnpm*), Bash(bun*), Bash(deno*), Bash(npx nx*), Bash(npx turbo*), Bash(npx lerna*), Bash(make test*), Bash(make check*), Bash(make*), Bash(git config --get*), Bash(gitleaks*), Bash(bash *secret-scan.sh*), Bash(bash *stash-guard.sh*), Bash(bash *run-gate.sh*), Bash(bash *detect-ecosystems.sh*), Bash(bash *resolve-scope.sh*)
metadata:
short-description: Safely commit staged changes with an Angular-style message
---
# Git Commit (Safety Enhanced, Angular Convention)
Create a commit from the current working tree with safety gates first, then generate a concise Angular-style message in English.
Maintaining this skill (not needed to run it): `references/design-evidence.md` records the measured evidence behind every contract below and the verified environment matrix; `scripts/run_cross_env_probe.sh`, `scripts/run_portability_matrix.sh` and `scripts/eval/` re-derive it.
## Hard Rules
- Never commit with unresolved conflicts.
- Never commit secrets, credentials, keys, or `.env` sensitive values.
- **Subject line must fit the repo's limit — default `<= 50 chars` total** (including `type(scope): `). A commitlint/`CONTRIBUTING` limit discovered in §5 replaces that default; the §6 guard enforces whichever applies. There is no separate absolute 50.
- Do not use `--amend` unless explicitly requested. If already pushed, warn about force push.
- If any safety gate fails, stop and report clearly.
- One commit = one logical change. Do not mix unrelated fixes, features, or formatting.
## Workflow
Bundled scripts are always invoked as `bash "<path-to-skill>/scripts/<name>.sh"` — `<path-to-skill>` is the absolute skill directory (where this SKILL.md was loaded from), never a path relative to the repository. Keep the working directory at the **target repo root**; the scripts read the repo through git.
### 1. Preflight
```bash
git rev-parse --is-inside-work-tree # must print "true"
git -c core.quotePath=false status --short # quotePath=false: see the raw path
BRANCH=$(git rev-parse --abbrev-ref HEAD) # "HEAD" means detached (allowed)
# In-progress operation? Worktree-safe: resolve real paths, never read .git/ directly.
for state in rebase-merge rebase-apply MERGE_HEAD CHERRY_PICK_HEAD REVERT_HEAD; do
[ -e "$(git rev-parse --git-path "$state")" ] && echo "IN_PROGRESS $state"
done
# Unresolved conflicts: this diff exits 0 regardless, so act on OUTPUT, not exit code.
git -c core.quotePath=false diff --name-only --diff-filter=U
```
STOP (hard block — report which one and what to resolve) if any of these hold:
- `--is-inside-work-tree` did not print `true`;
- any `IN_PROGRESS <state>` line printed (rebase / merge / cherry-pick / revert in progress);
- the conflict diff printed any path (unresolved merge conflicts).
A detached `HEAD` is not a stop condition — proceed and note it in the report.
### 2. Staging
- **Snapshot what is already staged first**: `git -c core.quotePath=false diff --cached --name-only`. If files are already staged that the user did not ask to commit, STOP and ask — commit them together, keep them staged for later, or split. Never silently fold pre-staged files into this commit.
- If the user names exact files, stage those only with `git add -- <paths>` (the `--` keeps a path that starts with `-` from being parsed as an option).
- If the user says "commit my changes", start from `git -c core.quotePath=false status --short`. **Use `-c core.quotePath=false` on any git command whose paths you show the user or pass back to git**: the default escapes non-ASCII paths to `"\346\272\220…"`, unreadable and not a path `git add --` accepts.
- Count changed paths. **If > 8 files, list the full file set and confirm before staging** — unless the user already authorized that exact set (named the files, said "commit all of these", or confirmed it earlier in this session). The prompt resolves scope ambiguity; it is not a second ask for a question already answered.
- If `<= 8` files, read the diffs and split by logical intent. If one file mixes intents, use `git add -p`.
- Stage task-related untracked files only when they clearly belong to the same change.
- If unstaged or untracked changes remain after staging, the quality gate MUST run through the isolation wrapper (§4) so it sees exactly the staged snapshot and your changes are restored on **every** exit path.
- Verify staging with `git diff --cached --stat`. If nothing is staged, stop.
- Run `git diff --cached --submodule=short`. If a submodule pointer changed, confirm it is intentional.
### 3. Secret/sensitive-content gate
```bash
bash "<path-to-skill>/scripts/secret-scan.sh"
```
Uses the repo's `gitleaks` when installed, and always runs a built-in regex fallback over **added** staged lines only — removing a secret is never blocked. Exit code: **0 = the scan completed** (clean, or findings printed for triage — "no secret" is never a gate failure); **2 = it could not complete**, and every stage that failed names itself. Treat 2 as a gate failure, never as clean. Output lines:
- `SECRET_CANDIDATE: <file>:<line>: <redacted>` — real source line numbers, cut at the match so only its first 4 chars survive.
- `CONTEXT: <file>:<line>: <text>` — 2 lines around each candidate, cut by the same rule.
A line is **reported** when it holds a known credential shape, or a sensitive key name followed by `:` or `=` (case-insensitive). It is **cut** at the earliest of those *or* any 20+ char token-ish run. **Residual risk: a short literal not attached to a sensitive key still prints** — context is a triage aid, not a sanitiser; do not paste it into a public channel.
- `ALLOWLISTED: …` — matched a glob in the **committed** `.commit-secret-allowlist` (`HEAD` version; a staged-but-uncommitted allowlist is ignored).
- `SENSITIVE_FILE: <path>` — staged filename that is key material by name (`.env`, `id_rsa`, `*.pem`, …).
- `SCANNER_ERROR: …` — gitleaks misconfiguration or crash (script exits 2); the regex fallback still ran but is NOT equivalent coverage — fix the scanner or get explicit user sign-off.
Triage every match. **Path/type never auto-dismisses a match — real keys do get committed into tests and docs. Only a committed allowlist hard-dismisses; everything else lowers confidence and is still surfaced.**
| # | Filter | Effect |
|---|---|---|
| 1 | `ALLOWLISTED:` line (committed `.commit-secret-allowlist` glob) | Hard-dismiss — but confirm the entry's scope/provenance; treat an unfamiliar allowlist as untrusted |
| 2 | Test/fixture path (`/test(s)/`, `/__tests__/`, `/spec/`, `/mock(s)/`, `/fixture(s)/`, `/example(s)/`, `/testdata/`, `/snapshot(s)/`, or a common test suffix) | Downgrade to low confidence — still report |
| 3 | Documentation file (`.md`, `.rst`, `.adoc`, `.mdx`) | Downgrade — still report |
| 4 | Comment line (after stripping `+`/whitespace: `//`, `#` not `#!/`, `--`, `/*`, `*`, `%`, `;;`, `REM `) | Downgrade — still report |
High-confidence matches are blockers; downgraded matches need a human decision (never silently dropped). Binary files and high-entropy strings are out of scope for the regex fallback — rely on `gitleaks`/`detect-secrets` for those.
### 4. Quality gate
Detect ecosystems — extensions **and** dependency-manifest markers, so staging only `go.mod` or `package.json` still selects the right gate:
```bash
bash "<path-to-skill>/scripts/detect-ecosystems.sh" # one per line, most staged files first
```
Deletions count. **Empty output = none; exit 2 = unknown** — never treat exit 2 as "nothing to gate".
- `go` → load `references/quality-gate-go.md`; `node` → `references/quality-gate-node.md`; `python` → `references/quality-gate-python.md`; `java` → `references/quality-gate-java.md`; `rust` → `references/quality-gate-rust.md`
- **Run the gate for EVERY detected ecosystem**, largest first — a 5-Go-file + 1-TS-file stage runs both the Go and the Node gates (scope each gate to its own files where the reference allows). Never skip a minority ecosystem.
- The marker list is not exhaustive. Detector prints nothing but the stage clearly belongs to an ecosystem (an unlisted marker such as an uncommon lockfile) → pick that gate by judgment and say so in the report; otherwise try `make test` / `make check`, else report `quality gate: not detected`.
- Makefile wrappers take precedence over raw ecosystem commands.
- If a check fails, stop and report it. The user may explicitly skip; report `quality gate: skipped by user`.
- **Timeout (enforced, not advisory)**: default **120 seconds** per gate command (each invocation bounds one command). If the repo wrapper or environment sets `COMMIT_TEST_TIMEOUT`, `QUALITY_GATE_TIMEOUT_SECONDS`, or `SKILL_QUALITY_GATE_TIMEOUT_SECONDS`, use it and report the chosen timeout before running long tests. **Exit `124` = timed out** (one code on every host); **exit 2 = the timeout could not be enforced**, so no gate ran. Both block the commit.
```bash
bash "<path-to-skill>/scripts/run-gate.sh" [-t <repo-wrapper-seconds>] <gate command>
```
- **Isolation (guaranteed restore)**: when unstaged or untracked changes are present, wrap the enforcer in the stash guard so those changes are stashed and restored exactly once on **every** exit path — gate pass, gate failure, timeout, or interrupt (SIGINT/SIGTERM exit 130/143 after restore, never 0):
```bash
bash "<path-to-skill>/scripts/stash-guard.sh" bash "<path-to-skill>/scripts/run-gate.sh" make test
```
Its exit code is the gate's. Every non-zero path **preserves the stash** and prints recovery instructions (`git stash apply --index <OID>`) — data is never lost, but a restore conflict can leave a **partial** worktree, so follow the instructions rather than assume a clean tree. It fails closed on both isolation edges: unstashable state (dirty submodule content) → exit 2 **without running the gate**; tracked-file drift while the gate ran → keeps the stash, refuses the reset.
### 5. Compose commit message
**First discover the repo's own convention — it overrides the defaults below:**
- commitlint config (`.commitlintrc*`, `commitlint.config.*`, or a `commitlint` key in `package.json`) → follow its `type-enum`, case, and length rules.
- `.gitmessage` template (`git config --get commit.template`), or commit rules in `CONTRIBUTING`/`AGENTS.md` → follow them.
- If a convention is found, adopt its type set, language, and subject length in place of the Angular / English / 50-char defaults. If none is found, use the defaults.
- Carry the discovered subject-length limit into the §6 guard as `SUBJECT_MAX` (default 50) so the executable check enforces the repo's actual limit, not a hardcoded 50.
Scope discovery — these rules are mechanical, so run them rather than re-deriving them:
```bash
bash "<path-to-skill>/scripts/resolve-scope.sh" # SCOPE: <name>|(none) + SCOPE_SOURCE:
```
- `canonical` — a scope used **>= 3 times in the last 50 commits** that matches a staged path and is **not contradicted by a second canonical scope**. A stage spanning two known scopes is a mixed commit: scope is omitted, never resolved to whichever is more frequent. A staged path matching no canonical scope (`CHANGELOG.md`) does not block.
- `bootstrap` — **fewer than 10 conventional commits total**, so the convention is not established; the scope comes from the staged directories with generic containers (`src`, `pkg`, `internal`, `services`, `tests`, …) stripped, and only when one stable directory remains.
- `omitted` — use `<type>: <subject>`. Exit 2 = the repo could not be read: omit the scope and say so.
- Never invent a scope from filenames or issue text. If the answer looks wrong for this change, omit — do not substitute your own.
- Format is `<type>(<scope>): <subject>` or `<type>: <subject>`; for a breaking change use `<type>!:` and/or a `BREAKING CHANGE: <what>` footer.
- Subject must be imperative, no trailing period, and <= 50 chars (or the repo's discovered limit); body explains **why** and wraps at 72 chars.
- Add required trailers when repo policy or the user asks: `Signed-off-by:` (`git commit -s`), `Co-authored-by:`, and issue refs (`Closes #123`).
### 6. Commit
Single-line commit:
```bash
SUBJECT='<type>(<scope>): <subject>'
SUBJECT_MAX=${SUBJECT_MAX:-50} # 50 default; set from the repo convention (§5)
[ ${#SUBJECT} -le "$SUBJECT_MAX" ] || { echo "subject too long (${#SUBJECT}/$SUBJECT_MAX)"; exit 1; }
case "$SUBJECT" in *.) echo "subject must not end with ."; exit 1 ;; esac
git commit -m "$SUBJECT"
```
Multi-line commit — the heredoc is the **single source**; the guard checks the exact first line that will be committed (no separate SUBJECT variable to drift):
```bash
SUBJECT_MAX=${SUBJECT_MAX:-50} # 50 default; set from the repo convention (§5)
MSG=$(cat <<'EOF'
<type>(<scope>): <subject>
<body — explain why, wrap at 72 chars>
<footer>
EOF
)
SUBJECT=${MSG%%$'\n'*}
[ ${#SUBJECT} -le "$SUBJECT_MAX" ] || { echo "subject too long (${#SUBJECT}/$SUBJECT_MAX)"; exit 1; }
case "$SUBJECT" in *.) echo "subject must not end with ."; exit 1 ;; esac
printf '%s\n' "$MSG" | git commit -F -
```
Prefer `git commit -F -` for any multi-paragraph body. Multiple `-m` flags *do* work — each becomes its own paragraph — but give no control over 72-char wrapping within a paragraph and are quoting-fragile for footers, so `-F -` is the reliable choice.
Hook awareness:
- If commitlint, pre-commit, husky, or lefthook rejects the commit, read the error and adapt the message.
- Never use `--no-verify` unless explicitly requested.
- Report the hook name and the adjustment made.
### 7. Post-commit report
```bash
git rev-parse --short HEAD
git show -s --format='%h %s' HEAD # hash + subject
git -c core.quotePath=false show --name-status --format= HEAD # changed files (NOT --no-patch: it disables --name-status)
```
Report the hash, final subject, changed files summary, and quality gate status.
## Message Examples
- `fix(account): guard nil balance map before merge`
- `refactor(service): simplify aggregation flow`
- `test: add case for tail-mapping loss`
```text
fix(auth): prevent token refresh race condition
Two concurrent requests could both trigger a refresh, causing one to
use an invalidated token. Add mutex to serialize refresh calls.
Closes #245
```
## Edge Cases
- `--allow-empty`: only if the user explicitly requests it. Note it in the post-commit report.
- If the file at `git rev-parse --git-path SQUASH_MSG` exists (worktree-safe — never read `.git/` directly), warn that this may be a squash-merge residual state.
- Submodule pointer changes are never auto-assumed safe.
## Failure Handling
- No staged changes → stop and report.
- Safety gate failure → stop and report the exact blocker.
- Commit failure → report the git error and leave staging untouched.
Files in this skill
SKILL.md15.3 KB
references/design-evidence.md8.3 KB
references/quality-gate-go.md1.5 KB
references/quality-gate-java.md2.7 KB
references/quality-gate-node.md2.1 KB
references/quality-gate-python.md1.7 KB
references/quality-gate-rust.md1.6 KB
scripts/detect-ecosystems.sh3.9 KB
scripts/eval/README.md5.5 KB
scripts/eval/run_eval.sh8.2 KB
scripts/eval/scenarios/01_ambiguous_bulk.sh527 B
scripts/eval/scenarios/02_explicit_bulk_authorization.sh660 B
scripts/eval/scenarios/03_mixed_intent.sh731 B
scripts/eval/scenarios/04_pre_staged_surprise.sh247 B
scripts/eval/scenarios/05_scanner_cannot_complete.sh600 B
scripts/eval/scenarios/06_downgraded_finding_in_test.sh447 B
scripts/eval/scenarios/07_uncommitted_allowlist.sh677 B