SYS/2026.Q1Agentic SEO audits delivered in 72 hoursSee how →
DevelopmentTutorial9 min readPublished May 2, 2026

A 200-line Node CLI that turns your staged diff into a conventional-commits message with Claude Haiku 4.5 — accept, edit, or reject, then commit.

Build an AI Git Commit Message Generator: Full Tutorial

A 200-line Node CLI that turns your staged diff into a conventional-commits message with Claude Haiku 4.5 — accept, edit, or reject, then commit. Wire it into husky for local enforcement and into GitHub Actions for PR-time lint. Paste-ready, reversible in sixty seconds.

DA
Digital Applied Team
Senior engineers · Published May 2, 2026
PublishedMay 2, 2026
Read time9 min
SourcesAnthropic SDK · husky · commitlint
Lines of code
~210
single-file CLI
Latency per call
~800ms
Haiku 4.5, P50
Cost per commit
<$0.001
Anthropic API list
Conv. Commits compliance
100%
JSON-mode parser

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.

Key takeaways
  1. 01
    Haiku 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.
  2. 02
    The 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.
  3. 03
    JSON 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.
  4. 04
    Conventional 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.
  5. 05
    Husky 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.

01Why 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

02ScaffoldAnthropic 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.

package.json shape
The 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.
Runtime
Node 22+
ESM, native fetch, top-level await

Modern 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 --version
Dependencies
Three packages
@anthropic-ai/sdk · commander · prompts

SDK 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 ...
Entry
One file
src/index.js · ~210 lines

Read 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-ai

The 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.

03Prompt 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.

The critical anti-hallucination rule
The single most important sentence in your system prompt: "If you cannot derive scope from the diff's file paths, omit it. Never invent a scope token that does not appear in the paths above." Without this, Haiku will confidently produce 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.

04Diff 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.

Default
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 ms
Large diff
Per-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 s
Mega diff
Refuse 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 rail

Token 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

05CLI 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.

a
Accept as-is
exit 0 · writes message

Writes 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 · ENTER
e
Edit in $EDITOR
exit 0 · writes edited message

Drops 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 user
r
Reject
exit 1 · aborts commit

The 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 valve

Exit 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.

06InstallHusky 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 message

Pattern 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.json

07CI 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
Lint only
PR check · pass/fail

The 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 check
Suggest
AI-suggest
PR bot comment · advisory

For 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.

Advisory
Auto-fix
Auto-fix on protected PRs
rebase · force-push · gated

The 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 only

The 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 $&#123;&#123; github.event.pull_request.base.sha &#125;&#125;
        env:
          ANTHROPIC_API_KEY: $&#123;&#123; secrets.ANTHROPIC_API_KEY &#125;&#125;

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.

Adoption velocity
1sprint
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 teams
Cost at scale
<$1
Per-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 pricing
Compliance
100%
JSON-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 Zod
Conclusion

Better 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.

Augment your engineering workflow

Small, sharp AI tools compound — pick one every quarter that turns minutes into seconds.

Our agentic AI engineering team designs and ships custom dev-workflow tools — commit assistants, PR reviewers, doc generators, test scaffolders — calibrated to your team's standards and shipped in days, not quarters.

Free consultationExpert guidanceTailored solutions
What we build

Developer-workflow engagements

  • Custom commit-message and PR-description assistants
  • AI-powered code review bots calibrated to your style guide
  • Doc-gen tools tied to your typed API surface
  • Test scaffolders tuned to your test framework
  • Pre-commit AI hooks for security and quality
FAQ · Commit assistant

The questions engineers ask before wiring AI into their git flow.

Commit-message generation is summarization plus light classification — it isn't reasoning-heavy. Haiku 4.5 is calibrated for exactly this task class: sub-second latency (P50 around 800 ms), roughly an order of magnitude cheaper than Sonnet on input tokens, and accurate enough that the accept-edit-reject UX rarely lands on 'edit' for well-formed diffs. Sonnet adds cost and latency without raising commit-message accuracy in practice; GPT-class models work equally well if your team has an OpenAI contract, but the structured-output ergonomics in the Anthropic SDK are slightly cleaner for this use case. The tool is provider-agnostic at the prompt level — swapping requires changing the SDK import and the model string.