A clerk records what actually happened. commitclerk reads your staged diff, asks an LLM for a Conventional Commits message, shows it to you, and commits — in a single Python file with no third-party packages.
$ git add . $ clerk --- commit message --- fix: prevent duplicate webhook deliveries on retry - Deduplicate by delivery id before enqueueing, so a provider retry no longer fans out into multiple downstream jobs. - Store the id in the existing idempotency table instead of a new one, keeping the retention policy in a single place. - Log a debug line on the dedupe path to make retry storms visible. ---------------------- [main a1b2c3d] fix: prevent duplicate webhook deliveries on retry
Titles stay imperative and under 72 characters. Bodies stay 2–6 bullets about why — never a file-by-file replay. No emojis, no headers, no code fences.
One downloadable file, no dependencies — you can read the whole thing before letting it near your source code. (The source is a small package; the file is a build of it.)
Standard library only — urllib, subprocess, argparse. Drop the file in and run it, or install it properly. Your call.
A .clerkignore — same syntax as .gitignore — withholds a file's contents: the model gets its name, its line counts and a placeholder. Three sensitive files no longer mean banning the tool everywhere.
--offline writes the message locally — no API call, no key, no model. Type from the file classes, bullets grouped by directory. It never guesses feat: or fix:, so it's a draft, not a replacement.
Stage a .env by accident and the request goes out before the commit exists. Known key shapes and high-entropy tokens stop the run with exit 3 — nothing sent, the file and line named, the secret never printed.
Installs as git clerk too, so it lives where the rest of your git muscle memory already is: git add -A && git clerk.
-m "feat: add X" uses your title verbatim and reduces the model's job to writing the body. The framing of the commit stays yours.
Detects documentation-only commits and refuses to describe already-shipped features as new work — the blind spot every diff-only generator has.
--dry-run prints the message and commits nothing. Look before you leap, every time, at no risk to your history.
OpenAI, Anthropic or a local Ollama model via --provider, any model via --model, and any OpenAI-compatible endpoint via --base-url — LM Studio, vLLM, OpenRouter, Groq, Azure. Cheap by default, stronger when the change is subtle, local when the diff must stay put.
A committed .clerk.json carries the team's provider, model and budgets with the repo, so the convention stops being flags everyone retypes. Flag beats environment beats project file beats your own ~/.config defaults — one order, written down, no surprises.
When a commit adds prose to a CHANGELOG, ROADMAP or README describing a feature that shipped
three commits ago, a diff-only generator reads that prose and announces the feature — for a commit that
changed nothing but Markdown. Your history is now lying to you, and git log --grep and your
release tooling inherit the lie.
…for a commit that only added a paragraph to the changelog. Nothing was implemented. The next release notes will claim otherwise.
Every staged file is documentation, so the prompt switches to a docs-only framing: describe the documentation change itself, never “implement X”.
.md, .mdx, .rst, .txt, .adoc, anything under docs/, or a known name like CHANGELOG / README / ROADMAP — the framing switches to docs:. When docs are mixed with code, the type comes only from the non-documentation lines, so a docstring tweak beside a big changelog entry is still docs:.-m as an override. You know what your change is. Pinning the title reduces the model's job to summarizing the diff underneath it — the recommended default for any commit whose intent isn't obvious from the diff alone.The whole thing is ~230 lines. It's meant to be read, forked, and adapted to your
team's conventions — start with the _RULES string.
Installing gives you three identical entry points: clerk, commitclerk, and git clerk.
| Flag | Default | What it does |
|---|---|---|
-m, --message TITLE | — | Use TITLE verbatim as the commit title; the AI writes only the body bullets. |
--dry-run | off | Print the generated message and exit without committing. |
--provider NAME | openai | openai, anthropic, or ollama (local, no key) — or set $CLERK_PROVIDER. |
--base-url URL | https://api.openai.com/v1 | Any OpenAI-compatible endpoint — Ollama, LM Studio, vLLM, OpenRouter, Groq, Azure — or set $OPENAI_BASE_URL. |
--model MODEL | gpt-4o-mini | Model to call — the provider's default, or set $OPENAI_MODEL. |
--timeout S | 60 | Seconds to wait for each API request. Raise it for a slow local model; transient failures are retried twice with backoff. |
--max-chars N | 60000 | Character budget for the diff. A larger diff is trimmed per file, so every changed file still reaches the model; lockfiles and vendored files collapse to one line first. |
--redact | off | When the pre-flight scan finds a suspected secret, mask it in the request and carry on instead of refusing. The commit still contains it — this protects what is sent, not what is committed. |
--no-scan | off | Skip the pre-flight secret scan entirely. |
--offline | off | Write the message locally — no API call, no key, no network. Never emits feat: or fix:, so treat it as a draft. |
--version | — | Print the version and exit. |
0 | Committed (or --dry-run printed the message). |
1 | Nothing staged — run git add first. |
2 | Configuration problem — the provider's API key is not set, or the provider does not exist. |
3 | A suspected secret in the staged diff — nothing was sent. |
| other | Passed through from git commit. |
Scriptable on purpose: the exit code tells your wrapper exactly what happened.
Three steps, and the third one is just git add.
pipx install commitclerk — or download the single file and run python commitclerk.py. Both work identically.
export OPENAI_API_KEY="sk-…" (on Windows, setx — then reopen the terminal).
git add . then clerk --dry-run to look, clerk to commit.
Install from PyPI:
pipx install commitclerk
Or skip installing entirely — it's one file with no dependencies:
curl -O https://raw.githubusercontent.com/alegauss/commitclerk/main/dist/commitclerk.py
Two wrappers ship with the repo — run-commit.cmd (Windows) and run-commit.sh (macOS / Linux). Both check the API key, stage everything with git add -A, then pass your arguments straight through.
Heads up: they stage everything, including new, deleted and dot-prefixed files. The Python script itself never stages anything on its own.
A .clerkignore keeps chosen files' contents off the wire entirely. Same syntax as
.gitignore, at the repository root. A matched file reaches the model as its path, its
line counts and a placeholder — never its body. The paths themselves are still sent; if a filename
cannot be disclosed either, use --offline.
Nothing is sent until the staged diff has been scanned for secrets. Every added line is
checked against known credential shapes and a high-entropy heuristic before the first request.
A hit stops the run with exit 3 and names the file and line — never the secret itself.
This runs on the diff as staged, before any trimming, so there is no ordering in which something
is sent and then checked.
Your staged diff is sent to the API you configured — api.openai.com by
default, or api.anthropic.com with --provider anthropic. That is the whole point, and it is the one thing to be deliberate about. On a
repository whose contents cannot leave your machine, run --provider ollama —
a local model, no API key, nothing over the network — or don't run the tool there at all.
Check your employer's policy first.
Nothing else is transmitted, stored, or logged by this tool — no telemetry, no analytics,
no remote config. The API key is read from the environment and never written to disk.
Cost is a single Chat Completions call per commit: with the default gpt-4o-mini
and a typical diff, a fraction of a cent.
Keep it dependency-free, keep it one file, and open an issue before a large change. Grab one of these, or propose your own.
prepare-commit-msg hookAn installer so the message is drafted by git itself, wherever you commit from.
Anthropic, Azure OpenAI, and Ollama / local models — without breaking the zero-dependency rule.
--editOpen the generated message in $EDITOR before the commit lands.
clerk --lintValidate an existing message with no API call — usable as a commit-msg hook.
An asciinema recording or GIF for the top of the README. Worth a thousand words.