An AI git commit message generator is the smallest possible piece of agentic developer tooling — and one of the highest-leverage. This tutorial walks through a 200-line Node CLI that reads your staged diff, sends it to Claude Haiku 4.5 with a tight system prompt, and produces a Conventional Commits message with accept, edit, or reject UX baked in.
The stakes are higher than they look. Commit messages are eternal— the one piece of engineering work that survives long after the code it describes is refactored, replaced, or deleted. Yet they're the line item engineers rush most. A small, sharp AI tool wired into your pre-commit hook turns the rushed two-second decision into a structured, reviewable artifact at sub-second latency for less than a tenth of a cent per commit.
This guide covers the full build: package layout, prompt design with anti-hallucination rules, large-diff handling and secret redaction, the interactive CLI flow, husky installation, and a GitHub Actions workflow that lints commit history on every PR. Everything is paste-ready, opinionated on defaults, and reversible in under a minute.
- 01Haiku 4.5 is the right model for commit-message work.Sub-second latency, roughly an order of magnitude cheaper than Sonnet on input tokens, and the task is summarization — not multi-step reasoning. Haiku is the calibration sweet spot.
- 02The diff is the prompt — keep it that way.Don't add PR descriptions, issue context, or branch metadata. The diff alone is what the model should reason from. Extra context invites drift and inflates token cost without raising accuracy.
- 03JSON mode prevents free-form chattiness.Force structured output with type, scope, subject, body fields. Parsing is trivial, validation is automatic, and the model can't smuggle in an apology paragraph or a 'Sure, here is your commit' preamble.
- 04Conventional Commits scope must be derived, never invented.The single most important prompt rule: scope tokens MUST appear in the diff's file paths. Without that constraint, Haiku will confidently fabricate scopes like 'auth' or 'core' that don't match your codebase.
- 05Husky plus commit-msg hook integrates without ceremony.One npm script, one .husky/commit-msg file. The tool can be uninstalled in 60 seconds. Reversibility is a feature — your team adopts faster when there's a visible exit.
01 — Why ThisCommit messages are eternal — and almost always rushed.
Every engineer who has bisected a production bug to a 2018 commit titled fix stuff understands the cost of poor commit history. Git history is the only audit trail of intent that survives the code itself — refactors come and go, but the commit log is forever. It powers code archaeology, release notes, security audits, git blame investigations, and the autogenerated changelogs that downstream consumers actually read.
Conventional Commits — the type(scope): subject convention — is the de facto standard for making that history machine-readable. It enables automated semver bumping, changelog generation, release-note scaffolding, and PR-time linting. Adopting it costs almost nothing; the friction is purely in the discipline of writing the message in the right shape every time, especially at 5:47 pm on a Friday when you just want to push and go home.
AI augmentation is the right fix here for three reasons. First, the input — a unified diff — is small and structured, exactly the kind of payload a fast model handles well. Second, the output is constrained: a fixed-grammar string, not free-form prose. Third, the task happens at a moment of high cognitive load (the engineer is context-switching out of the work) where a generated draft is almost always better than a rushed first pass. Accept-edit-reject UX preserves human authority while removing the blank-page penalty.
"The commit message is the single piece of engineering work that survives every refactor — and the line item engineers rush most. AI augmentation here pays compound interest."— Digital Applied engineering principle
02 — ScaffoldAnthropic SDK + Commander, zero ceremony.
The whole tool is one file. Three runtime dependencies, zero build step, single binary entry. The fewer moving parts a developer-tool CLI has, the faster a team adopts it — every extra config file is a line of resistance.
Initialize the package and install the SDK plus two CLI helpers. commander handles argv parsing and prompts handles the interactive accept-edit-reject flow. @anthropic-ai/sdk is the official Anthropic client — typed, streaming-capable, and honest about error shapes.
bin field is what turns your script into a global command. Once the package is installed (locally or globally), running commit-ai from anywhere inside a git repo invokes ./src/index.js. Keep the entry point ESM (`"type": "module"`) and use the shebang #!/usr/bin/env node on the first line so the binary works on Linux and macOS without a launcher.Node 22+
ESM, native fetch, top-level awaitModern Node simplifies the implementation — no node-fetch shim, no Babel, no TypeScript build step for the MVP. Ship plain .js with JSDoc types if you want IDE help without a compiler.
node --versionThree packages
@anthropic-ai/sdk · commander · promptsSDK for the model call, commander for argv (--diff-file, --dry-run, --auto-accept), prompts for the terminal UI. Total install size under 8 MB. No native modules, no postinstall hooks.
pnpm add ...One file
src/index.js · ~210 linesRead staged diff, redact, prompt the model, parse JSON, render the message, run the interactive accept-edit-reject loop, exit with the right code. No internal modules to navigate.
bin: commit-aiThe shape of package.json matters because the bin entry is what makes the CLI feel like a first- class tool rather than a script. Wire it like this:
{
"name": "@you/commit-ai",
"version": "0.1.0",
"type": "module",
"bin": {
"commit-ai": "./src/index.js"
},
"engines": { "node": ">=22" },
"dependencies": {
"@anthropic-ai/sdk": "^0.40.0",
"commander": "^14.0.0",
"prompts": "^2.4.2"
}
}Run pnpm link --global once during development and commit-ai is on your PATH. For team adoption, publish to your internal registry or vendor the tool into a monorepo workspace and reference the binary via pnpm exec commit-ai.
03 — Prompt DesignThe system prompt that won't fabricate scopes.
The model receives two things and only two things: a static system prompt that defines the output contract, and the user-message payload containing the unified diff from git diff --staged. Nothing else. No PR title, no issue number, no branch name, no readme excerpt. Adding context here sounds helpful and is actually corrosive — the model starts blending descriptions instead of summarizing the actual change.
Force structured JSON output. The output schema has four fields: type (an enum from the Conventional Commits spec), scope (optional, a single kebab-case token), subject (≤72 chars, imperative mood, no trailing period), and body(optional, wrapped at 72 chars, used for the "why"). JSON mode means parsing reduces to JSON.parse and validation is a 20-line Zod schema — no regex acrobatics.
feat(auth): ... for a diff that touched lib/payments.ts. With it, scope hallucination drops to effectively zero across hundreds of test commits.A working system prompt — copy it, tune the type enum to your team's conventions, ship it:
You write Conventional Commits messages from staged git diffs.
OUTPUT: JSON only, matching the schema:
{
"type": "feat" | "fix" | "docs" | "style" | "refactor" |
"perf" | "test" | "build" | "ci" | "chore" | "revert",
"scope": string | null,
"subject": string,
"body": string | null
}
RULES:
- subject: imperative mood, lowercase first letter, no trailing period, <= 72 chars.
- body: optional, wrap lines at 72 chars, explain the WHY not the WHAT.
- scope: derive from the diff's file paths only. If you cannot derive scope
from the file paths, set it to null. NEVER invent a scope token.
- type: pick the single best match. Multi-purpose diffs default to "refactor".
- No preambles, no apologies, no commentary outside the JSON.The Anthropic SDK call is correspondingly small. Set model: "claude-haiku-4-5", max_tokens: 512 (commit messages never exceed it), temperature 0.2 for stability, and pass the system prompt via the top-level system field rather than as a user message. Structured output is enforced via the response_formatparameter when the SDK exposes it; if not, append "Return JSON only." to the user message and validate post-parse.
04 — Diff HandlingTruncation, redaction, and large-PR fallback.
Most staged diffs are small — a hundred lines, two files. A working CLI must also handle the diff that arrives with thirty migrations, a regenerated lockfile, and a vendored SDK update. Three concerns: token budget, secret leakage, and how to summarize the diff that won't fit even in Haiku's generous context window.
Redaction comes first. Run the raw diff through a regex sweep for common secret patterns before sending it to any model: AWS keys (AKIA[0-9A-Z]{16}), Stripe live keys (sk_live_), Slack tokens, GitHub PATs, hex-encoded JWT bodies. Replace each match with <REDACTED:aws-access-key> markers — the model still understands a key was changed without seeing the value. This is non-negotiable in any team where the tool runs against production code.
Send the full diff
If the redacted diff fits under ~30k tokens (the practical sweet spot for sub-second latency and clean attention), send it as-is. Haiku 4.5 handles this easily. Most commits — even feature work — land here. Roughly 92% of real-world commits in our test corpus fit this path.
Hot path · ~800 msPer-file summarization
Above the threshold, split the diff by file boundary, summarize each file in a single sub-call (3-5 bullet points of intent), then run a final pass that takes the summaries plus the file-path list and produces the commit message. Higher latency (~3-5 seconds) and 2-3× cost, but stays under any context limit.
Fallback · ~3-5 sRefuse and prompt
If even per-file summarization exceeds budget (a 50-file refactor with regenerated bundles), the right behavior is to refuse politely: print 'diff exceeds budget — split into smaller commits or use --message manually' and exit non-zero. The CLI should never silently produce a misleading message from a partial view.
Safety railToken estimation is approximate — a 4-chars-per-token rule of thumb is fine for budgeting. Wrap the model call in a token-counter helper that decides between the three strategies above. The decision logic is roughly twenty lines; don't overengineer it.
One additional rail worth wiring in: skip the diff entirely for commits that are exclusively binary or lockfile changes. If git diff --staged --name-only returns only *.lock, *.png, package-lock.json, generate the message deterministically — chore: update lockfile or chore: add assets — and skip the API call. Saves tokens and avoids the model staring at a 40k-line lockfile diff and inventing meaning.
"Redaction is not optional. Any tool that ships a developer's staged diff to a third-party model must scrub secrets first, period."— Security review checklist, internal
05 — CLI UXAccept, edit, or reject.
The interactive loop is where a useful tool becomes a tool the team actually keeps installed. Three keystrokes, no mouse, sub-second response. Anything slower or more clicky than that and engineers will bypass it on every push.
Stream the model output as it generates so the user sees progress — even though the message itself is small, the perceptual latency is what matters. Render the parsed Conventional Commits line in uppercase color (orange for feat, blue for fix, dim for chore), then drop into a single prompts.select with three options pre-bound to hotkeys a, e, r.
Accept as-is
exit 0 · writes messageWrites the proposed message to the COMMIT_EDITMSG file (when invoked from a husky hook) or echoes it to stdout (when invoked manually). The git pipeline picks up the message and the commit completes immediately.
Default · ENTEREdit in $EDITOR
exit 0 · writes edited messageDrops the proposed message into your $EDITOR (vim, nvim, code --wait, whatever git is configured to use). Save and quit and the edited version is written. This is the fallback when 95% of the message is right but the subject needs a noun swap.
Power userReject
exit 1 · aborts commitThe CLI exits non-zero. Husky sees the failure and aborts the commit. The engineer can then run git commit -m '...' manually with whatever message they want, or rerun commit-ai to get a fresh proposal.
Safety valveExit codes matter because husky's commit-msg hook respects them. Exit 0 and the commit proceeds; exit non-zero and git aborts. The CLI must also write the generated message to the file path that git passes as argv[2] when invoked from the hook context. A three-line conditional handles both modes:
const msgFile = process.argv[2]; // set by husky/git
if (msgFile && fs.existsSync(msgFile)) {
fs.writeFileSync(msgFile, formatted);
} else {
process.stdout.write(formatted + '\n');
}Two more UX rails worth adding before you call it done. First, --auto-acceptskips the interactive prompt entirely (useful for scripted commits and CI flows where there's no terminal). Second, --dry-run generates and prints the message but never writes or commits — perfect for benchmarking against your real history before you let the tool touch a real commit.
06 — InstallHusky pre-commit hook or manual commit-ai.
Two installation patterns. Pick one based on how aggressive your team wants to be about adoption. Manual use leaves the tool as an opt-in command an engineer types when they want a draft. Husky use wires it into every commit, with reject-to-bail as the safety valve.
Pattern A — manual
Install globally and call it when you want help. No git hooks touched, no team buy-in required, no surprises.
pnpm add -g @you/commit-ai
# stage changes, then:
git add .
commit-ai # generates, prompts, writes the messagePattern B — husky commit-msg hook
The hook intercepts every commit and offers a generated message. If the user typed a message via git commit -m, the hook sees it and exits 0 (don't overwrite an intentional message). If no message was supplied (e.g. plain git commit), the hook generates one.
# one-time setup in the repo
pnpm add -D husky
pnpm dlx husky init
# .husky/commit-msg (one file, three lines)
#!/usr/bin/env sh
[ -s "$1" ] && head -c1 "$1" | grep -qv '^#' && exit 0
commit-ai "$1"Configuration file
A small .commit-ai.json at the repo root lets each team tune defaults without forking the tool:
{
"model": "claude-haiku-4-5",
"style": "conventional-commits",
"branchScope": {
"feat/payments-*": "payments",
"fix/auth-*": "auth"
},
"redactionPatterns": ["custom-secret-regex-here"],
"maxTokens": 512
}The branchScope map is a small quality-of-life win: when the engineer is on feat/payments-checkout, the tool injects paymentsas a default scope hint into the user message, biasing the model toward the correct scope without forcing it. The anti-hallucination rule in the system prompt still applies — if the diff doesn't actually touch the payments/ path, scope falls back to null.
Uninstall
Reversibility matters. The whole tool comes out cleanly in three commands — keep this in your README so adopters trust the off- ramp:
rm .husky/commit-msg
pnpm remove @you/commit-ai
rm .commit-ai.json07 — CI IntegrationGitHub Actions: commit message lint on every PR.
Local hooks are honor-system — engineers can always git commit --no-verify. CI is the enforcement layer. Reuse the same CLI in a GitHub Actions workflow to lint the commit history of every PR, with three modes ranging from advisory to prescriptive.
Lint only
PR check · pass/failThe cheapest mode. Walk the PR's commits, parse each subject line, fail the check if any commit doesn't match the Conventional Commits grammar. No model call needed for lint — it's a regex. Set this as a required check and the team will adapt within a sprint.
Required checkAI-suggest
PR bot comment · advisoryFor each commit that fails the lint, generate a proposed message via commit-ai --dry-run on the commit's diff and post it as a sticky PR comment. The author can copy-paste or rebase. No automation acts on their behalf — just a high-quality nudge.
AdvisoryAuto-fix on protected PRs
rebase · force-push · gatedThe aggressive mode. For an opt-in set of repos or PR labels, the workflow rewrites failing commit messages via interactive rebase and force-pushes the corrected history. Gate behind a label like 'commit-ai:auto-fix' so it's never a surprise.
Opt-in onlyThe workflow shape is straightforward — checkout the PR branch with fetch-depth: 0, walk git log ${{ github.event.pull_request.base.sha }}..HEAD, and run the validation. The Anthropic API key lives in organization secrets and is passed via env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} only for the suggest and auto-fix modes (lint mode needs no API key).
# .github/workflows/commit-lint.yml
name: Commit lint
on: pull_request
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with: { fetch-depth: 0 }
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v5
with: { node-version: 22 }
- run: pnpm install --frozen-lockfile
- name: Validate commit history
run: pnpm exec commit-ai lint \
--base ${{ github.event.pull_request.base.sha }}
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}The comment-back bot pattern uses the GitHub REST API via @octokit/rest or the bundled actions/github-script. Find an existing sticky comment with a marker like <!-- commit-ai -->; update it in place rather than spamming a fresh comment on every push.
Required-check effect
Once Conventional Commits is a required PR check, adoption hits 100% within a sprint. The cost is one team member explaining the type enum once in standup; the payoff is searchable history forever.
Pattern-A teamsPer-engineer / month
Haiku 4.5 input + output at ~20 commits per engineer per day, with PR-time lint added: typically under $1 per active engineer per month. Less than a single cup of coffee for the entire team in many cases.
Anthropic list pricingJSON-mode parser
When the model returns structured JSON with a validated schema, parsing is exact. Conventional Commits compliance is 100% by construction — the failure mode is 'tool errored' (caught, reverts to manual), never 'tool produced malformed output that got merged'.
Validated by ZodBetter commit messages cost less than a cup of coffee per engineer per month.
A ~210-line Node CLI built around Claude Haiku 4.5, a tight JSON-mode system prompt with the anti-hallucination scope rule, secret redaction and large-diff fallback, a three-key accept-edit-reject UX, a husky commit-msg hook for local enforcement, and a GitHub Actions workflow for PR-time lint. Five sections of work, one weekend of build, forever of cleaner history.
What it unlocks is bigger than the surface feature. A searchable, Conventional-Commits-compliant git history is the input to automated changelogs, semantic-versioning bumps, release-note scaffolding, faster code review (because reviewers can scan history rather than re-deriving intent), and per-scope code- ownership analytics. Every downstream tool that consumes commit messages gets sharper the moment the upstream message is structured.
The next step is yours. Fork the example, tune the type enum to your team's conventions, swap the model if you prefer a different provider (the pattern is provider-agnostic), and ship it to one repo this week. Once it works there, fan it out — the install is reversible in sixty seconds, so the bar for adoption is low. If your team wants help designing a more ambitious developer- workflow tool — PR description generators, AI code-review bots, or test scaffolders calibrated to your style guide — our AI transformation engagements are built around exactly this kind of small, sharp tool that compounds.