We standardized our commits with a tiny Rust CLI (and it changed our code reviews)

We standardized our commits with a tiny Rust CLI (and it changed our code reviews)


TL;DR: At Bookcicle we wanted every commit and PR to read like a pro wrote it—concise, conventional, and consistent. We built a small Rust binary, commitmsg, that shells out to git, composes a structured prompt, and calls OpenAI to generate either a Conventional Commit message or a crisp PR description from the actual diff. It’s fast, repeatable, and—most importantly—keeps human intent front and center.

Repo: bookcicle/commitmsg (public on GitHub)


Why we did this

If you’ve ever reviewed code after a long day, you know the pain:

  • “wip”
  • “fix stuff”
  • a 25-line commit body that says… nothing
  • PR descriptions that explain the what but not the why

We wanted a lightweight way to nudge everyone (including me!) toward the same high bar, without adding friction or inventing a new process. The constraints were simple:

  1. Use the diff as truth. Don’t hallucinate filenames or components that aren’t there.
  2. Conventional Commits, every time. Clear headers, focused bullets, optional BREAKING CHANGE footer.
  3. Work locally, work offline-ish, be fast. Rust for CLI ergonomics and speed; plain git under the hood.
  4. No surprises. Diff size caps, streaming optional, and “store” off by default.


What we built

commitmsg is a single binary that does two jobs:

  • Commit mode (default): Generate a strict Conventional Commit message from your staged changes (or --head for the last commit).
  • PR mode (--desc): Generate a clean, skimmable PR description with a Reader’s Map and test notes from a base…HEAD diff.

Under the hood it:

  • Captures the diff + filenames via git (staged, HEAD, or base...HEAD)
  • Truncates safely at a configurable size (default 1024 KB)
  • Sends a tight, rules-first prompt to OpenAI (default model gpt-5)
  • Filters the response:
  • Writes the result to /tmp/commitmsg.txt (or /tmp/pr_description.md) and prints it to stdout

Safety & DX defaults you’ll care about:

  • --store is off (we don’t ask OpenAI to store your interaction)
  • --stream is off (enable for live token streaming)
  • Progress spinners you can disable (--no-progress)
  • Dry-run helper for comfort commits: --dry-run does git commit --dry-run and prompts you to approve


Quickstart

Requires OPENAI_API_KEY and git on your PATH - More installation instructions, and use details in the project Readme.md

Dry Run -- Our Favorite Use

 commitmsg --dry-run        

Full Example:

$commitmsg --dry-run

feat(processor): persist stage on phase gate open and probe fan-out fix
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    
- track open_now to respect successful probe and enable fan-out                                                                                                                                                                                                                                                                                                                                                                                                                     
- update stage in checkpoint store when gate opens for watchdog/UI                                                                                                                                                                                                                                                                                                                                                                                                                  
- warn on stage update failure without aborting workflow                                                                                                                                                                                                                                                                                                                                                                                                                            
- refactor test to seed starter stage and assert stage persistence                                                                                                                                                                                                                                                                                                                                                                                                                  
- add helper to seed task with sections for reuse                                                                                                                                                                                                                                                                                                                                                                                                                                   
- expand test name to cover stage and probe behavior                                                                                                                                                                                                                                                                                                                                                                                                                                
saved → /tmp/commitmsg.txt
--- end of commit message ---
| Running git commit --dry-run…                                                                                                                                                                                                                                                                                                                                                                                                                                                     → git commit --dry-run -F /tmp/commitmsg.txt                                                                                                                                                                                                                                                                                                                                                                                                                                        
/ Running git commit --dry-run…                                                                                                                                                                                                                                                                                                                                                                                                                                                     On branch ab/persist-stage-on-phase-gate-open
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   src/processor/core.rs
        modified:   tests/expanded_ghostwriting_workflow_integration.rs

Approve commit? [y/N] y        

Generate a Conventional Commit from staged changes:

commitmsg --strict        

Use the last commit (HEAD) instead of staged:

