Conventional Commits
Write commit messages that follow the Conventional Commits spec, prefixed with the ticket number (from the current branch) and the project tag (API / CLIENT / CONSOLE / DB) when those can be determined.
Format
[#<ticket>] [(<PROJECT>)] <type>[(<scope>)][!]: <description>
[optional body]
[optional footer(s)]
Bracketed segments are included only when they apply (see Branch & project context). A fully-tagged example:
#12345 (CLIENT) fix: prevent modal z-index regression on settings page
Types
- feat — a new feature (correlates with MINOR in semver)
- fix — a bug fix (correlates with PATCH in semver)
- docs — documentation only changes
- style — formatting, missing semicolons, etc; no code change
- refactor — code change that neither fixes a bug nor adds a feature
- perf — a code change that improves performance
- test — adding or correcting tests
- build — changes to the build system or external dependencies
- ci — changes to CI configuration files and scripts
- chore — other changes that don't modify src or test files
- revert — reverts a previous commit
Branch & project context
Before composing the message, gather two pieces of context from the repo.
Ticket number (from current branch)
- Run
git rev-parse --abbrev-ref HEADto get the current branch name. - Take the last
/-separated segment of the branch (sofeature/12345-fix_this_bugbecomes12345-fix_this_bug). - If that segment starts with one or more digits followed by
-,_, or end-of-string, those leading digits are the ticket number. - If no leading numeric ID is found, omit
#<ticket>from the message entirely — do not invent one, do not guess one from conversation context, do not reuse a stale number mentioned earlier in the session, and do not prompt the user for it. The branch name is the only source of truth for the ticket.
Examples:
| Branch | Extracted ticket |
|---|---|
feature/12345-fix_this_bug |
12345 |
12345-fix_this_bug |
12345 |
bugfix/9-typo |
9 |
main, release/v2, feature/redesign |
(none — omit #) |
Project tag
Pick ONE of API, CLIENT, CONSOLE, or DB based on what was actually changed in the diff:
- API — backend services, REST / GraphQL endpoints, server-side handlers. Signals: paths like
api/,server/,backend/,services/api/,apps/api/; server framework code (Express, NestJS, FastAPI, Django, Rails, Spring); OpenAPI specs. - CLIENT — user-facing frontend. Signals: paths like
client/,web/,frontend/,apps/web/,apps/client/,ui/;.tsx/.jsx/.vue/.svelte; React / Vue / Angular / Svelte component files; user-facing CSS / Tailwind / design assets. - CONSOLE — .NET console applications (CLIs, worker services, background tools). Signals:
.csprojfiles with<OutputType>Exe</OutputType>orSdk="Microsoft.NET.Sdk"(and NOTMicrosoft.NET.Sdk.Web);Program.cswithout ASP.NET / web-host bootstrap;Microsoft.Extensions.Hostingworker orBackgroundServiceusage; CLI libraries likeSystem.CommandLine,Spectre.Console,CommandLineParser,McMaster.Extensions.CommandLineUtils; project names ending in.Console,.Cli,.Worker, or.Tool; paths likeconsole/,apps/console/,tools/,cli/. - DB — database schema, migrations, seeds, ORM models. Signals: paths like
db/,migrations/,prisma/,schema.prisma, SQL files,alembic/,knex/, model-only changes.
How to decide:
- Run
git diff --cached --name-only(fall back togit diff --name-onlyif nothing is staged). - Match the changed paths against the signals above. If all changed files cluster under one bucket, use that tag.
- If the diff genuinely spans multiple buckets, pick the dominant one (most files or largest change) and mention the secondary in the body.
- If none of the four buckets fit the repo (e.g., a library, a docs-only repo, a meta-tooling project), omit
(PROJECT)— same fallback rule as the missing ticket. - If two buckets are plausibly equal and you can't break the tie from the diff, ask the user which one applies. Do not guess silently.
Rules
- Description must be lowercase, present tense ("add" not "added"), and under 72 characters. The
#12345 (CLIENT)prefix counts toward the 72-char header budget — keep the description tight. - No period at the end of the description.
- Breaking changes get a
!after the type / scope (e.g.,#12345 (API) feat!: drop support for Node 16) and aBREAKING CHANGE:footer. - Inner
type(scope)is optional and usually redundant once(PROJECT)is present. Only use it when it adds real specificity beyond the project tag (e.g.,#12345 (API) fix(auth): ...when "auth" narrows further than "API"). - Body explains the why, not the what. Wrap at 72 chars.
- The description states what changed at the level a reader scanning a changelog needs; the why goes in the body. A description that only restates the type ("fix bug") carries no information — rewrite it.
- ❌
fix: fix bug in login - ✅
fix: reject expired refresh tokens instead of issuing new access tokens
- ❌
- The type must match the diff, not the user's phrasing. The user saying "quick fix" while the staged diff adds a new endpoint means
feat, notfix. Determine the type fromgit diff --stagedoutput you actually read this session — never from the conversation summary or the user's wording alone. - If the staged diff contains two unrelated changes (e.g., an auth bug fix plus a new export page), say so and suggest splitting into two commits — do not paper over it with a vague umbrella description like
chore: various updates.
Workflow
When the user asks for a commit message:
- Get the current branch with
git rev-parse --abbrev-ref HEADand extract the ticket number per the rules above. - Inspect the staged diff:
git diff --cached(orgit diffif nothing is staged). - From the changed file paths, determine the project tag — or decide to omit it.
- Categorize the change into one of the types — based on the diff you just read, not on how the user described the work.
- Write a concise description.
- If the change is non-trivial, add a body explaining the rationale.
- Flag breaking changes explicitly.
- Assemble the header as
[#<ticket>] [(<PROJECT>)] <type>: <description>, including only the segments that apply.
Examples
#12345 (CLIENT) fix: prevent modal z-index regression on settings page
#12345 (API) feat(auth): add OAuth2 PKCE flow for mobile clients
#987 (DB) chore: add index on users.created_at for analytics query
#42 (CONSOLE) feat: add bulk-export action to billing dashboard
(API) refactor: extract token validator into shared middleware
# ^ branch had no ticket — `#` omitted
#7 fix: correct off-by-one in pagination cursor
# ^ repo doesn't fit API / CLIENT / CONSOLE / DB — `(PROJECT)` omitted
fix: prevent race condition in cache invalidation
The previous implementation could double-invalidate when two requests
arrived within the lock window. Use a single atomic CAS.
Fixes #482
# ^ no ticket in branch, no project bucket fits — both prefixes omitted
#1024 (API) refactor!: rename `user.email` column to `user.email_address`
BREAKING CHANGE: clients reading `user.email` must update to `user.email_address`.
Anti-patterns to avoid
- ❌
update stuff(no type, vague) - ❌
Fix: Bug in login page.(capitalized, trailing period) - ❌
feat: added the ability to export CSV files(past tense) - ❌
#12345 (CLIENT) feat: added export.(past tense + trailing period) - ❌ Inventing a ticket number when the branch doesn't have one — or guessing one from conversation context
- ❌
fix: quick fix per requestwhen the staged diff adds a new endpoint (type from the user's phrasing, not the diff — should befeat) - ❌
chore: various updatescovering two unrelated changes — flag the split instead - ❌ Forcing a
(PROJECT)tag when the diff doesn't actually map to API / CLIENT / CONSOLE / DB - ✅
#12345 (CLIENT) feat: add CSV export
