Back to blog
Engineering29 min read

Free Commit Message Generator (Conventional Commits)

Paste a diff, get a Conventional Commits message. 21 prompts covering splits, breaking-change footers and PR descriptions, plus the spec rules and semver mapping most generators get wrong.

NH
Nafiul Hasan
Founder, Prompt Architects

TL;DR: A commit message generator can read your diff and describe what changed. It cannot read why you changed it, so every prompt below forces the model to mark what it inferred. Twenty-one copy-paste prompts, grounded in Conventional Commits 1.0.0, covering splits, breaking changes, and PR descriptions.

Why can't a commit message generator tell you why you made the change?

Because the diff does not contain it. A diff is a complete record of what the code looks like now versus before, and a total blank on the question every future reader of git log is actually asking: what was the problem, and why this fix rather than the obvious one.

That gap is the whole reason commit messages are hard, and it does not close when you point a language model at the patch. The model has the diff and nothing else. Ask it for a message and it will produce one, complete with a confident-sounding reason, because producing a plausible continuation is what it does. The Angular commit guidelines are blunt about what the body is for: "Explain the motivation for the change in the commit message body." A model reading a rename cannot know whether you renamed the function because it was misleading, because a reviewer asked, or because you are three commits into a migration.

So a good commit message generator is not a one-shot "diff in, message out" machine. It is two jobs stapled together. Describing the change accurately in the spec's grammar, a model does well, faster and more consistently than you do at 6pm. Supplying the motivation is yours, and the only safe design is a prompt that either asks you for it or clearly labels the part it made up. Every template below does one or the other.

What does the Conventional Commits spec actually require?

Conventional Commits 1.0.0 is a published specification, and its rules use RFC 2119 language, so "MUST" and "MAY" mean what they mean in an RFC. It gives the message structure as:

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

The normative sentences worth knowing by heart, quoted from the spec at conventionalcommits.org/en/v1.0.0/:

RuleThe spec's words
The prefix"Commits MUST be prefixed with a type, which consists of a noun, feat, fix, etc., followed by the OPTIONAL scope, OPTIONAL !, and REQUIRED terminal colon and space."
Scope"A scope MAY be provided after a type. A scope MUST consist of a noun describing a section of the codebase surrounded by parenthesis"
Description"A description MUST immediately follow the colon and space after the type/scope prefix."
Body"The body MUST begin one blank line after the description." And: "A commit body is free-form and MAY consist of any number of newline separated paragraphs."
Footers"One or more footers MAY be provided one blank line after the body."
Breaking changes"Breaking changes MUST be indicated in the type/scope prefix of a commit, or as an entry in the footer."
Case"The units of information that make up Conventional Commits MUST NOT be treated as case-sensitive by implementors, with the exception of BREAKING CHANGE which MUST be uppercase."

Two details in there are worth slowing down for, because they are where most generators go wrong.

The breaking-change signal has two forms and either one counts. The spec says a footer version "MUST consist of the uppercase text BREAKING CHANGE, followed by a colon, space, and description", and separately that "If ! is used, BREAKING CHANGE: MAY be omitted from the footer section, and the commit description SHALL be used to describe the breaking change." So feat!: drop Node 6 support is valid on its own, and so is a feat: with a BREAKING CHANGE: footer. The spec also notes that "A BREAKING CHANGE can be part of commits of any type" and that "BREAKING-CHANGE MUST be synonymous with BREAKING CHANGE, when used as a token in a footer." Whether your tooling agrees is a separate question, answered two sections down.

Footers are git trailers, not free text. Each footer is a token, then either a colon and a space or a space and a #, then a value. Tokens use hyphens in place of spaces, which is why you see Reviewed-by: and Refs: rather than Reviewed by:. BREAKING CHANGE is the one exception that keeps its space.

Here is a message that satisfies every rule above, taken from the spec's own examples:

fix: prevent racing of requests

Introduce a request id and a reference to latest request. Dismiss
incoming responses other than from latest request.

Remove timeouts which were used to mitigate the racing issue but are
obsolete now.

Reviewed-by: Z
Refs: #123

Which commit types are in the spec, and which come from Angular?

