Guide

Commitizen, the Node one and the Python one

Commitizen asks you a few questions and writes the commit message for you. Two separate projects use the name, and search results mix them up. I set both up on test repos, and this page is what they actually did, including the answer the Node prompt asks for and then throws away.

Definition

What Commitizen is

Commitizen is a command-line prompt that asks what kind of change you made and turns your answers into a correctly formatted commit message, so nobody has to remember the format. Use it when you or your contributors want to write conventional commits without learning the rules first. The name covers two separate projects. The first is cz-cli, the npm package commitizen, which you run as cz or git cz. It only writes commits, and the format comes from a plugin it calls an adapter, usually cz-conventional-changelog. The other lives at commitizen-tools/commitizen. It’s a Python package that also installs a command called cz. It writes commits too, then keeps going: cz bump works out the next version from your history, tags it and updates the changelog. Same idea, different GitHub organizations, different config files.

Which one you found

Two tools, one name

cz-cli (Node)commitizen-tools (Python)
Installnpm install --save-dev commitizenpipx install commitizen, Python 3.10+
Write a commitnpx cz, or git cz if installed globallycz commit or cz c
Configconfig.commitizen in package.json, or .czrc.cz.toml, cz.toml, .cz.json, cz.json, .cz.yaml, cz.yaml or pyproject.toml
Checks commitsNocz check
Bumps the version and changelogNocz bump, cz changelog

If your project has a package.json, you almost certainly want the first one. The Python one isn’t only for Python code. Its version providers include npm, cargo and composer. It does need Python on every machine that commits.

Setup

Set up cz-cli in a Node project

The README’s first suggestion is a global install. I’d install it in the project instead, so every contributor runs the same version:

npm install --save-dev commitizen
npx commitizen init cz-conventional-changelog --save-dev --save-exact

Install commitizen first. When I ran init in a fresh repo without it, only the adapter landed in devDependencies. This is what init added to my package.json:

"devDependencies": {
  "commitizen": "^4.3.2",
  "cz-conventional-changelog": "^3.3.0"
},
"config": {
  "commitizen": {
    "path": "./node_modules/cz-conventional-changelog"
  }
}

Two things differ from the README. The path it writes is ./node_modules/cz-conventional-changelog, not the bare package name the README shows. Both resolve, so leave it. And the versions have a caret even though I passed --save-exact. More on that under errors.

Then give people a script to run:

"scripts": {
  "commit": "cz"
}

Stage your files, then npm run commit or npx cz. git cz only works after npm install -g commitizen. One trap from the README: if you still have a precommit npm script, npm runs it before any script named commit, so your hook fires twice. Name the script cm in that case.

If you’d rather plain git commit opened the prompt, the README has a prepare-commit-msg hook for that:

#!/bin/bash
exec < /dev/tty && node_modules/.bin/cz --hook || true

The questions

What the prompt asks and writes

The questions come from the adapter, not from Commitizen. The prompt needs a real terminal, so I read cz-conventional-changelog 3.3.0’s source and then fed its prompt function scripted answers. In order, it asks for:

  • The type, from a list of eleven: feat, fix, docs, style, refactor, perf, test, build, ci, chore and revert. Same list commitlint’s default config allows.
  • An optional scope, which it lower-cases.
  • A short description, with a live character count. The limit is 100 minus the type and scope, so feat(search) gets 86. It lower-cases the first letter and strips any trailing full stop.
  • An optional longer description, which becomes the body.
  • Whether there are breaking changes, and if so, a description of them.
  • Whether it affects open issues, and if so, references like fix #123.

There’s one more. Say yes to breaking changes after leaving the body blank and it asks for a body, since “a BREAKING CHANGE commit requires a body.” I typed “Node 18 is end of life.” and got this:

feat: drop node 18

BREAKING CHANGE: Node 20 or newer is required.

closes #12

My body is gone. The adapter builds the message from the header, the body answer, the breaking note and the issues, and that extra answer isn’t on the list. If you want a body on a breaking change, type it at the longer description question, before it asks about breaking changes.

Notice it marks the break with a BREAKING CHANGE: footer, never a !. That’s the safe choice. semantic-release with no config doesn’t understand ! and releases nothing for it, while release-please and semantic-release both read the footer.

The lower-casing has a side effect. “API keys page shows the real error” came out as fix: aPI keys page shows the real error, and commitlint passed it. You can turn that off in the same config block the adapter reads its options from:

"config": {
  "commitizen": {
    "path": "./node_modules/cz-conventional-changelog",
    "disableSubjectLowerCase": true,
    "maxHeaderWidth": 72
  }
}

With that, the description stays as typed, and maxHeaderWidth: 72 dropped the limit for feat(search) to 58. The catch is that commitlint’s default config then rejects the capital A. I’d leave the lower-casing on and write “the API keys page” instead.