commitmsg --head        

Generate a PR description (auto-detects base like origin/main → main → master):

commitmsg --desc        

Be explicit about the range/base:

commitmsg --desc --range main...HEAD
# or
commitmsg --desc --base origin/main        

Turn on streaming + bump reasoning effort/verbosity (when diffs are gnarly):

commitmsg --stream --effort high --verbosity high        

Examples Real, Generated Commits:

feat(healers): add watchdog throttle/marking and ghostwriting nudge
- introduce wd_should_throttle and wd_mark with cooldown tracking
- throttle and mark actions in QC, export, aggregation, analytics flows
- add ghostwriting narrative nudge for expanded sections
- include watchdog fields in scans and streamline updates
- export/qc messaging enriched with stable options and keys
- minor refactors for cloning keys and utility visibility        
ci(workflow): add env input and deploy-test job; refine triggers (#29)
- add workflow_dispatch input to select dev or test environment
- gate deploy-dev on target_env=dev for manual runs
- introduce deploy-test job with ECR build/push and CFN deploy
- clean up redundant comments and blank lines in workflow
- update CloudFormation test AllowedOrigins to test.bookcicle.com        

What the tool enforces (so you don’t have to)

  • Commit header rules: type(scope): subject — ≤72 chars, imperative mood, no period. type ∈ { feat, fix, refactor, docs, test, chore, perf, build, ci }
  • Bullets: 3–6 crisp lines, action-oriented, fact-based.
  • No fluff: it won’t include components or files not present in the diff.
  • Optional footers: BREAKING CHANGE: and named “Footers:” supported in lenient mode.


Where it fits in your Git strategy

This has been surprisingly neutral and helpful across styles. Here’s how we use it (and what we recommend):

1) Linear history (rebase & merge)

  • Every commit on the main line is a tiny, parseable unit.
  • commitmsg shines here: conventional headers become a living changelog.
  • Release tooling can derive notes by type (feat, fix, etc.).

Recommendation: Use --strict for atomic commits. Enforce fast-forward merges and Conventional Commits in CI.

2) Squash-and-merge (clean main, messy branches) - our favorite

  • Many teams prefer a single commit on main per PR.
  • commitmsg --desc ensures the PR body is excellent and can be used as the squash commit message.
  • Developers still benefit from commitmsg locally to keep branch history readable.

Recommendation: Make PR descriptions the source of truth. Set your platform to “Use PR title & description” on squash.

3) Merge commits (the “graph tells the story” crowd)

  • commitmsg still standardizes each commit and improves PR bodies.
  • Consider using --strict on commits that land near integration points.

Bottom line: We default to linear on main for signal, but commitmsg delivers value for any merge policy—as long as you pick one and stick to it.


A peek at the prompt (commit mode)

We keep it short and bossy:

  • Output only the commit message
  • Header format + constraints
  • 3–6 bullets, imperative, concrete
  • Optional BREAKING CHANGE
  • Files actually changed (from the diff) for context
  • Then the diff (truncated safely if needed)

That’s it. No essays, no fluff.


What changed for us

  • Faster reviews: Reviewers spend brain cycles on code, not deciphering intent.
  • Fewer nits: The style discussion disappears; the message is consistent.
  • Better release notes: Conventional headers map cleanly to change types.
  • Onboarding win: New engineers “sound senior” on day one.

(We didn’t gold-plate with vanity metrics; the qualitative lift was obvious within a sprint.)


Try it & tell us what breaks

  • Works wherever git works.
  • Defaults are conservative (no storing, 1MB diff cap).
  • Flags are there when you need more “reasoning” on hairy diffs.

If you want to kick the tires, check out bookcicle/commitmsg on GitHub and open an issue or PR. We’re already exploring pre-commit hooks, CI guards for Conventional Commit headers, and optional release-note generation.

Happy shipping—and happy reading those commit messages. 🧹✨

To view or add a comment, sign in

More articles by Andrew Barraford

Explore content categories