This is the distinction almost every cheat sheet on this topic gets wrong. The Conventional Commits specification defines exactly two types. feat "MUST be used when a commit adds a new feature" and fix "MUST be used when a commit represents a bug fix". Everything else is convention borrowed from elsewhere, and the spec says so directly: "Additional types are not mandated by the Conventional Commits specification, and have no implicit effect in Semantic Versioning (unless they include a BREAKING CHANGE)."

The spec then points at where the rest come from: "types other than fix: and feat: are allowed, for example @commitlint/config-conventional (based on the Angular convention) recommends build:, chore:, ci:, docs:, style:, refactor:, perf:, test:, and others."

Follow that pointer and the two lists turn out not to match. Angular's own commit-message guidelines list a header type of build|ci|docs|feat|fix|perf|refactor|test, eight entries, and chore is not one of them. Reverts are handled by a separate rule in that document rather than by a header type. @commitlint/config-conventional ships an eleven-entry type-enum: those eight plus chore, style and revert. So the chore: prefix that half the internet treats as canonical Angular is, strictly, a commitlint addition.

Four layers people all call Conventional Commits. Read from each project's own source, 27 August 2026.
FeatureConventional Commits 1.0.0Angular guidelines@commitlint/config-conventionalsemantic-release (default preset)
Types it namesfeat and fix are normative; others allowedbuild, ci, docs, feat, fix, perf, refactor, testthose eight, plus chore, style, revertfeat, fix, perf, revert
Rejects a type outside its list
Recognises the ! shorthandNot mentioned
Recognises BREAKING-CHANGE with a hyphenNot mentioned
Caps the header length100 characters
Decides the version bumpDescribes the mapping

The practical consequence: find out which list your repository enforces before you generate anything. If there is a commitlint.config.js, the type-enum in it or in the config it extends is what the commit-msg hook will fail on.

How does a commit type become a version number?

This mapping is the actual reason teams adopt the convention. Semantic Versioning 2.0.0 defines the three components as: "MAJOR version when you make incompatible API changes", "MINOR version when you add functionality in a backward compatible manner", and "PATCH version when you make backward compatible bug fixes". Conventional Commits was designed to make those three decisions readable from the log, and its own FAQ states the correspondence: fix commits translate to PATCH, feat to MINOR, and a BREAKING CHANGE in any type to MAJOR.

Release tooling then adds rules the spec never mandated. semantic-release's README says plainly that it "uses the commit messages to determine the consumer impact of changes in the codebase", and that "By default, semantic-release uses Angular Commit Message Conventions". Its commit-analyzer ships a default release-rules file that includes { breaking: true, release: "major" }, { type: "perf", release: "patch" } and { revert: true, release: "patch" } alongside the two you would expect.

CommitConventional Commits 1.0.0semantic-release, default config
fix: …PATCHpatch
feat: …MINORminor
Any type with a BREAKING CHANGE: footerMAJORmajor
perf: …No implicit effectpatch
revert: …Behaviour not defined by the specpatch
docs:, chore:, style:, test:, ci:, build:, refactor:No implicit effectNo release

The safe habit that survives every preset: write the ! and the footer. The spec permits omitting the footer; no spec anywhere penalises including both.

The base prompt: a diff in, a conventional message out

Every prompt in this section assumes you paste real diff output. Get it with git diff --staged for what you are about to commit, or git diff HEAD~1 for the last one. Stage first, then generate: a message written from your whole working tree will describe changes that are not in the commit.

Prompt 1: the core generator, with inference marked

You are writing a git commit message that follows Conventional Commits 1.0.0.

Here is the staged diff:

<paste `git diff --staged` here>

Rules:
- Format: <type>[optional scope]: <description>, then a blank line, then an
  optional body, then a blank line, then optional footers.
- Description: imperative mood, lowercase first letter, no full stop, under
  72 characters.
- Use only these types: feat, fix, docs, style, refactor, perf, test, build,
  ci, chore, revert.
- Choose the scope from a path or module that actually appears in the diff.
  If no single scope covers it, omit the scope entirely. Do not invent one.

Then, under a heading INFERRED, list every claim in your message that is not
directly visible in the diff, one line each, with the words you used and why
you believed it. If you inferred nothing, write INFERRED: none.

Do not write a body unless the diff itself shows the reason. If the reason is
not visible, write BODY: needs author input instead of guessing.

Prompt 2: pick the scope from the repository's allowed list