The other one

The Python Commitizen

The README recommends pipx:

pipx install commitizen

I didn’t have pipx, so I used pip in a virtualenv and got 4.19.0. cz init walks you through a config file interactively. It asks for the file, the commit rules, where the version lives, your latest tag, the version scheme, the tag format and whether to update the changelog on bump. It couldn’t run without a real terminal, so I wrote the same keys its source writes to .cz.toml myself:

[tool.commitizen]
name = "cz_conventional_commits"
tag_format = "v$version"
version_scheme = "semver2"
version = "0.1.0"
update_changelog_on_bump = true

I tagged v0.1.0, then committed a feat, a fix and a docs change. One command did the release:

$ cz bump --yes
bump: version 0.1.0 → 0.2.0
tag to create: v0.2.0
increment detected: MINOR

It wrote the new version back into .cz.toml, committed that as bump: version 0.1.0 → 0.2.0, tagged v0.2.0, and started a CHANGELOG.md:

## v0.2.0 (2026-09-23)

### Feat

- **search**: find frogs by pond

### Fix

- stop the double submit

The docs commit is left out. A feat!: commit on 0.2.0 went straight to 1.0.0 in a dry run, because I’d left major_version_zero off. cz init only asks about that when your version starts with 0.

cz commit asks much the same questions as the Node adapter, with two differences. Its type list has nine entries and leaves out chore and revert, though cz check accepts both. And while the prompt says “lower case and no period,” it only strips the period. Here’s cz check failing a message:

$ cz check -m "Fix: Login button"
commit validation: failed!
please enter a commit message in the commitizen format.
commit "": "Fix: Login button
"pattern: (?s)(build|bump|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\(\S+\))?!?: ([^\n\r]+)((\n\n.*)|(\s*))?$

That exits 14. It checks the type and the shape and nothing else, so fix: API keys page. passes, capital and full stop included. The README wires it up as a commit-msg hook through pre-commit or prek.

Troubleshooting

Errors you’ll hit

No files added to staging! Did you forget to run git add? cz-cli won’t open the prompt with nothing staged. Run git add first. The README says git cz passes git commit options through, so git cz -a works too.

git: 'cz' is not a git command. See 'git --help'. You installed Commitizen in the project, and git cz needs it global. Use npx cz or npm run commit.

A previous adapter is already configured. Use --force to override You ran init twice. Add --force to replace the adapter. Watch out in scripts: this error still exited 0 for me.

A caret where you asked for an exact version. No message for this one. --save-exact is silently dropped. The error above prints the install command init builds, and for me it was npm install cz-conventional-changelog --save-dev. The argument parser hands over save-exact, and init looks for saveExact. Pin with npm directly:

npm install --save-dev --save-exact commitizen@4.3.2 cz-conventional-changelog@3.3.0

[NO_COMMITS_TO_BUMP] The commits found are not eligible to be bumped That’s the Python cz bump after only docs or chore commits, and it exits 21, which fails a CI job. Nothing is wrong. Run cz -nr 21 bump and it exits 0 instead. With no commits at all since the tag you get [NO_COMMITS_FOUND] and exit 3.

Pairing

Commitizen and commitlint

Commitizen writes good commits. commitlint rejects bad ones. They do different jobs, and the cz-cli README answers “Commitizen or commit hooks?” with “Both!”

Every message the Node adapter produced with its default settings passed commitlint’s default config in my runs, the breaking change and aPI included. So they don’t fight. The Python cz check is looser than commitlint, as above, so if you want capitals and full stops caught, commitlint is the stricter checker.

If I could only have one, it’s commitlint running in CI. It checks every commit, whoever wrote it and however. Commitizen only helps the people who remember to type npx cz instead of git commit. Add it when you have contributors who don’t know the format and you’d rather they got a menu than a rejection.

The ecosystem

Where it fits with release tools

The Node Commitizen stops at the commit. Something else has to read those commits and cut a release, and that’s release-please if you want a release PR to merge, or semantic-release if you want it to happen on every push.

The Python Commitizen is already a release tool, so it overlaps with both. Pick one thing to own the version number. Two tools tagging the same repo is a mess I wouldn’t want to debug.

Honesty

What this doesn’t fix

Commitizen makes the history tidy, and a tidy history makes a correct changelog. Nobody outside the repo reads either one. The people who’d care about feat(search): find frogs by pond find out when you tell them, and a commit is the wrong voice for that.

That gap is why I built Merge & Tell. It reads each merged pull request, the diff and not just the title, and drafts a post for each network. I read every one before it goes out. The updates page is that running on this product.

The commit is tidy. Nobody outside the repo saw it.

You were going to merge the PR anyway.

Commitizen setup: cz-cli for Node and cz for Python