The Keep a Changelog format, explained
Keep a Changelog is the closest thing CHANGELOG.md has to a standard. It's a markdown convention, not a spec with a validator, which is exactly why half the changelogs in the wild get it subtly wrong. This is a practical reference: the structure, the rules that matter, and the mistakes that break both human readers and the tools that parse these files.
The skeleton
# Changelog All notable changes to this project will be documented in this file. ## [Unreleased] ## [1.4.0] - 2026-07-20 ### Added - CSV export on all report pages ### Fixed - Timezone handling for weekly digests sent near midnight UTC ## [1.3.1] - 2026-06-02 ### Security - Upgraded jsonwebtoken to patch CVE-2026-XXXX
Four structural rules make this format work:
- One
##heading per version, newest first, in the form## [version] - YYYY-MM-DD. The brackets are convention (they become links, below); the ISO date is not optional. An undated release is a rumor. ###headings are categories, not versions. Only six exist: Added, Changed, Deprecated, Removed, Fixed, Security. Omit empty ones; don't invent new ones ("Misc", "Chores") β that's where commit noise sneaks in.- Entries are bullets under a category, written for users, not a pasted commit log.
- An
[Unreleased]section sits at the top. It's the staging area: changes land there as they merge, and cutting a release is renaming the heading to a version + date and starting a fresh empty Unreleased.
What the six categories actually mean
- Added β new features, new endpoints, new config options.
- Changed β existing behavior that's different now. Breaking changes live here (or in Removed), flagged loudly: many projects prefix the bullet with Breaking:.
- Deprecated β still works, will be removed; say when and what to use instead. This category is the whole reason upgrade paths can be graceful (see writing a deprecation policy).
- Removed β gone in this release. Every Removed entry should have appeared under Deprecated in an earlier one.
- Fixed β bugs. Name the symptom users saw, not the internal cause.
- Security β vulnerabilities patched. Never bury these in Fixed; scanners and security teams grep for this heading specifically. (When you also need a full advisory, see security advisories vs changelog.)
Version links (the part everyone skips)
The bracketed versions are meant to be markdown reference links to diffs, defined at the bottom of the file:
[Unreleased]: https://github.com/you/proj/compare/v1.4.0...HEAD [1.4.0]: https://github.com/you/proj/compare/v1.3.1...v1.4.0 [1.3.1]: https://github.com/you/proj/releases/tag/v1.3.1
This turns every version heading into a clickable "show me the actual code" link. Tedious to maintain by hand β which is why it's usually the release script's job.
How it pairs with semantic versioning
The format is version-scheme-agnostic, but it pairs naturally with SemVer (explained in plain English here): anything under Removed or a breaking Changed means a major bump, Added means at least a minor, only-Fixed/Security can be a patch. If your categories and your version bumps disagree, one of them is lying.
Mistakes that break parsers (and readers)
Plenty of tools read CHANGELOG.md: release automation, package registries, hosted changelog importers. These are the failure modes we see most when parsing real-world files:
- Versions at the wrong heading level β versions as
#or###, or categories promoted to##. The h2/h3 split is the only structure the format has; respect it. - Creative dates β "July 20th", "20/07/2026", "2026-7-20". Use
YYYY-MM-DD, zero-padded. It's unambiguous in every locale and sorts as text. - Prose between the h1 and the first version that contains its own headings β parsers can mistake them for releases. Keep the preamble to plain paragraphs.
- Duplicate version headings after a messy merge. Every version appears exactly once.
- An Unreleased section that never empties β if entries pile up there across several releases, your release script isn't rotating it.
When this format is the wrong tool
Keep a Changelog assumes versioned releases. If you ship a SaaS continuously, there are no versions for users to reason about β forcing "## [2026-07-20]" pseudo-versions works, but a dated stream of human entries (what most product changelogs are) fits better. The categories still earn their keep as tags: new / improved / fix / security map cleanly onto Added / Changed / Fixed / Security. See changelog vs release notes for that split.
Where Wakelog fits
Wakelog speaks this format in both directions. Paste any keepachangelog-style file (or a GitHub repo URL) and it becomes a hosted changelog page β versions become dated entries, categories fold into the body, [Unreleased] is skipped. And everything you post can be exported back as a keepachangelog-style CHANGELOG.md anytime, so you're never locked in. Wondering how much history to import? See starting a changelog for an existing product. The importer also survives the real-world quirks above β mixed heading levels, human dates, setext headings β because we tested it on the changelogs of axios, Express, and friends.
Preview your CHANGELOG.md β no signup Next: how to write a changelog β
Related guides
- Open source changelogs: CHANGELOG.md, GitHub Releases, or both?
Your changelog gets read in the 30 seconds someone spends deciding whether to merge a dependency bump. Where it should live and how to write for that moment. - Generate a changelog from git commits (without publishing garbage)
git log one-liners, Conventional Commits, git-cliff & friends β and the one rule that keeps auto-generated changelogs from being unreadable. - WordPress plugin changelogs: making readme.txt work for you
Your changelog is the only thing between a site owner and the Update button. The readme.txt format, the Upgrade Notice trick, and the habits that earn prompt updates.
Last updated 2026-07-28 Β· All guides