Same task as before, but this repository restricts scopes. The allowed scopes are:

<paste the scope list from CONTRIBUTING.md or commitlint.config.js>

Choose exactly one from that list, or omit the scope if none of them is a
better fit than no scope at all. Never emit a scope outside the list. If two
scopes fit equally, say so and give me both candidate headers rather than
picking silently.

Prompt 3: make it pass commitlint before I paste it

Rewrite this commit message so it passes @commitlint/config-conventional:

<paste the message>

The rules that fail builds:
- header at most 100 characters
- type must be lowercase and one of: build, chore, ci, docs, feat, fix, perf,
  refactor, revert, style, test
- subject must not be empty
- subject must not end with a full stop
- subject must not be sentence-case, start-case, pascal-case or upper-case
- body and footer lines at most 100 characters, each preceded by a blank line

Return the corrected message, then a list of which rules the original broke.
Change wording only as far as a rule requires. Do not rewrite for style.

Prompt 4: the one-line mode for trivial diffs

This diff is small and mechanical. Give me a single-line Conventional Commits
header and nothing else: no body, no footer, no explanation.

<paste the diff>

If the diff is not actually trivial, meaning it changes behaviour, touches a
public interface, or does more than one thing, do not give me a header. Reply
with NOT TRIVIAL and one sentence saying why.

That last clause matters more than it looks. The refusal path stops a chore: prefix being attached to a behaviour change, and a behaviour change filed as a chore is a release that never happens.

What if the diff should be several commits, not one?

This is the case every commit message generator skips, and it is the one where a model actually earns its keep. You have been heads-down for two hours. The working tree contains a bug fix, a refactor you did on the way, and a dependency bump. One commit message cannot honestly describe that, and the spec's own FAQ takes a position on it. Asked what to do when a commit conforms to more than one type, Conventional Commits answers: "Go back and make multiple commits whenever possible. Part of the benefit of Conventional Commits is its ability to drive us to make more organized commits and PRs."

The model is well suited to the first half of that work. It can read a diff and group hunks by concern faster than you can scroll. It cannot decide whether the split is worth your next ten minutes, so these prompts stop at a proposal.

Prompt 5: should this be one commit or several?

Here is my full working diff:

<paste `git diff` output>

Answer one question first: is this one logical change or several?

Apply this test. Two hunks belong in the same commit only if reverting one
without the other would leave the tree broken or the intent incoherent. If
either hunk could be reverted alone and still make sense, they are separate
commits.

Reply with ONE COMMIT or N COMMITS. If it is more than one, list the commits
in the order they should be applied, and for each give: a one-line Conventional
Commits header, the files and hunks it should contain, and one sentence on why
it is separate. Do not write bodies yet.

Prompt 6: turn the split into a staging plan

Take the split you just proposed. For each commit in order, give me the exact
commands to stage only that commit's changes, using `git add -p` where a file
contains hunks belonging to more than one commit, and plain `git add <path>`
where a whole file belongs to one commit.

For each `git add -p` step, tell me which hunk to answer y to and which to
answer n to, described by the first changed line of the hunk so I can identify
it on screen. Do not guess hunk numbers; the numbering depends on my git
config and you cannot see it.

Prompt 7: keep every intermediate commit buildable

Reorder the commit sequence you proposed so that each commit leaves the
repository in a state that compiles and passes its own tests. Move type
definitions, config and migrations ahead of the code that depends on them.

You cannot run the build, so state your ordering as a claim I have to verify,
not as a fact. After the list, add a section CANNOT VERIFY listing every
ordering decision that depends on something the diff does not show, such as
generated files, lockfiles, or a test that reads fixtures not in the diff.

Prompt 8: rescue an enormous WIP blob

This diff is the result of two days of unstructured work across many files.

<paste the diff, or a `git diff --stat` plus the diffs of the largest files>

Group it into at most six commits. Prefer fewer, larger commits over a long
tail of one-line ones. Anything you cannot confidently assign, put in a final
group called UNASSIGNED rather than forcing it into a theme.

For each group give: a Conventional Commits header, the file paths, and a
confidence of high, medium or low. Sort by confidence, lowest last, so I know
which groups to check by hand.

The confidence sort is the useful part. On a large diff a model will produce six plausible groupings whether or not six real ones exist, and the low-confidence tail is where the grouping is invented rather than observed.

