Release notes for CLI tools and libraries: writing for people who upgrade on purpose
Release notes for a web app describe changes that already happened to the reader — the deploy went out, the button moved, the entry explains it. Release notes for a CLI tool or a library work the other way around: nothing changes until the reader acts. Someone has to run the package-manager command, bump the version in a manifest, merge the update PR. That one difference — upgrades are chosen, not received — changes what the notes are for, when they get read, and what happens when they're wrong. This guide covers the craft for developer tools specifically: what actually counts as your interface, how to write for someone jumping four versions at once, and where your readers really look.
Your reader upgrades on purpose
Three consequences follow from chosen upgrades, and they drive everything else:
- The notes are read at upgrade time, not publish time. Almost nobody reads a library changelog the day it ships. They read it weeks later, in bulk, in the minutes before deciding to bump — or in the minutes after the bump broke the build. Write for that moment of need, not for launch-day applause.
- Bad notes have a price you pay, not them. A reader who gets burned by an undocumented change once will pin your version and stop upgrading. Pinned users are how tools accumulate a long tail of ancient versions you have to support forever — trustworthy notes are cheaper than that.
- The notes are the sales page for the upgrade. “Why should I take any risk to move from 2.3 to 2.6?” is a real question with a default answer of “I shouldn't.” The notes are where you answer it.
The interface is bigger than the docs say
For an HTTP API, the interface is the documented endpoints. For CLIs and libraries the real interface — the set of things whose change can break someone — is much wider than most authors track, because anything observable gets depended on. Treat a change to any of these as changelog-worthy, and a breaking change to any of them as major:
- For a CLI: command and flag names, flag defaults, stdout format (scripts parse it — a reworded line or reordered column breaks pipelines just as hard as a removed flag), exit codes, config-file location and schema, environment variables it reads, and the minimum OS or runtime it runs on.
- For a library: the public API, type signatures (a widened parameter is safe; a narrowed return type is not), error classes and messages people match on, default option values, transitive dependency majors you re-export, and minimum language or platform versions — dropping an old runtime is a breaking change for whoever is still on it, even though not one line of your API moved.
The practical move: write down which of these surfaces you consider stable, which are explicitly not (debug output, internal modules), and hold your own version numbers to that list. The worst release-notes failures in dev tools aren't missing entries — they're changes the author genuinely didn't consider part of the interface.
Write for the version-jumper
Your median reader is not moving from yesterday's release to today's. They're moving from 1.3.2 — pinned eight months ago — to 2.1.0, and they will read (or skim) every entry in between. Three habits make that jump survivable:
- Every entry stands alone. Full flag names, full function names, no “as mentioned last release.” The jumper reads your last eight entries as one document; each must make sense without conversational context.
- Breaking changes carry their migration inline. Old call, new call, one worked example. A jumper crossing three breaking changes does three migrations in one sitting — don't make them chase links across scattered blog posts.
- Majors get a cumulative upgrade guide. A single “upgrading from 1.x” document that consolidates every breaking change since the last major, in the order a migrator should tackle them. The per-release entries stay complete on their own; the guide is the jumper's checklist. Link it from the major's entry and from every 2.x entry after it.
The 30-second upgrade decision
Much of your audience never visits your changelog at all: they meet your release notes inside a dependabot or Renovate PR, embedded in someone else's code review, and decide in half a minute. Structure every entry for that reader: breaking changes first and labeled, then a one-line safety verdict (“safe drop-in upgrade” or “action needed if you use X”), then features, then fixes named by symptom. The open-source changelog guide covers the mechanics of where that text lives; the craft rule is simply that the first two lines decide whether the bump merges today or rots in the PR queue.
Where developer readers actually look
Package managers mostly do not show your release notes. npm's registry page links to your
repo and displays a README; brew upgrade scrolls past in a wall of formulae;
pip and cargo say nothing at all. Nobody sees your notes at install
time unless you put them somewhere findable:
- A canonical URL per release. One permanent page per version that everything else — README badge, registry description, update notice, PR embeds — points at. Feeds and a CHANGELOG.md mirror it for scripts and tarballs.
- CHANGELOG.md ships in the package. It's the only copy that works
offline, greppable in
node_modulesor a vendored checkout, and it survives registry outages and platform moves. - In-tool update notices, used politely. The one-line “v2.1 available → changelog link” pattern (npm's update-notifier popularized it) reaches exactly the right person at exactly the right moment. The etiquette: never block the command, never nag more than once a day, make opting out trivial and documented, and say plainly in the docs that the tool checks a version endpoint — an unexpected network call from a CLI reads as spyware to exactly the audience you serve.
A release entry that does the job
The skeleton, adapted from the general templates for the upgrade-decision reader:
## 2.4.0 — 2026-07-27 Safe drop-in upgrade unless you parse the JSON output of `stats`. ### Breaking - `stats --json` now emits ISO 8601 timestamps (was Unix epoch). Migration: parse dates with your language's ISO parser; epoch output returns with `--time-format=epoch`. ### New - `export --since=TAG` exports only entries after TAG. ### Fixed - `init` no longer fails on read-only home directories (config now honors $XDG_CONFIG_HOME). Minimum supported Node is now 20 (22 recommended).
Note what the skeleton forces: the verdict line exists, breaking changes lead and carry their migration, fixes are named by the symptom a searcher would type, and the runtime-support change is stated rather than buried in a dependency bump.
Anti-patterns
- The dependency-bump wall. Forty lines of “bump lodash from 4.17.20 to 4.17.21” burying one line that matters. Collapse routine bumps to a single line; call out only the majors and anything that changes your minimum versions.
- Breaking changes in a minor. The fastest way to teach users to pin and ignore you. If the surface list above says it can break someone, the version says so too — no exceptions for “but nobody uses that flag.”
- Notes only in the git tag message.
git tag -nis not a publishing channel. Tag messages are invisible to the registry page, the PR embed, and every reader who doesn't have your repo cloned. - Output-format changes as “patch”. If
--quietprints one new line, somebody's cron email just went from silent to daily. stdout is interface. - “Misc fixes and improvements.” In a tool for developers this reads as “we don't know what changed either.” The commit log can draft the list; the author's job is only to curate it.
- Removing things without a deprecation release. A flag should warn for at least one minor before it disappears — the warning in the terminal reaches people no changelog ever will. A written deprecation policy makes the rhythm predictable.
Where Wakelog fits
Wakelog is built by people who ship CLIs, for people who ship CLIs. The
wakelog CLI posts an entry from the end of a release script —
wakelog post --from-git turns the commits since your last tag into a draft you
edit before publishing, and CI can post
drafts on tag push. Every entry gets the permanent URL your update
notice and PR embeds need, the README badge shows “shipped 3d
ago” next to your install instructions, and the changelog.md export
round-trips your whole history back into the repo — canonical page for humans,
CHANGELOG.md in the tarball, one source.
Start your free changelog Next: open source changelogs →
Related guides
- SDK changelogs: shipping one API in seven languages
One canonical API changelog plus per-SDK changelogs, lockstep vs independent versions (and the mapping table you owe), generated-SDK regen walls, parity honesty, and the entry that survives a bump PR. - Framework and runtime release notes: shipping a version an ecosystem has to absorb
When you ship a framework or runtime major, almost nobody can upgrade the day you announce it — apps are stuck until the libraries they depend on move first. Your release notes get read twice, by two different audiences, months apart. How to write for adoption in dependency order: RC notes for library authors, GA notes for app developers, codemods, readiness tables, and the one rule about renaming. - Infrastructure-as-code changelogs: Terraform modules, Helm charts, and changes that touch running systems
Blast-radius versioning, the expected-plan-diff line, moved blocks and values mapping tables, the Helm CRD gap, and release notes that survive the 30-second Renovate-PR review.
Last updated 2026-07-27 · All guides