# Commit

> Use when about to make git commits, when tempted to bundle unrelated changes behind a single message, or when commit history needs to support git bisect, revert, and cherry-pick workflows. Also use when reviewing a branch before creating a PR. If you are writing a plan, specify that the executor must load this skill before start working.

- Skill: `arcadi4/commit` (Agent Skill)
- Install (CLI): `npx skillmds@latest add arcadi4/commit`
- Raw SKILL.md: https://api.skillmd.com/api/skills/arcadi4/commit/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: CC-BY-SA-4.0
- Author: Arcadi4 (https://skillmd.com/u/arcadi4)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/arcadi4/commit

---


# Make Atomic Git Commits

## Core Principle

Each commit = one logical change, one intention. If you cannot describe the commit in one sentence without "and", split it.

- "Fix null pointer dereference in checkout flow" — passes
- "Fix cart quantity bug and update user model and add tests" — fails ("and") → three commits

Each commit must be **independent** (understandable alone), **buildable** (compiles, tests pass), and **complete** (no partial work).

Atomic ≠ tiny. A refactor touching 20 files can be one atomic commit if it does one thing. The test: can it be reverted independently?

### Red Flags — Split Now

- Subject contains "and" or two commit types apply (e.g. feat + fix)
- You're tempted to write "misc", "various", "cleanup", or "updates"
- The body describes unrelated effects
- Change A can be reverted independently from change B

### Revert-Boundary Gate

Before committing a large staged diff, list the independent revert boundaries in one-line phrases. If more than one phrase is required, split the commit — even if the changes are coupled or all tests only pass after the full refactor. "They are coupled" is not an atomicity argument; use transitional buildable commits or explain why no independent revert boundary exists.

## Common Rationalizations

| Rationalization | Reality |
|---|---|
| "Only a few lines" | Size is irrelevant. Different intentions = different commits. |
| "Fixed it while working on X" | Work context ≠ commit boundary. Different reason-to-exist = different commit. |
| "Both are `chore:` type" | Same type ≠ same intention. `chore: update prettier` ≠ `chore: add dependency`. |
| "Splitting is pedantic" | Non-atomic commits break bisect, complicate reverts, confuse reviewers. |
| "Test file is too small" | No minimum line count. Tests are `test:` type — always separate. |
| "They are deeply coupled" | Coupling explains staging order, not commit boundaries. Separate intentions with separate revert boundaries = separate commits. Dependency between commits is normal. |
| "I'll just count the characters" | Manual counting fails. Always. Use the length gate script — never eyeball it. |

## Message Rules

### Discover Convention First

Run `git log --oneline -20` and match the repo's existing format exactly:
- **Prefix format**: Conventional types (`feat:`, `fix:`)? Module names (`auth:`, `ui:`)? None?
- **Capitalization/punctuation**: Sentence case or lowercase? Trailing period?
- **Scopes**: If parenthesized scopes exist (`feat(auth):`), use existing ones. Never invent scopes. Never use commit types as scopes (`fix(perf):` is wrong when `perf` is intended as a type).

No commit history? Pick one convention and apply consistently. Conventional Commits (`type(scope): summary`) or plain prefix (`area: summary`).

### Subject Line

- Max 72 characters. **Always commit through the length gate script** — never call `git commit` directly. Write the matching script below to an executable file and run it; it enforces the limit and is the only sanctioned commit command. Never count characters yourself — it does not work.

  ```bash
  #!/usr/bin/env bash
  # commit-length-gate.sh "subject" ["body paragraph" ...] — each arg after the subject becomes a -m body paragraph.
  set -euo pipefail
  [[ $# -ge 1 ]] || { echo "usage: $0 \"subject\" [\"body\"...]" >&2; exit 1; }
  s=$1; shift
  ((${#s} <= 72)) || { echo "Subject too long: ${#s} chars (max 72): $s" >&2; exit 1; }
  a=(-m "$s"); for b in "$@"; do a+=(-m "$b"); done
  exec git commit "${a[@]}"
  ```

  ```powershell
  # commit-length-gate.ps1 "subject" ["body paragraph" ...] — each arg after the subject becomes a -m body paragraph.
  param([Parameter(Mandatory, Position = 0)][string]$Subject, [Parameter(ValueFromRemainingArguments)][string[]]$Body)
  if ($Subject.Length -gt 72) { throw "Subject too long: $($Subject.Length) chars (max 72)`n$Subject" }
  $c = @('-m', $Subject) + @(foreach ($p in $Body) { '-m', $p })
  git commit @c
  ```
- Start with an imperative verb after any prefix: `add`, `fix`, `extract`, `remove`. Not `feature`, `make`, `let's`.
- Formal technical language. No conversational constructions ("make X Y, not Z", "do X instead of Y").

### Body

**Prefer no body.** Make the subject self-explanatory. Only add a body when the subject genuinely cannot carry the explanation.

Body explains **WHY**, not WHAT — the diff shows what changed. Write prose, never bullet points or checklists. A one-line why-statement is almost always sufficient; if you need more than 3 `-m` paragraphs, your commit isn't atomic.

| Body needed | No body needed |
|---|---|
| Bug fix with non-obvious cause | Typo, formatting, dead code removal |
| Refactor that changes structure (why?) | Self-explanatory rename |
| Performance improvement (bottleneck? gain?) | Single-line config or dependency bump |
| Breaking change or migration step | Subject fully describes the change |

### Forbidden in Messages

- **File lists**: Git tracks files. "3 files changed, 5 tests added" is noise.
- **Plan-internal terminology**: No task numbers ("Task 3"), wave/phase names, step labels. Commits must stand alone in `git log`. Reference ticket IDs from issue trackers, not planning artifacts.

## Commit Types

| Type | Description |
|------|-------------|
| feat | New user-facing feature |
| fix | User-facing bug fix |
| docs | Documentation or comments only |
| style | Formatting, whitespace, linting — no logic change (never CSS/design) |
| refactor | Code restructuring, no behavior change |
| perf | Performance improvement |
| test | Test-related changes (fixing tests = `test:`, not `fix:`) |
| chore | Build process, dependencies, tooling |
| ci | CI configuration and scripts |

When a change is both restructuring and performance, choose the emphasis.

## Common Mistakes

| Mistake | Fix |
|---------|-----|
| "Misc cleanup" commits | Each cleanup item = its own commit |
| Generated files mixed with hand-written code | Generated files go in a separate commit |
| Rename combined with semantic edits | Rename first (using `git mv`), then edit in next commit |
| Rename split across commits (delete old + add new) | Stage both in one commit — git needs old and new together for rename detection |
| One commit per file | Commit per logical change, not per file |
| Sub-agent output left uncommitted | Incorporate sub-agent changes and commit appropriately |
| Calling `git commit` directly | Always use the length gate script. Raw `git commit` bypasses the 72-char enforcement. |

## Emergency Exception

During active P1 outages, land the minimal fix first. Clean up history in a follow-up commit.