How do you write the body when the "why" is not in the diff?

By making the model ask before it writes. This is the same interrogate-then-assemble move that works for bug reports and ticket writing, and it applies here for the same reason: the person with the answers is sitting at the keyboard, and a one-shot prompt has no way to reach them.

Prompt 9: interrogate first, draft second

I need a commit body for this change. Do not write anything yet.

<paste the diff>

Ask me these questions, one message at a time, and wait for my answer before
the next:

1. What was the problem, in one sentence, as someone hitting it would describe it?
2. What was the root cause, if you know it?
3. Why this fix rather than the more obvious alternative?
4. What did you consider and reject?
5. What else does this change affect that the diff does not show?
6. Is there a ticket, incident, or review comment this came from?

Accept "I don't know" and "not applicable" as complete answers and move on.
When you have all six, write the full Conventional Commits message: header,
blank line, body wrapped at 72 characters, blank line, footers.

Use only what I told you. Do not add motivation I did not give you. If an
answer was "I don't know", leave that out of the body rather than filling it in.

Prompt 10: mine the body out of the ticket

Here is the issue thread this work came from, and the diff that closes it.

ISSUE:
<paste the issue title, description and the most relevant comments>

DIFF:
<paste the diff>

Write the Conventional Commits message. The body must explain the motivation
using only what the issue says, and must describe the change using only what
the diff shows. Do not let the two blur.

Add a footer referencing the issue in the form the repository already uses.
If you cannot tell from what I pasted which form that is, ask me instead of
picking one.

Then list, under CROSS-CHECK, anything the issue asked for that you cannot
find in the diff, and anything the diff does that the issue never asked for.

That cross-check earns the extra tokens: a diff that quietly does more than the ticket asked is a common cause of surprise in review, and it is visible from the two texts side by side.

Prompt 11: turn a review comment into the reason

This commit exists because of a code review comment. Here is the comment and
the change I made in response.

COMMENT:
<paste it>

DIFF:
<paste it>

Write the message. The body should record what the original approach was, what
the reviewer objected to, and what changed, in that order, in at most four
sentences. Add a Reviewed-by footer if I give you a name; do not invent one.

Prompt 12: the fix body, in the shape a future reader needs

Write a Conventional Commits message for this bug fix. The body must have
exactly four short paragraphs, in this order:

1. The symptom, as a user or caller experienced it.
2. The root cause, mechanically.
3. What this change does about it.
4. Blast radius: what else touches the changed code path.

For any of the four you cannot support from the diff or from what I told you,
write the paragraph as UNKNOWN: <what you would need to know>. An honest gap
is more useful to whoever reads this in a year than a confident guess.

<paste the diff>

Carefully, and conservatively. Deciding that a change is breaking is a judgement about your public surface, and a model looking at a diff of one package cannot see who calls you.

Prompt 13: is this actually breaking?

Read this diff and decide whether it contains a breaking change under Semantic
Versioning: an incompatible change to the public API.

<paste the diff>

Treat as breaking: removing or renaming an exported symbol, changing a function
signature in a way existing calls would fail on, removing or renaming a config
key or environment variable, changing a default that alters existing behaviour,
tightening validation so previously accepted input is rejected, changing a
response shape, removing a database column or route.

Treat as not breaking: additions, internal renames of non-exported symbols,
new optional parameters, performance work with identical semantics.

Reply BREAKING or NOT BREAKING, then the specific lines that decided it. If it
depends on something you cannot see, such as whether a symbol is re-exported
from a package entry point, say DEPENDS and name exactly what I need to check.

Prompt 14: write both signals, and the migration

This change is breaking. Write the full Conventional Commits message.

Requirements:
- Put ! immediately before the colon in the header.
- Also include a BREAKING CHANGE: footer, uppercase, with a colon and a space.
  The spec allows omitting it when ! is present; include it anyway, because
  some release parsers only read the footer.
- The footer's first line is a one-sentence summary of what breaks.
- Then a blank line, then migration instructions a caller can follow: the old
  call, the new call, and anything they must change in config.

<paste the diff, and any migration notes you already have>

If you do not have enough information to write real migration steps, write
MIGRATION: needs author input rather than writing generic advice.

Applied to a real change, that produces a message like this:

