Skip to content
Back to skills

Git Commit

BSecurity

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.

  • 30 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 19, 2026
ai-agentspythonrustgojavabashangularnodegitdocumentation

Works with

  • cli

Security analysis

B75/100
  • criticalReads or references SSH private keys

Pro scans all 20 files and shows the line behind each finding

Scanned September 19, 2026

npx -y skills add johnqtcg/awesome-skills --skill git-commit --agent claude-code

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.

Security grade badge for Git Commit
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/johnqtcg-git-commit/badge)](https://www.skillsdirectory.com/skills/johnqtcg-git-commit)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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
  • scripts/eval/scenarios/08_two_canonical_scopes.sh1.1 KB
  • scripts/eval/scenarios/09_hook_rejects.sh755 B
  • scripts/eval/scenarios/10_subject_length_limit.sh878 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.