Skip to content

Repository files navigation

cmt

Conventional Commits CLI — a single native binary. Linux, macOS, Windows.

npm Conventional Commits License: MIT


Install

curl (Linux / macOS)

curl -fsSL https://raw.githubusercontent.com/mihai-ro/cmt/main/install.sh | sh

Windows (PowerShell)

irm https://raw.githubusercontent.com/mihai-ro/cmt/main/install.ps1 | iex

npm (any platform — no Node at runtime)

npm install -g @mihairo/cmt

pnpm

pnpm blocks package install scripts by default, and cmt downloads its binary in a postinstall step. After installing, approve the build:

pnpm add -g @mihairo/cmt
pnpm approve-builds -g        # select @mihairo/cmt, then confirm

For a project dependency, allowlist it in package.json instead:

{
  "pnpm": {
    "onlyBuiltDependencies": ["@mihairo/cmt"]
  }
}

Homebrew

brew tap mihairo/tap
brew install cmt

Usage

cmt <command> [flags]

  init [--husky] [--lint]   create .cmt.json + install git hook(s)
  commit                     interactive commit builder
  lint [file]                lint a message file or stdin  →  exit 1 on error
  log [n]                    pretty log of last n commits  (default: 20)
  types                      list available commit types
  uninstall                  remove cmt-managed hooks and .cmt.json
  version

Set up a repo:

cd my-project
cmt init                 # picker hook only
cmt init --lint          # picker + lint git commit -m "..." commits
cmt init --husky         # husky v9 format  (.husky/prepare-commit-msg)
cmt init --husky --lint  # both hooks, husky format

Guided interactive commit:

git add .
git commit     # triggers the interactive picker automatically
# or:
cmt commit     # run directly

Lint from anywhere:

echo "feat(api): add login" | cmt lint          # exit 0
echo "bad message"          | cmt lint          # exit 1

# lint every commit on a branch (CI)
git log --format="%s" origin/main..HEAD | while IFS= read -r msg; do
  echo "$msg" | cmt lint || exit 1
done

Commit format

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

Breaking change:

feat!: drop support for Node 14
feat(api)!: redesign endpoints
BREAKING CHANGE: /auth now returns JWT

Built-in types

Type Emoji SemVer impact
feat minor
fix 🐛 patch
docs 📚
style 💅
refactor ♻️
perf patch
test 🧪
build 🏗️
ci 🔧
chore 🔩
revert patch

Configuration — .cmt.json

cmt init creates .cmt.json at your repo root with a $schema pointer. VS Code, JetBrains, and any JSON Language Server will autocomplete and validate it automatically — no extension needed.

{
  "$schema": "https://raw.githubusercontent.com/mihai-ro/cmt/main/schema/cmt.schema.json",
  "customTypes": [
    {
      "type": "wip",
      "emoji": "🚧",
      "semver": "none",
      "description": "Work in progress"
    },
    {
      "type": "security",
      "emoji": "🔒",
      "semver": "patch",
      "description": "Security fix"
    }
  ],
  "scopes": ["auth", "api", "ui", "db"],
  "rules": {
    "maxHeaderLength": 72,
    "requireScope": false,
    "allowBreakingChanges": ["feat", "fix"],
    "disallowUpperCaseDescription": false,
    "disallowTrailingPeriod": false
  }
}

Scopes — when the scopes array is non-empty, cmt commit shows an arrow-key picker with your configured scopes, a "custom" option for free-text entry, and a "skip" option. Leave scopes empty to always use free-text input.

Commit .cmt.json — your whole team shares the same types, scopes, and rules.


Hooks

prepare-commit-msg (always installed)

Intercepts plain git commit, runs the interactive picker, and writes the message — so git never opens its editor. Skips amends, merges, squashes, and any commit that already has a source message.

commit-msg (opt-in via --lint)

Lints the final commit message. Catches git commit -m "...", --amend, and commits from GUI tools:

cmt init --lint

Both hooks follow the same append/create pattern — if a hook file already exists they append a clearly-marked block rather than overwriting it. cmt uninstall removes only the cmt blocks, leaving any other content intact.


Integrations

Husky v9

cmt init --husky --lint    # writes .husky/prepare-commit-msg + .husky/commit-msg
git add .husky/            # commit them — every teammate gets them on clone

lint-staged

Add to .husky/pre-commit:

npx lint-staged

cmt handles commit message linting separately — the two hooks are completely independent.

GitHub Actions

- name: Lint commit messages
  run: |
    git log --format="%s" origin/main..HEAD | while IFS= read -r msg; do
      echo "$msg" | cmt lint || exit 1
    done

Git operations and hook behaviour

Operation Hook fires? Result
git commit picker runs
git commit -m "..." ✅ with --lint linted
git commit --amend ✅ with --lint linted
git merge --no-ff ⏭ skipped (auto-generated message)
git revert ⏭ skipped (auto-generated message)
fixup! / squash! ⏭ skipped
git pull --rebase ✅ passes (replays existing commits)
Empty/aborted commit ⏭ skipped

Lint rules

Rule Config key Default On fail
Header format type(scope)?: description required ❌ error
Valid type required ❌ error
Non-empty description required ❌ error
Blank line before body required ❌ error
Scope required requireScope false ❌ error
Uppercase description disallowUpperCaseDescription false ⚠️ / ❌
Trailing period disallowTrailingPeriod false ⚠️ / ❌
Header length maxHeaderLength 72 ⚠️ warning

Warnings exit 0. Errors exit 1.


Why not commitlint + husky + commitizen?

cmt commitlint + husky commitizen
Runtime dependencies 0 ~15 npm packages Python + pip
Works in any language repo ❌ needs Node ❌ needs Python
Install copy one file npm install pip install
Interactive commit prompt via cz-commitlint
Commit-msg linting ✅ opt-in --lint
Husky v9 support native via config
JSON Schema / intellisense partial
Custom types + scopes .cmt.json

License

MIT

About

Zero-dependency conventional commits CLI - interactive picker, linter, and git hook installer. One bash script, works in any repo.

Topics

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages