Create Commit Message
Overview
Generate a precise, Conventional Commits-formatted commit message by analyzing the staged git diff. The message classifies the change type, infers the scope from changed files, writes a clear subject line under 72 characters, and adds a body for complex changes explaining why the change was made.
Workflow
Read project context -- Check for .chalk/docs/engineering/ files, especially:
- Commit message conventions or contributing guides
- Any custom commit types or scopes used by this project
- Check for a
commitlint.config.js or similar configuration that defines allowed types and scopes
- If custom conventions exist, follow those instead of the defaults below
Get the staged diff -- Run:
git diff --cached --stat for a high-level overview of changed files
git diff --cached for the full diff content
git status to check for unstaged changes the user may have forgotten to stage
- If nothing is staged, inform the user and suggest staging commands
Classify the change type -- Analyze the diff content (not just file names) to determine the primary type:
| Type |
When to Use |
feat |
New functionality visible to users or consumers of the module |
fix |
Corrects a bug -- behavior was wrong, now it is right |
refactor |
Code restructuring with no behavior change |
docs |
Documentation only -- README, JSDoc, comments, guides |
test |
Adding or updating tests with no production code change |
chore |
Maintenance tasks: dependency updates, config changes, tooling |
perf |
Performance improvement with no functional change |
ci |
CI/CD pipeline changes (GitHub Actions, CircleCI, etc.) |
build |
Build system changes (webpack, tsconfig, Dockerfile, etc.) |
style |
Code formatting only -- whitespace, semicolons, no logic change |
revert |
Reverts a previous commit |
Decision rules:
- If the diff includes both a feature and tests for that feature, the type is
feat (tests are part of the feature)
- If the diff includes both a fix and a test that reproduces the bug, the type is
fix
- If you are genuinely unsure, ask the user -- do not guess
Infer the scope -- Determine the scope from the changed files:
- If all changes are in
src/auth/ or lib/auth/, scope is auth
- If all changes are in
components/Button/, scope is button
- If changes span multiple directories but share a domain, use the domain name
- If changes are truly cross-cutting, omit the scope
- Match existing scope conventions -- run
git log --oneline -50 and extract scopes from prior commits
- Scope should be lowercase, typically one word
Write the subject line -- Format: type(scope): description
- Total length: 72 characters maximum (including type and scope)
- Start with a lowercase imperative verb: "add", "fix", "remove", "update", "refactor"
- Do not end with a period
- Describe the what concisely -- the body explains why
- Be specific: "fix null pointer in user lookup" not "fix bug"
Write the body (if needed) -- Add a body separated by a blank line when:
- The change is not obvious from the subject line alone
- There is important context about why this change was made
- The approach was non-obvious and the reasoning should be documented
- The body should explain motivation, not repeat the diff
- Wrap lines at 72 characters
Write the footer (if needed) -- Add footers for:
- Breaking changes:
BREAKING CHANGE: <description of what breaks and migration path>
- Issue references:
Fixes #123 or Closes #456 or Refs #789
- Co-authors:
Co-authored-by: Name <email>
Present the commit message -- Output the complete commit message in a code block. Also output the git command to create the commit so the user can copy-paste it.
Commit Message Format
type(scope): subject line under 72 chars
Optional body explaining WHY this change was made, not WHAT changed
(the diff shows what changed). Wrap at 72 characters.
Motivation: explain the problem or opportunity.
Approach: explain why this approach was chosen over alternatives.
BREAKING CHANGE: description of what breaks and how to migrate
Fixes #123
Examples
Simple Feature
feat(auth): add password reset via email link
Bug Fix with Context
fix(checkout): prevent double-charge on retry after timeout
When a payment request timed out, the retry logic did not check whether
the original charge succeeded. This caused double-charges for ~0.3% of
transactions. Now we query the payment provider for existing charges
before retrying.
Fixes #892
Refactor
refactor(api): extract validation middleware from route handlers
Validation logic was duplicated across 12 route handlers with slight
variations. Extracted into a shared middleware that takes a Zod schema,
reducing duplication and ensuring consistent error responses.
Breaking Change
feat(api): require API key for all public endpoints
Previously, rate limiting on public endpoints relied on IP-based
throttling, which was easily circumvented. All public endpoints now
require an API key passed via the X-API-Key header.
BREAKING CHANGE: All API requests must include an X-API-Key header.
Existing clients need to register for an API key at /developer/keys
before upgrading.
Refs #1045
Chore
chore(deps): upgrade express from 4.18 to 4.19
Includes security patch for CVE-2024-XXXXX (prototype pollution in
query parser). No breaking changes per the changelog.
Subject Line Rules
| Rule |
Good |
Bad |
| Imperative mood |
add user search |
added user search |
| Lowercase start |
fix timeout |
Fix timeout |
| No period |
update readme |
update readme. |
| Specific |
fix null pointer in user lookup |
fix bug |
| Under 72 chars |
feat(auth): add OAuth2 PKCE flow |
feat(authentication-service): add the OAuth2 PKCE flow for single-page applications |
| Describes what, not how |
add retry logic for payments |
add try-catch block around payment call |
Type Classification Decision Tree
Is there new user-visible functionality?
├── Yes → feat
└── No
Was behavior incorrect before?
├── Yes → fix
└── No
Did only tests change?
├── Yes → test
└── No
Did only docs/comments change?
├── Yes → docs
└── No
Did behavior change?
├── No (same behavior, different structure) → refactor
└── No (same behavior, faster) → perf
Is it CI/CD config?
├── Yes → ci
└── Is it build config?
├── Yes → build
└── Is it formatting only?
├── Yes → style
└── chore
Anti-patterns
- Meaningless subjects -- "fix stuff", "WIP", "update", "changes", "misc" communicate nothing. Every commit message must describe a specific change. If you cannot describe it specifically, the commit is doing too many things -- split it.
- Scope that does not match the changed area --
fix(auth): update button color is wrong. The scope must reflect the area of code that actually changed. If the scope does not match, either the scope is wrong or the files are in the wrong directory.
- Body that repeats the diff -- "Changed line 45 from X to Y" is useless as a body. The body explains why -- the what is in the diff. If the subject line fully explains the change, omit the body entirely.
- Multiple unrelated changes in one commit -- If you cannot describe the commit in one subject line, it should be multiple commits. A commit that "add search and fix login and update deps" is three commits.
- Missing BREAKING CHANGE footer -- If the change breaks backwards compatibility for any consumer, the
BREAKING CHANGE: footer is mandatory. Omitting it means consumers get surprised at deploy time.
- Past tense -- "fixed", "added", "updated" instead of "fix", "add", "update". Conventional Commits uses the imperative mood because commit messages complete the sentence "This commit will...".
- Overly broad scope --
feat(app): ... or fix(src): ... are not useful scopes. The scope should identify the module, component, or domain -- not the top-level directory.
- Commit message longer than the change -- A one-line config change does not need a 10-line body. Scale the message to the complexity of the change.
1---2name: create-commit-message3description: Generate a Conventional Commit message from staged git changes when the user asks to write a commit message, create a commit, or format a commit4---5
6# Create Commit Message
7
8## Overview
9
10Generate a precise, Conventional Commits-formatted commit message by analyzing the staged git diff. The message classifies the change type, infers the scope from changed files, writes a clear subject line under 72 characters, and adds a body for complex changes explaining *why* the change was made.
11
12## Workflow
13
141. **Read project context** -- Check for `.chalk/docs/engineering/` files, especially:
15 - Commit message conventions or contributing guides
16 - Any custom commit types or scopes used by this project
17 - Check for a `commitlint.config.js` or similar configuration that defines allowed types and scopes
18 - If custom conventions exist, follow those instead of the defaults below
19
202. **Get the staged diff** -- Run:
21 - `git diff --cached --stat` for a high-level overview of changed files
22 - `git diff --cached` for the full diff content
23 - `git status` to check for unstaged changes the user may have forgotten to stage
24 - If nothing is staged, inform the user and suggest staging commands
25
263. **Classify the change type** -- Analyze the diff content (not just file names) to determine the primary type:
27
28 | Type | When to Use |
29 |------|-------------|
30 | `feat` | New functionality visible to users or consumers of the module |
31 | `fix` | Corrects a bug -- behavior was wrong, now it is right |
32 | `refactor` | Code restructuring with no behavior change |
33 | `docs` | Documentation only -- README, JSDoc, comments, guides |
34 | `test` | Adding or updating tests with no production code change |
35 | `chore` | Maintenance tasks: dependency updates, config changes, tooling |
36 | `perf` | Performance improvement with no functional change |
37 | `ci` | CI/CD pipeline changes (GitHub Actions, CircleCI, etc.) |
38 | `build` | Build system changes (webpack, tsconfig, Dockerfile, etc.) |
39 | `style` | Code formatting only -- whitespace, semicolons, no logic change |
40 | `revert` | Reverts a previous commit |
41
42 **Decision rules:**
43 - If the diff includes both a feature and tests for that feature, the type is `feat` (tests are part of the feature)
44 - If the diff includes both a fix and a test that reproduces the bug, the type is `fix`
45 - If you are genuinely unsure, ask the user -- do not guess
46
474. **Infer the scope** -- Determine the scope from the changed files:
48 - If all changes are in `src/auth/` or `lib/auth/`, scope is `auth`
49 - If all changes are in `components/Button/`, scope is `button`
50 - If changes span multiple directories but share a domain, use the domain name
51 - If changes are truly cross-cutting, omit the scope
52 - Match existing scope conventions -- run `git log --oneline -50` and extract scopes from prior commits
53 - Scope should be lowercase, typically one word
54
555. **Write the subject line** -- Format: `type(scope): description`
56 - Total length: 72 characters maximum (including type and scope)
57 - Start with a lowercase imperative verb: "add", "fix", "remove", "update", "refactor"
58 - Do not end with a period
59 - Describe the *what* concisely -- the body explains *why*
60 - Be specific: "fix null pointer in user lookup" not "fix bug"
61
626. **Write the body (if needed)** -- Add a body separated by a blank line when:
63 - The change is not obvious from the subject line alone
64 - There is important context about *why* this change was made
65 - The approach was non-obvious and the reasoning should be documented
66 - The body should explain motivation, not repeat the diff
67 - Wrap lines at 72 characters
68
697. **Write the footer (if needed)** -- Add footers for:
70 - Breaking changes: `BREAKING CHANGE: <description of what breaks and migration path>`
71 - Issue references: `Fixes #123` or `Closes #456` or `Refs #789`
72 - Co-authors: `Co-authored-by: Name <email>`
73
748. **Present the commit message** -- Output the complete commit message in a code block. Also output the git command to create the commit so the user can copy-paste it.
75
76## Commit Message Format
77
78```
79type(scope): subject line under 72 chars
80
81Optional body explaining WHY this change was made, not WHAT changed
82(the diff shows what changed). Wrap at 72 characters.
83
84Motivation: explain the problem or opportunity.
85Approach: explain why this approach was chosen over alternatives.
86
87BREAKING CHANGE: description of what breaks and how to migrate
88
89Fixes #123
90```
91
92## Examples
93
94### Simple Feature
95
96```
97feat(auth): add password reset via email link
98```
99
100### Bug Fix with Context
101
102```
103fix(checkout): prevent double-charge on retry after timeout
104
105When a payment request timed out, the retry logic did not check whether
106the original charge succeeded. This caused double-charges for ~0.3% of
107transactions. Now we query the payment provider for existing charges
108before retrying.
109
110Fixes #892
111```
112
113### Refactor
114
115```
116refactor(api): extract validation middleware from route handlers
117
118Validation logic was duplicated across 12 route handlers with slight
119variations. Extracted into a shared middleware that takes a Zod schema,
120reducing duplication and ensuring consistent error responses.
121```
122
123### Breaking Change
124
125```
126feat(api): require API key for all public endpoints
127
128Previously, rate limiting on public endpoints relied on IP-based
129throttling, which was easily circumvented. All public endpoints now
130require an API key passed via the X-API-Key header.
131
132BREAKING CHANGE: All API requests must include an X-API-Key header.
133Existing clients need to register for an API key at /developer/keys
134before upgrading.
135
136Refs #1045
137```
138
139### Chore
140
141```
142chore(deps): upgrade express from 4.18 to 4.19
143
144Includes security patch for CVE-2024-XXXXX (prototype pollution in
145query parser). No breaking changes per the changelog.
146```
147
148## Subject Line Rules
149
150| Rule | Good | Bad |
151|------|------|-----|
152| Imperative mood | `add user search` | `added user search` |
153| Lowercase start | `fix timeout` | `Fix timeout` |
154| No period | `update readme` | `update readme.` |
155| Specific | `fix null pointer in user lookup` | `fix bug` |
156| Under 72 chars | `feat(auth): add OAuth2 PKCE flow` | `feat(authentication-service): add the OAuth2 PKCE flow for single-page applications` |
157| Describes what, not how | `add retry logic for payments` | `add try-catch block around payment call` |
158
159## Type Classification Decision Tree
160
161```
162Is there new user-visible functionality?
163├── Yes → feat
164└── No
165 Was behavior incorrect before?
166 ├── Yes → fix
167 └── No
168 Did only tests change?
169 ├── Yes → test
170 └── No
171 Did only docs/comments change?
172 ├── Yes → docs
173 └── No
174 Did behavior change?
175 ├── No (same behavior, different structure) → refactor
176 └── No (same behavior, faster) → perf
177 Is it CI/CD config?
178 ├── Yes → ci
179 └── Is it build config?
180 ├── Yes → build
181 └── Is it formatting only?
182 ├── Yes → style
183 └── chore
184```
185
186## Anti-patterns
187
188- **Meaningless subjects** -- "fix stuff", "WIP", "update", "changes", "misc" communicate nothing. Every commit message must describe a specific change. If you cannot describe it specifically, the commit is doing too many things -- split it.
189- **Scope that does not match the changed area** -- `fix(auth): update button color` is wrong. The scope must reflect the area of code that actually changed. If the scope does not match, either the scope is wrong or the files are in the wrong directory.
190- **Body that repeats the diff** -- "Changed line 45 from X to Y" is useless as a body. The body explains *why* -- the *what* is in the diff. If the subject line fully explains the change, omit the body entirely.
191- **Multiple unrelated changes in one commit** -- If you cannot describe the commit in one subject line, it should be multiple commits. A commit that "add search and fix login and update deps" is three commits.
192- **Missing BREAKING CHANGE footer** -- If the change breaks backwards compatibility for any consumer, the `BREAKING CHANGE:` footer is mandatory. Omitting it means consumers get surprised at deploy time.
193- **Past tense** -- "fixed", "added", "updated" instead of "fix", "add", "update". Conventional Commits uses the imperative mood because commit messages complete the sentence "This commit will...".
194- **Overly broad scope** -- `feat(app): ...` or `fix(src): ...` are not useful scopes. The scope should identify the module, component, or domain -- not the top-level directory.
195- **Commit message longer than the change** -- A one-line config change does not need a 10-line body. Scale the message to the complexity of the change.