On heavy coding days, you can watch your git log slowly fill up with "update", "fix", and "wip" — nothing that tells you, a month later, what each commit was actually for. Running several projects solo, I hit this wall often enough that history archaeology started eating real time.
Claude Code lives right next to git diff, so writing commit messages should be one of its most natural tasks. But if you just ask it to "commit nicely", you'll get a polished subject line with an empty body, or a new format every time, depending on the mood of the prompt. The trick is to give it just enough structure to be predictable without turning the whole thing into a brittle template.
This post shares a small, concrete setup for getting Conventional Commits out of Claude Code reliably, plus a few tricks for cutting down on miscategorisation (like tagging a bug fix as a feature) that you'll otherwise hit again and again.
Why hand Conventional Commits to Claude Code at all
Conventional Commits prefix each message with the change type — feat:, fix:, refactor:, and so on. When you come back to the log weeks later, you can scan it in seconds instead of re-reading every diff. That payoff compounds: release notes write themselves, CI tooling can filter by type, and git log --oneline becomes legible again even on a chatty branch.
You could of course write these by hand. But picking a type and scope every single commit gets surprisingly tiring, and tired developers produce the "update"-flavoured history mentioned above. It's the kind of repetitive judgement call Claude Code is genuinely good at offloading — low creativity, high consistency. The rest of this post assumes you want that tradeoff.
If you want the bigger picture first — how commits fit into a broader automated Git flow — the Claude Code Git workflow automation guide is a good companion piece.
Start small — one prompt, one result
Before you reach for hooks or slash commands, I'd suggest testing the behaviour with a single prompt. Paste this straight into a Claude Code chat and see what comes back.
Please write a commit message using these steps:
1. Run `git diff --staged` and read the staged changes.
2. Summarise the changes in one or two sentences.
3. Output using the template below (the body can be omitted if unneeded).
<type>(<scope>): <subject>
<body>
<footer>
Choose type from: feat / fix / refactor / perf / docs / test / chore / build / ci.
Add a one-line note explaining why you picked that type.The important line here is "add a one-line note explaining why you picked that type". Without it, you'll get clean-looking messages that quietly conflate feat and fix. With it, you can immediately reply "that's a refactor, rewrite it" and finish the interaction in a single round trip.
It's worth running this on three or four real commits before you invest in automation. Watch for patterns in where the model disagrees with you — those are the exact cases your later rubric needs to cover.
Grow it into a /commit slash command
Once you're happy with the behaviour, pasting the prompt every time gets old. Promote it to a slash command by dropping this file under .claude/commands/ in your project.
---
description: "Generate a commit message in Conventional Commits format"
allowed-tools: Bash(git diff:*), Bash(git status:*)
---
Run `git diff --staged` and propose a commit message in Conventional Commits format.
Follow these rules:
- Pick type from feat / fix / refactor / perf / docs / test / chore / build / ci.
- Keep subject under 50 characters and use imperative mood.
- Keep body to one paragraph, ordered as "what" then "why".
- When it's unclear whether something is a new feature or a behaviour change, pick feat (not fix) and explain why in the body.
At the end, also print a single line in `git commit -m "..."` form so it's easy to paste.Restricting allowed-tools to git diff and git status keeps unrelated commands from sneaking in, which makes the behaviour noticeably more consistent. It also prevents a common failure mode where the model, mid-generation, decides it "needs" to look at the whole file tree and silently expands the scope of the task.
If you want to organise several commands like this, the Claude Code custom slash commands guide covers the design side in more detail, including how to share commands across a team via a committed .claude/commands/ directory.
Logging the staged diff with a PostToolUse hook
If you want to go one step further, you can have a hook capture a summary of each staging event. The pattern is: watch for git add via PostToolUse and append a tiny stats block to a session log.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash -c 'echo \"$(date -Iseconds)\" >> .claude/last-stage.log && git diff --staged --stat >> .claude/last-stage.log'"
}
]
}
]
}
}The point is to give you something to glance at just before you commit: "ah yes, these are the files I staged in this session". It also turns out to be useful when you accidentally work for an hour without committing — the log shows exactly when each staging step happened, so you can split the work into two or three sensible commits afterwards.
If you want the whole map of hook events first, the Claude Code hooks automation guide is the best place to start.
One note of caution — avoid designs where the hook itself triggers Claude to write a message. Keeping the hook to logging only, and generating the message when the user explicitly runs /commit, makes accidental commits much less likely. Automation that makes its own decisions about what to commit is exactly the kind of thing you'll regret at 11 PM on a Friday.
Reducing chore / refactor / fix mistakes
The stickiest part of running Conventional Commits is choosing a type, and Claude Code is no exception. Left unguided, you'll typically see these patterns:
- Bug fix labelled as feat: It reads the diff and interprets the fix as "adding" the missing behaviour.
- Refactor labelled as fix: Nothing about user-visible behaviour changed, but the diff looks like it "corrected something broken".
- Config change labelled as chore: Dependency bumps get folded under chore, burying build and ci changes that should be more visible in the log.
The fix is to put a short decision rubric in your project's CLAUDE.md. Here's the version I use across my four sites:
## Commit message policy
- User-visible new/changed behaviour: feat
- Returning broken behaviour back to the intended state: fix
- Internal structure change with no behaviour change: refactor
- Dependency updates: build when the package name is known, otherwise chore
- Changes only under `.github/workflows/`: ci
- When in doubt, prefer feat or refactor — do not use fix"When in doubt, do not use fix" is the single most effective line here. The reason is practical: fix carries extra weight in downstream tooling like CHANGELOG generators, so misusing it creates a log full of fixes for things that were never broken. If you care about clean release notes, this one rule pays for itself within a release or two.
A second rule I've added over time is about scope. If the model tries to invent a new scope on every commit, you end up with 40 scopes and no consistency. I usually tell it to pick scopes from an explicit list that matches the top-level directories of the repo — api, web, docs, infra — and only invent a new one if nothing fits. That small constraint keeps the log browsable.
For broader CLAUDE.md design patterns, the CLAUDE.md and memory management guide covers several layouts worth borrowing.
A real before/after on one of my own commits
To make this concrete, here's a commit I made last week, the way it would have come out if I'd just typed something off the top of my head, and the way /commit produced it after reading the diff.
Before (hand-written, tired):
fix article slug
After (generated from the diff):
fix(articles): correct category path for newly-migrated MDX files
Two MDX files were moved from api-sdk/ to claude-code/ but their
internal links still pointed at the old category, causing 404s in
production. Update the links to match the new category.
The hand-written message needs me to open the commit just to remember what was wrong. The generated one tells future-me exactly what incident this resolved. Every time I generate a commit, I recover a bit of time I'd otherwise spend on history archaeology — and over a few hundred commits, that adds up.
A five-minute first step
Trying to install every piece at once is the fastest way to give up on the whole setup. Today, I'd just add .claude/commands/commit.md and try /commit once. It takes five minutes, and the quality of your git log changes immediately.
You can layer in hooks and CLAUDE.md guardrails later, once /commit feels natural. Tools that last are the ones you add in the order you actually need them — not the ones you build up front because the blog post you read happened to list them in a particular order.