feat(api)!: return ISO 8601 strings from /events instead of epoch seconds

The events endpoint now serialises `startsAt` and `endsAt` as ISO 8601
strings in UTC. Epoch integers were ambiguous across clients that applied
a local timezone offset before display.

BREAKING CHANGE: `startsAt` and `endsAt` in the /events response are ISO 8601
strings, not integers.

Clients parsing these fields as numbers must switch to a date parser. If you
need the previous format during migration, send `Accept-Version: 2025-11-01`,
which will be removed in the next major.

Refs: #4821

Every part of that is spec-legal: type with a scope and !, a description in the imperative with no full stop, a body one blank line down, and an uppercase BREAKING CHANGE: footer alongside a Refs: trailer.

Prompt 15: audit a breaking change I already wrote

Check this commit message against Conventional Commits 1.0.0:

<paste the message>

Verify: the header matches <type>[optional scope]: <description>; the ! if
present sits immediately before the colon; the body starts one blank line
after the description; footers start one blank line after the body; the
BREAKING CHANGE token is uppercase with a colon and a space; every other
footer token uses hyphens instead of spaces.

List violations with the rule each one breaks. Then say whether a parser that
only reads footers, ignoring !, would still detect the breaking change.

How do you rewrite a bad commit message that is already in history?

The messages are already written; the diffs are the only reliable source. Note the constraint before you start: rewriting a message rewrites the commit, which changes its hash. Do that only on commits you have not pushed, or on a branch nobody else has based work on.

Prompt 16: rewrite one message from its own diff

This commit has a useless message. Rewrite it from the diff.

CURRENT MESSAGE:
<paste it>

DIFF:
<paste `git show <sha>` output>

Write a Conventional Commits message that describes what the diff actually
does. Keep any real information the old message contained, including ticket
numbers, even if the rest of it was noise.

Under INFERRED, list everything in your new message that came from you rather
than from the diff or the old message.

Prompt 17: rewrite a range, as a rebase plan

Here are the last N commits on my branch, each with its message and diffstat:

<paste `git log --format='%h %s' --stat -n 10` or similar>

For each, give me: the original short SHA, the original subject, and a
Conventional Commits replacement subject. Output as a three-column table so I
can work through an interactive rebase against it.

Where a commit's diffstat is too thin to tell what it did, write NEEDS DIFF in
the replacement column instead of guessing, and I will paste that commit's
full diff separately.

Do not propose squashing or reordering. Subjects only.

Prompt 18: audit without fixing

Check every message below against @commitlint/config-conventional and report
only violations. Do not rewrite anything.

<paste `git log --format='%s' <base>..HEAD`>

For each violating message: the message, the rule it breaks, and whether it
would block a commit-msg hook or merely read badly. Finish with a count of
clean versus violating messages.

How do you turn a commit range into a PR description?

If the commits are clean, the PR description is mostly assembly, and this is where the convention pays back the discipline it cost. If the commits are not clean, the model will produce a tidy summary of a messy branch, which is worse than no summary, because it hides the mess from the reviewer instead of surfacing it.

Prompt 19: the PR description

Here is the commit range for this pull request:

<paste `git log --format='%s%n%b%n---' origin/main..HEAD`>

And the diffstat:

<paste `git diff --stat origin/main..HEAD`>

Write a pull request description with these sections:

## What changed
Grouped by Conventional Commits type, in the order feat, fix, perf, refactor,
everything else. One bullet per commit, using the commit's own subject.

## Why
Only what the commit bodies actually say. If the bodies say nothing about
motivation, write "Not stated in the commits" rather than inferring it.

## Breaking changes
Every BREAKING CHANGE footer in the range, verbatim. If there are none, write
"None".

## Review focus
Three to five specific things a reviewer should look at, each naming a file or
function from the diffstat.

Do not add a summary paragraph at the top. Do not use marketing language.

Prompt 20: the changelog section

From the same commit range, produce a CHANGELOG entry for an unreleased
version, grouped under the headings Features, Bug Fixes, Performance
Improvements and Reverts. Omit any heading with no entries.

Then state which version bump this range implies under Conventional Commits:
MAJOR if any commit is breaking, MINOR if any feat and none breaking, PATCH if
only fix commits, and no release if none of the above. Show the commit that
drove the decision.

Prompt 21: a squash-merge title

