Field Note 52current

Conventional Commits: the log is an interface, not a diary

Write every commit message as Conventional Commits — type, scope, breaking marker, prose body, reference footers. The log is the only documentation permanently welded to the change; structure is what makes it queryable and versions derivable. Enforce it in CI.

By
Theo Zourzouvillys
Published
Tags
processinteropllmprinciples

TL;DR

Every commit message, in every repository, follows Conventional CommitsConventional Commits 1.0.0A lightweight specification for commit messages that adds a small structured header — a type, an optional parenthesised scope, an optional `!` breaking marker, and a description — followed by an optional prose body and git-trailer-style footers including `BREAKING CHANGE:`. It is deliberately short, defines the mapping onto Semantic Versioning, and is designed to be parsed by release tooling.conventionalcommits.org ↗:

type(scope)!: imperative summary, lower case, no trailing period

Why this change exists and what it rules out. Wrapped prose — the part
that cannot be recovered from the diff.

BREAKING CHANGE: <what breaks, and what a caller has to do about it>
Refs: DR-12
  • The header is structured data. A type from a small closed vocabulary, an optional scope, ! for a breaking change. This is the part machines read.
  • The body is prose, and it is the actual message. The specification says nothing about whether a commit message is any good. fix(auth): fix bug is perfectly conventional and worthless.
  • BREAKING CHANGE: is a claim about compatibility, not decoration. If a release pipeline reads it, a missing one ships an incompatible change as a patch.
  • Footers carry references — decision record ids, issue ids, co-authors — so the commit points at the argument instead of restating it.
  • Enforce it mechanically, in CI, blocking. Including on squashed pull request titles, which is the step almost everyone misses.
  • Don’t invent a house prefix scheme. This one is specified, stable, and already has tooling (ZFN-30Field Note · currentZFN-30 — Use the standard; don't reinvent the protocolWhen a standard exists for a common or complex problem, use it — don't reinvent the protocol. Standards encode huge adversarial expertise, especially in auth and crypto; a partial implementation beats rolling your own. You're not that special, and your problem isn't either.Why it's cited here: Why to adopt the published commit spec rather than growing a house prefix scheme that means the same thing, badly.Open ZFN-30 →).

Context

The commit message is the only piece of documentation permanently welded to the change it describes.

Everything else detaches. A pull request description lives in a forge’s database, and forges get migrated, acquired, or abandoned; what survives the export is rarely what you wanted. Ticket ids outlive at most two trackers. A code comment describes the state the code is in, never the transition it went through or the option that was rejected on the way. Design docs describe the plan, which is not always what shipped. The log is the one artifact that travels inside the repository, forever, and it is what you actually reach for at two in the morning when a line makes no sense and you need to know what its author was worried about.

And in most repositories it is the least disciplined text in the project. That is not laziness so much as structural: nobody reviews a commit message, it can’t be edited once it’s pushed, its reader is a stranger — usually a future version of you who has forgotten everything — and the feedback loop between writing it badly and needing it is measured in years. Every incentive at the moment of writing points at typing fix stuff and moving on.

Two things changed that make this worth fixing now rather than accepting as a fact of life.

The first is that tooling which reads the log became standard. Changelog generation, version derivation, release automation, “what shipped in this deploy” — all of it wants to parse the history, and prose is not parseable. Deriving the version number from the log rather than choosing it by hand is only possible if each commit declares what kind of change it is (SemVerSemantic Versioning 2.0.0Defines MAJOR.MINOR.PATCH so a version number carries a compatibility promise: patch for backwards-compatible fixes, minor for backwards-compatible additions, major for anything that breaks a documented contract. Conventional Commits exists largely to let that number be derived from the log rather than chosen by hand.semver.org ↗ is a promise about compatibility, and something has to make that promise).

The second is that agents now write most commits and read the history to understand the code. That cuts both ways, and both ways favour a format. An LLM applies a documented convention exactly and without fatigue, which is precisely what humans are worst at over a thousand commits — the convention stops decaying by the third contributor. And on the reading side, an agent doing archaeology on a subsystem is querying git log; a structured, greppable history is one it can answer questions from, and a wall of wip is one it can only guess at. This is the same asymmetry as ZFN-1Field Note · currentZFN-1 — Keep engineering decision recordsRecord significant engineering decisions as short, versioned markdown files — context, decision, consequences. Write one for cross-team contracts, directional principles, hard-to-reverse choices, and conventions others must follow. Cite them instead of re-arguing.Why it's cited here: Where the reasoning actually lives — a commit footer cites the decision record rather than re-arguing it in every message.Open ZFN-1 →: models are excellent at applying rules they can find and hopeless at inferring rules from absence.

There is a shape here worth naming, because it is the same one as ZFN-51Field Note · currentZFN-51 — Design the request envelope before the first endpointAuth, idempotency keys, trace IDs, vector clocks: context about a request, not part of it. Define an envelope alongside the payload in the schema on day one — transport-native, per-hop vs propagated, owned by generated SDKs and middleware. The retrofit is what costs you.Why it's cited here: The same envelope-versus-payload split, applied to a commit: metadata about the change belongs in defined fields, not smeared into free text.Open ZFN-51 →. A commit has an envelope — metadata about the change: what kind it is, what it touches, whether it breaks anything, what it references — and a payload: the prose explaining why. If the envelope has no defined home, it doesn’t disappear; it gets smeared into the free-text body, where nothing can act on it. [URGENT] fix login (this breaks the SDK, see TICKET-4021) contains every field this note asks for, and not one of them is addressable by anything except a human reading carefully.

The realistic alternative to adopting the spec was never “no convention.” It is a local convention, undocumented and unenforced: [fix], then FIX:, then fix -, then Fix -, diverging quietly with each new contributor until nothing can be parsed and nobody remembers there was a rule. That is the reinvention ZFN-30Field Note · currentZFN-30 — Use the standard; don't reinvent the protocolWhen a standard exists for a common or complex problem, use it — don't reinvent the protocol. Standards encode huge adversarial expertise, especially in auth and crypto; a partial implementation beats rolling your own. You're not that special, and your problem isn't either.Why it's cited here: Why to adopt the published commit spec rather than growing a house prefix scheme that means the same thing, badly.Open ZFN-30 → is about, in miniature. The specification is two pages, it is stable at 1.0.0, and the linters and release tools are already written (ZFN-45Field Note · currentZFN-45 — Read the standards; better yet, help write themLearn to read standards docs — RFCs, W3C recs — fluently; they're the primary source, not a last resort. Even better, get involved: reading them well makes you a sharper builder, and helping write them is the best protocol education there is.Why it's cited here: Read the spec itself before adopting it; it is two pages and settles most of the arguments teams have about it.Open ZFN-45 → — read it, it settles most of the arguments teams have about it).

Recommendation

Adopt Conventional Commits verbatim, enforce it in CI, and keep the type vocabulary closed.

Keep the type list short and boring

The taxonomy is the only part of this anyone argues about, and the argument has no payoff. Take the Angular setAngular commit message guidelinesThe in-repo convention Conventional Commits was generalised from, and still the origin of the canonical type vocabulary (feat, fix, docs, refactor, perf, test, build, ci, chore). Worth reading for how a large project actually applies the format day to day, including its rules on scope and on what belongs in the body.github.com ↗ as given — feat, fix, docs, refactor, perf, test, build, ci, chore — and resist every proposal to add wip, hotfix, style, or security. Two rules settle most disputes:

  • feat and fix are the only types that describe a change in observable behaviour for someone outside the repository. They are the two that drive the version number. Everything else is a bucket, and which bucket a change lands in matters far less than people argue as though it does.
  • On the feat/fix boundary, ask what a consumer who read the documentation would expect. New capability that wasn’t promised is a feature; restoring behaviour that was promised and wasn’t delivered is a fix.

Treat scope as a vocabulary with an owner

Scope earns its keep in a monorepo and is close to dead weight in a single-service repository. If you use it, the legal scopes are an enumerated list the linter validates — for the same reason an envelope enumerates its fields rather than accepting arbitrary keys. Left unvalidated, a scope field becomes forty spellings of the same three packages within a year, which is worse than no scope because it looks queryable and isn’t. Where the scope names line up with team boundaries they are a shared vocabulary between teams, and that makes them a contract with an owner and a change process (ZFN-47Field Note · currentZFN-47 — Govern the contract between teams, not the code inside themTeams own services end to end; one team owns the gateway that dispatches to them. Govern exactly one thing centrally — the contract at the boundary (schema, identity, errors, idempotency) — and enforce it at runtime. Don't mandate libraries; ship them as an opt-in blueprint.Why it's cited here: Scope names are a shared vocabulary across teams, so they need an owner and a change process like any boundary contract.Open ZFN-47 →) rather than a free-text field.

BREAKING CHANGE: is the part that has to be honest

This is where the format stops being cosmetic. If the release pipeline derives the version from the log, a missing breaking marker does not produce a slightly untidy changelog — it publishes an incompatible change as a patch release, carrying the confident assertion that it is safe to take automatically. That is a worse outcome than choosing versions by hand, because hand-chosen versions fail visibly and in front of a person.

So the breaking marker is the one part of a commit message a reviewer genuinely has to check, and it is not delegable to the linter — no tool can tell from a diff whether a caller’s assumption broke. The body must say what breaks and what a consumer does about it, because that text becomes the migration note in the changelog whether or not anyone writes one later.

Enforce mechanically, never socially

Run commitlintcommitlintA linter that checks commit messages against a configured ruleset, shipped with a Conventional Commits preset. Runs as a `commit-msg` hook for immediate local feedback and as a CI job for enforcement, and can be pointed at a pull request title as well as at individual commits.commitlint.js.org ↗ as a commit-msg hook so authors get the error in the second it happens, and again as a blocking CI check, because hooks are opt-in, aren’t installed on a fresh clone, and don’t run for web edits or for every agent.

Then the step almost everyone gets wrong: if you squash-merge, lint the pull request title. The squashed commit takes its message from the PR title, so a branch of immaculately linted commits is collapsed into one unlinted message and the linted ones are discarded. You end up enforcing the convention on exactly the commits that don’t survive.

A convention enforced by code review is a convention that decays, because it decays one tired reviewer at a time and there is never a good moment to block a merge over a prefix.

Write the format into the agent instructions

Since agents author most commits now, put the type list, the legal scopes, an example, and the breaking-change rule in whatever file the repository uses for agent instructions. It costs ten lines and it is the single highest-compliance intervention available, because a documented format is the one thing a model follows perfectly.

The body is still the whole point

The header tells a reader what kind of change this is. Only the body tells them why, and why is precisely the part that cannot be reconstructed from the diff. State the reason, and state the alternative you rejected — the rejection is usually the more valuable half. Where the reasoning is big enough to have been written down properly, cite the decision record in a footer rather than paraphrasing it (ZFN-1Field Note · currentZFN-1 — Keep engineering decision recordsRecord significant engineering decisions as short, versioned markdown files — context, decision, consequences. Write one for cross-team contracts, directional principles, hard-to-reverse choices, and conventions others must follow. Cite them instead of re-arguing.Why it's cited here: Where the reasoning actually lives — a commit footer cites the decision record rather than re-arguing it in every message.Open ZFN-1 →); the commit should point at the argument, not re-run it. Tim Pope’s postTim Pope — A Note About Git Commit MessagesThe 2008 post that established the conventions git tooling still assumes: a short capitalised summary line in the imperative mood, a blank line, then wrapped prose explaining the motivation for the change and contrasting it with previous behaviour. Predates Conventional Commits and covers the half that Conventional Commits deliberately does not specify — whether the message is any good.tbaggery.com ↗ is still the best short guidance on the prose half, and it predates all of this.

Using an LLM to draft that prose is fine and needs no disclaimer — the obligation is that you read it and can defend it, exactly as with anything else published under your name (ZFN-26Field Note · currentZFN-26 — LLM-assisted content needs no disclaimer, only a human who can back itDrafting engineering content with an LLM needs no disclaimer — inside a team or codebase that already shares the co-sign norm. The obligation is human co-signing. Writing for strangers inverts it: there, stating your policy builds trust rather than diluting a default.Why it's cited here: Why an LLM-drafted commit message needs no disclaimer but does need someone who read it and can defend it.Open ZFN-26 →).

Consequences

Easier:

  • Changelogs and version bumps are derived rather than assembled by hand before a release, which means they stop being a release-day task that gets skipped under pressure.
  • git log --grep becomes an actual query interface. “Every breaking change to this package in the last year” is a one-liner instead of a reading exercise.
  • Reviewers get the author’s claim about intent before reading the diff, which is the right order and frequently the thing that reveals the diff doesn’t match the claim.
  • Regression hunting narrows fast — filtering fix and feat away from chore and docs cuts the search space before you start bisecting.
  • Agents write consistent messages and can answer questions from the history, rather than adding to a pile of prose only a human can skim.

Harder:

  • It is ceremony, and on a one-character typo fix the ceremony genuinely costs more than the change. I pay it because the value is entirely in the consistency, and a convention with defensible exceptions is one that erodes.
  • The type taxonomy invites bikeshedding that produces no value whatsoever. It has to be capped by decree, early, by someone willing to be arbitrary.
  • A format is not a message, and this is the failure mode I’d watch for. A repository can be one hundred percent conventional and still say nothing — chore: updates, fix: fixes, forever. That is arguably worse than obviously-scruffy history, because it looks rigorous, so nobody notices the information isn’t there. The linter can only check the envelope; the body is unenforceable and it’s the part that matters.
  • Derived versions are only as truthful as the breaking markers, and a missed marker produces a confidently wrong release rather than a visible mistake.
  • Retrofitting doesn’t work backwards. History before the adoption date is unparseable, so every tool that reads the log has a start date and you live with the seam permanently.
  • A format an agent follows perfectly says nothing about whether it understood the change it is describing (ZFN-28Open problem · currentZFN-28 — Capability without understanding: brute-force LLM PRsAn open problem: people brute-force PRs with LLMs in domains they don't understand, taking on more than their knowledge supports — and the struggle that used to teach them is smoothed away. How do we stop un-understood code without killing learning or banning a good tool?Why it's cited here: The failure this format cannot fix — a well-formed message on a change nobody understands.Open ZFN-28 →). Well-formed messages on un-understood changes are a real risk, and this note does not address it.

New obligations:

  • CI enforces the format on commits and on pull request titles wherever squash-merging is in use.
  • The scope vocabulary needs an owner and a way to change it, or it silently becomes free text.
  • Reviewers own the breaking-change marker specifically — the one field no linter can verify.
  • The repository’s agent instructions carry the format, and get updated when it changes.
  • Someone has to say, out loud and more than once, that chore: updates passes the linter and still isn’t acceptable.

References

  • Conventional Commits 1.0.0Conventional Commits 1.0.0A lightweight specification for commit messages that adds a small structured header — a type, an optional parenthesised scope, an optional `!` breaking marker, and a description — followed by an optional prose body and git-trailer-style footers including `BREAKING CHANGE:`. It is deliberately short, defines the mapping onto Semantic Versioning, and is designed to be parsed by release tooling.conventionalcommits.org ↗ — the specification itself; short enough to read in full before adopting it.
  • Semantic Versioning 2.0.0Semantic Versioning 2.0.0Defines MAJOR.MINOR.PATCH so a version number carries a compatibility promise: patch for backwards-compatible fixes, minor for backwards-compatible additions, major for anything that breaks a documented contract. Conventional Commits exists largely to let that number be derived from the log rather than chosen by hand.semver.org ↗ — the compatibility promise the commit types exist to derive.
  • Angular commit message guidelinesAngular commit message guidelinesThe in-repo convention Conventional Commits was generalised from, and still the origin of the canonical type vocabulary (feat, fix, docs, refactor, perf, test, build, ci, chore). Worth reading for how a large project actually applies the format day to day, including its rules on scope and on what belongs in the body.github.com ↗ — the origin of the type vocabulary, and a large project’s day-to-day application of it.
  • Tim Pope, A Note About Git Commit MessagesTim Pope — A Note About Git Commit MessagesThe 2008 post that established the conventions git tooling still assumes: a short capitalised summary line in the imperative mood, a blank line, then wrapped prose explaining the motivation for the change and contrasting it with previous behaviour. Predates Conventional Commits and covers the half that Conventional Commits deliberately does not specify — whether the message is any good.tbaggery.com ↗ — the prose half, which Conventional Commits deliberately leaves unspecified.
  • commitlintcommitlintA linter that checks commit messages against a configured ruleset, shipped with a Conventional Commits preset. Runs as a `commit-msg` hook for immediate local feedback and as a CI job for enforcement, and can be pointed at a pull request title as well as at individual commits.commitlint.js.org ↗ — hook and CI enforcement, including against a pull request title.
  • ZFN-30Field Note · currentZFN-30 — Use the standard; don't reinvent the protocolWhen a standard exists for a common or complex problem, use it — don't reinvent the protocol. Standards encode huge adversarial expertise, especially in auth and crypto; a partial implementation beats rolling your own. You're not that special, and your problem isn't either.Why it's cited here: Why to adopt the published commit spec rather than growing a house prefix scheme that means the same thing, badly.Open ZFN-30 → and ZFN-45Field Note · currentZFN-45 — Read the standards; better yet, help write themLearn to read standards docs — RFCs, W3C recs — fluently; they're the primary source, not a last resort. Even better, get involved: reading them well makes you a sharper builder, and helping write them is the best protocol education there is.Why it's cited here: Read the spec itself before adopting it; it is two pages and settles most of the arguments teams have about it.Open ZFN-45 → — a specification already exists for this; use it rather than growing a house dialect.
  • ZFN-51Field Note · currentZFN-51 — Design the request envelope before the first endpointAuth, idempotency keys, trace IDs, vector clocks: context about a request, not part of it. Define an envelope alongside the payload in the schema on day one — transport-native, per-hop vs propagated, owned by generated SDKs and middleware. The retrofit is what costs you.Why it's cited here: The same envelope-versus-payload split, applied to a commit: metadata about the change belongs in defined fields, not smeared into free text.Open ZFN-51 → — the envelope-and-payload split this applies to a commit message.
  • ZFN-1Field Note · currentZFN-1 — Keep engineering decision recordsRecord significant engineering decisions as short, versioned markdown files — context, decision, consequences. Write one for cross-team contracts, directional principles, hard-to-reverse choices, and conventions others must follow. Cite them instead of re-arguing.Why it's cited here: Where the reasoning actually lives — a commit footer cites the decision record rather than re-arguing it in every message.Open ZFN-1 → — where the reasoning lives when it is too big for a commit body; cite it in a footer.
  • ZFN-14Field Note · currentZFN-14 — Define every API with a schema, and generate the clientsDefine every API with a machine-readable schema (OpenAPI, Protobuf, GraphQL) as the source of truth, and generate clients and server stubs from it — never hand-roll request-building and JSON parsing. Hand-written clients drift and break silently; check schema compatibility in CI.Why it's cited here: The same move applied to APIs: define the contract as a schema and generate from it, instead of letting each author improvise the shape.Open ZFN-14 → — the same instinct applied to APIs: define the shape once, generate rather than improvise.
  • ZFN-47Field Note · currentZFN-47 — Govern the contract between teams, not the code inside themTeams own services end to end; one team owns the gateway that dispatches to them. Govern exactly one thing centrally — the contract at the boundary (schema, identity, errors, idempotency) — and enforce it at runtime. Don't mandate libraries; ship them as an opt-in blueprint.Why it's cited here: Scope names are a shared vocabulary across teams, so they need an owner and a change process like any boundary contract.Open ZFN-47 → — why a scope vocabulary shared across teams needs governing.
  • ZFN-26Field Note · currentZFN-26 — LLM-assisted content needs no disclaimer, only a human who can back itDrafting engineering content with an LLM needs no disclaimer — inside a team or codebase that already shares the co-sign norm. The obligation is human co-signing. Writing for strangers inverts it: there, stating your policy builds trust rather than diluting a default.Why it's cited here: Why an LLM-drafted commit message needs no disclaimer but does need someone who read it and can defend it.Open ZFN-26 → — drafting a commit message with an LLM needs no disclaimer, only someone who read it.

Changelog

  • 2026-08-02: First published as a Field Note.