@mihairo/cmt
v1.2.0
Published
Zero-dependency conventional commits CLI — interactive picker, linter, and git hook installer. One bash script, works in any repo.
Maintainers
Readme
cmt
Conventional Commits CLI — zero dependencies, one bash script.
Install
npm (any project — no Node required at runtime)
npm install -g @mihairo/cmtHomebrew
brew tap mihairo/tap
brew install cmtcurl (no package manager)
curl -fsSL https://raw.githubusercontent.com/mihai-ro/cmt/main/cmt \
-o ~/.local/bin/cmt && chmod +x ~/.local/bin/cmtUsage
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
versionSet 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 formatGuided interactive commit:
git add .
git commit # triggers the interactive picker automatically
# or:
cmt commit # run directlyLint 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
doneCommit 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 JWTBuilt-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 --lintBoth 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 clonelint-staged
Add to .husky/pre-commit:
npx lint-stagedcmt 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
doneGit 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