This branch will be squash-merged, so the PR title becomes the single commit
message on main. Write it as one Conventional Commits header.

<paste the commit subjects>

Pick the highest-impact type present in the range: breaking beats feat, feat
beats fix, fix beats everything else. If the branch contains both a feat and a
breaking change, the header must carry the ! and the PR body must carry the
BREAKING CHANGE footer, because the squashed commit is the only one the release
tool will see.

That last sentence is a genuine trap in squash-merge repositories. A BREAKING CHANGE: footer written on a commit inside the branch disappears when the branch is squashed, and the release that should have been a major goes out as a minor. If your team squashes, the PR body is where the footer has to live.

Where does a model reading a diff reliably go wrong?

In four places, all of them predictable, which is what makes them manageable.

It states a reason it inferred from shape. A model that sees a try block added around a network call will write "to handle intermittent upstream failures", because that is the usual reason. If your actual reason was a specific incident last Thursday, the message now contains a plausible falsehood that will outlive you in the log. This is ordinary model behaviour rather than a bug, and it is the same mechanism behind every other confident invention, which the longer explanation of why models make things up covers properly.

It picks the wrong type when the diff is ambiguous. A refactor that also fixes a bug is a fix, because the release consequence matters more than the shape of the edit. A model reading the diff sees mostly moved code and reaches for refactor, which under most configurations produces no release at all, so your fix ships to nobody.

It invents a scope. Scopes are project vocabulary. A model handed a diff touching src/lib/auth-claims.ts will offer auth, claims, lib or auth-claims with equal confidence, and only one of them is what your team writes. Give it the allowed list, or tell it to omit the scope.

It writes a body about the code rather than the change. "This commit updates the parser to handle nested objects" restates the diff in English, which adds nothing a reader could not get from git show. A body earns its place only when it says something the diff cannot: why, what was rejected, what else is affected.

The single instruction that mitigates all four is the one repeated through every prompt above: make the model separate observation from inference, in a labelled block, every time. Once the INFERRED section exists, checking it takes about fifteen seconds and needs no judgement at all. You either said that or you did not.

Free Chrome Extension

Stop rewriting prompts. Start shipping.

Works with ChatGPT, Claude, Gemini, Grok, Midjourney, Ideogram, Veo3 & Kling. 5.0★ on the Chrome Web Store.

Create An Account

A checklist before you paste the message into git

Six checks, roughly thirty seconds.

  1. Read the INFERRED block first, before the message. Delete anything you did not actually say.
  2. Check the type against the release you want. If this should ship, is it feat or fix? If it should not, is it one of the no-release types?
  3. Check the scope exists in your repository's vocabulary, or is absent.
  4. Check the description is imperative, lowercase, and has no full stop. Angular's rules are exactly those three: "use the imperative, present tense", "don't capitalize the first letter", "no dot (.) at the end".
  5. Check breaking-change signalling twice. The ! and the footer, both present, footer uppercase.
  6. Check the blank lines. Header, blank, body, blank, footers. Parsers key off those blank lines, and a missing one turns your footer into the last paragraph of your body.

The failure mode this list guards against is not the obviously wrong message. It is the one that reads perfectly and says something you never said, which is why the same discipline applies to AI-assisted code review: the output that looks finished is the one that gets checked least.

One practical note. A twenty-one-prompt set decays under deadline pressure: people retype an abbreviated version, the INFERRED clause is the first thing dropped, and within a month the discipline is gone. Keeping the exact wording one keystroke away, whether as a saved template, a shell alias, or an MCP server wired into your editor, is the difference between using this once and using it on every commit. The same argument applies to versioning prompts the way you version code: a prompt you improved last month is worth nothing if you cannot find it this month.

Prompt Architects is a prompt library and enhancer with a free plan; its /faq page publishes the free allowance as 5 prompt enhancements per day, forever. It does not read your repository and it does not write your commits. It holds these prompts in the wording you settled on and hands them back when you need them. Whether that beats a text file in your dotfiles depends mostly on how many machines you work from.

Frequently asked questions

Free Chrome Extension

Stop rewriting prompts. Start shipping.

Works with ChatGPT, Claude, Gemini, Grok, Midjourney, Ideogram, Veo3 & Kling. 5.0★ on the Chrome Web Store.

Create An Account