Introduction

cgen is a CLI tool that generates git commit messages from your staged diff using an LLM. It ships as a single ~3 MB executable with no language runtime to install. The executable is cgen; the crates.io package is auto-commit-rs.

How it works

Running cgen in a repository with staged changes walks through a short pipeline:

  1. Inspect the staging area — staged files are listed up front; files excluded from the LLM payload are marked (not sent to LLM).
  2. Filter the diff — binary, generated, and lockfile-style paths are dropped from the payload by default (diff exclusion patterns), and sensitive paths or secret-looking content block the request entirely (safety & workflow controls).
  3. Generate — the filtered diff is sent to the configured provider. If the call fails transiently, cgen can fall back through your saved presets automatically (fallback order).
  4. Review — the proposed message is shown for you to accept, regenerate, edit in your editor, or cancel (configurable via ACR_REVIEW_COMMIT).
  5. Commit and push — the commit is created via git commit, and post-commit push behavior follows your ACR_POST_COMMIT_PUSH setting.

Beyond generation, cgen can rewrite past commit messages (cgen alter), undo the latest commit safely (cgen undo), create semantic version tags (--tag), browse previously generated commits (cgen history), and update itself (cgen update).

Why Rust?

Tools like opencommit do the same thing but require Node.js and weigh in at ~100MB of node_modules. cgen is a roughly 3MB self-contained executable. GNU/Linux release builds use the platform C library; a musl artifact is also published for portable x86_64 Linux installs.

cgenopencommit
Install size~2 MB~100 MB
Runtime depsNoneNode.js
Startup timeInstant~300ms (Node cold start)
Generation time~800ms~4s
DistributionSingle binarynpm install

Reading this book

⚠️ Disclaimer: AI Generation and Code Quality

The majority of the code in this repository was generated by agentic AI. To ensure quality and stability:

  • Human oversight — every pull request, architecture decision, and code block generated by the AI is reviewed and refined by a human developer.
  • Testing — the codebase is supported by comprehensive unit tests, a CI coverage gate, strict lints, and cross-platform test runs.
  • Use at your own risk — this software is provided "as is", without warranty of any kind.

Installation

Linux / macOS (curl)

curl -fsSL https://raw.githubusercontent.com/gtkacz/smart-commit-rs/main/scripts/install.sh | bash

This detects your OS and architecture, downloads the latest release binary to /usr/local/bin, and makes it executable. Set INSTALL_DIR to change the target:

INSTALL_DIR=~/.local/bin curl -fsSL https://raw.githubusercontent.com/gtkacz/smart-commit-rs/main/scripts/install.sh | bash

Windows (PowerShell)

irm https://raw.githubusercontent.com/gtkacz/smart-commit-rs/main/scripts/install.ps1 | iex

This downloads the latest release to %LOCALAPPDATA%\cgen\ and adds it to your user PATH.

Cargo

From crates.io:

cargo install auto-commit-rs

From git:

cargo install --git https://github.com/gtkacz/smart-commit-rs

Manual Download

Grab a binary from the Releases page and place it somewhere in your PATH.

Available binaries:

  • cgen-linux-amd64, Linux x86_64
  • cgen-linux-amd64-musl, portable Linux x86_64 (static musl build, works on any distro)
  • cgen-linux-arm64, Linux ARM64
  • cgen-macos-amd64, macOS Intel
  • cgen-macos-arm64, macOS Apple Silicon
  • cgen-windows-amd64.exe, Windows x86_64

Release artifacts ship with a checksums.sha256 file for verification.

Verify

cgen --version

Then continue with the Quick Start.

Quick Start

1. Get an API key

cgen defaults to Groq, which has a free tier. Any other built-in provider works the same way — and local providers (Ollama, LM Studio) need no key at all.

2. Configure

Either run the interactive editor and set API Key (and optionally Provider/Model) under the Basic group:

cgen config

or export the key in your shell:

export ACR_API_KEY=your-key-here

cgen config saves to the global config file, so this is a one-time step. See Config File & Locations for per-repository overrides.

3. Stage and generate

git add .
cgen

cgen lists the staged files (marking any that are excluded from the LLM payload), sends the filtered diff to your provider, and shows the proposed message with a review menu:

  • Accept — create the commit with this message
  • Regenerate — ask the LLM for a new message
  • Edit — open the message in your editor before committing
  • Cancel — abort; nothing is committed

After the commit, cgen asks whether to push (change this with ACR_POST_COMMIT_PUSH=never|ask|always). On first run it also asks once whether to enable automatic updates.

Not ready to commit? cgen --dry-run prints the generated message without creating a commit.

Next steps

  • Usage — all commands and flags
  • Configuration — every setting, message language, gitmoji, templates
  • Presets — save and switch between provider setups

Usage

Generating a commit

cgen with no subcommand generates a message from the staged diff and creates the commit (after review, if enabled). Any arguments that are not cgen flags are forwarded directly to git commit:

cgen                    # generate message and commit
cgen --no-verify        # forwarded: git commit --no-verify
cgen -S                 # forwarded: git commit -S (signed)

Flags

All flags are global: they may appear before or after a subcommand, and the diff/override flags also apply to cgen alter.

FlagEffect
-a, --allRun git add --update first, staging tracked modifications and deletions but never untracked files
--stdoutWrite exactly one final, templated message and a newline; never commit, push, tag, update, review, or track history
-g N, --generate NGenerate N independently validated candidates and choose one (counts above five require confirmation)
-p TEXT, --prompt TEXTAdd invocation-only content/style guidance without overriding format, locale, or safety rules
--dry-runGenerate and print the message without committing (with alter: without rewriting)
--verbosePrint the final system prompt sent to the LLM (never the diff payload)
--tagCreate the next semantic version tag after a successful commit (details)
--set KEY=VALUEOverride any setting for this run only, repeatable (details)
--diff-include GLOBForce-include matching files in the LLM diff, repeatable (details)
--diff-exclude GLOBExclude additional files from the LLM diff this run, repeatable
--allow-large-diffAllow a payload over the ACR_MAX_DIFF_BYTES budget
--allow-sensitiveAllow a diff flagged by the sensitive-data guard

Always quote globs so your shell does not expand them: --diff-include "*.xml".

--stdout is explicit: redirecting normal output does not enable it. It works for staged generation and cgen --stdout alter <hash>. It conflicts with --dry-run, --verbose, --tag, forwarded commit arguments, and candidate counts other than one. Diagnostics go to stderr, while the sensitive-content and diff-size guards remain active.

When N > 1, --generate N requires an interactive terminal. Regeneration replaces the entire candidate set. After choosing, the normal review menu still appears when review is enabled; otherwise the selected candidate is committed immediately.

Subcommands

cgen config

Open the interactive configuration editor. Inside a git repo it asks whether to edit local (.env) or global settings; outside a repo it opens the global config directly. The menu supports searching (/), per-setting descriptions (?), and managing presets and fallback order.

cgen alter <hash> / cgen alter <old> <new>

Regenerate a past commit's message and rewrite it. With one hash, the message is generated from that commit's own diff. With two hashes, the old..new net diff is used as LLM input and only the <new> commit's message is rewritten. Rewriting already-pushed commits requires explicit confirmation; see safety & workflow controls.

cgen undo

Undo the latest commit with a soft reset, keeping its changes staged. Warns before undoing pushed commits and never pushes anything itself.

cgen history

Browse commits previously generated by cgen for the current repository (or pick a repository when run outside one). Requires ACR_TRACK_GENERATED_COMMITS=1 (the default). See Commit History.

cgen preset / cgen fallback

Manage saved provider presets and the fallback order directly — the same UIs available from the cgen config menu.

cgen model

Discover live models for the configured provider, search the returned catalog, and save only the selected model locally or globally. The current and provider default models are placed first. If discovery is unsupported, times out, or fails, cgen explains the problem and offers the current/default values plus manual entry. Cancellation writes nothing.

Perplexity uses manual model entry because its current model-list endpoint describes a different API surface. Custom OpenAI-compatible providers support discovery when ACR_API_URL has a recognizable /chat/completions suffix.

cgen hook install|uninstall|status

Manage the current repository's prepare-commit-msg integration. Installation uses Git's effective hooks directory, including core.hooksPath and worktrees. If an existing hook is present, cgen backs it up and runs it first. Uninstall only removes a recognized cgen wrapper and restores that exact backup.

The hook generates only for normal commits whose message is still empty apart from Git comments. Existing-hook failures stop the commit; model or generation failures produce a warning and let Git continue.

cgen prompt

Print the full LLM system prompt assembled from the current config, without making any LLM call or git operation. Useful for inspecting the effect of ACR_LLM_SYSTEM_PROMPT, gitmoji, locale, and one-liner settings.

cgen update

Update cgen to the latest release, using the install method that produced the running binary. See Updating.

Config File & Locations

All settings use the ACR_ prefix. Layered resolution is defaults → global TOML → local .env → process environment → CLI --set (highest priority, this run only). A project .env is a sparse overlay: it may contain only the keys that project overrides, and every absent key continues to inherit from global config/defaults. Saving local config preserves unrelated variables and comments; choosing "Inherit global value" removes that local assignment.

ACR_AUTO_UPDATE is the one global-only setting: it is never written to local .env files and cannot be overridden with --set.

Settings Reference

VariableDefaultDescription
ACR_PROVIDERgroqLLM provider (groq, openai, anthropic, gemini, grok, deepseek, openrouter, mistral, together, fireworks, perplexity, lm_studio, ollama, or custom)
ACR_MODELllama-3.3-70b-versatileModel name
ACR_API_KEYunsetAPI key (required by cloud providers)
ACR_API_URLautoAPI endpoint (auto-resolved from provider)
ACR_API_HEADERSautoCustom headers (Key: Value, Key2: Value2)
ACR_LOCALEenCommit message language
ACR_ONE_LINER1Single-line commits (1/0)
ACR_COMMIT_TEMPLATE$msgTemplate, $msg is replaced with LLM output
ACR_LLM_SYSTEM_PROMPT(built-in)Base system prompt
ACR_USE_GITMOJI0Enable gitmoji (1/0)
ACR_GITMOJI_FORMATunicodeGitmoji style (unicode/shortcode)
ACR_REVIEW_COMMIT1Review message before committing (1/0)
ACR_POST_COMMIT_PUSHaskPost-commit push behavior (never/ask/always)
ACR_SUPPRESS_TOOL_OUTPUT0Suppress git subprocess output (1/0)
ACR_WARN_STAGED_FILES_ENABLED1Warn when staged file count exceeds threshold (1/0)
ACR_WARN_STAGED_FILES_THRESHOLD20Staged files warning threshold (warn when count is greater)
ACR_WARN_LLM_FILES_ENABLED1Warn when the count of files sent to the LLM exceeds threshold (1/0)
ACR_WARN_LLM_FILES_THRESHOLD20LLM-analyzed files warning threshold (warn when count is greater)
ACR_CONFIRM_NEW_VERSION1Ask before creating the computed --tag version (1/0)
ACR_AUTO_UPDATEunsetEnable automatic updates (1/0); prompts on first run if unset
ACR_FALLBACK_ENABLED1Try fallback presets when primary LLM fails (1/0)
ACR_TRACK_GENERATED_COMMITS1Track AI-generated commits per repository (1/0)
ACR_DIFF_EXCLUDE_GLOBS(see Diff Exclusion Patterns)Comma-separated glob patterns for files to exclude from LLM analysis
ACR_MAX_DIFF_BYTES200000Maximum filtered diff bytes accepted without --allow-large-diff
ACR_MAX_OUTPUT_TOKENS512Maximum tokens the LLM may generate for one commit message
ACR_SENSITIVE_FILE_GLOBS.env,.env.*,*.pem,*.key,id_rsa,id_ed25519,*credentials*,*secrets*Paths that require --allow-sensitive before LLM analysis

Config Locations

  • Global: ~/.config/cgen/config.toml (Linux), ~/Library/Application Support/cgen/config.toml (macOS), %APPDATA%\cgen\config.toml (Windows)
  • Local: .env in git repo root

The cgen config editor

cgen config edits these files interactively. Inside a git repo it asks whether to edit local or global settings; outside a repo it opens the global config directly. The menu groups settings, supports Show descriptions [?] (inline help for each setting) and Search settings [/] (auto-expands matching groups), and includes entries for managing presets and fallback order. Changing the provider automatically resets the model to that provider's default.

Variable Interpolation

ACR_API_URL and ACR_API_HEADERS support $VARIABLE interpolation from environment variables:

ACR_API_URL=https://api.example.com/v1/$ACR_MODEL/chat
ACR_API_HEADERS=Authorization: Bearer $ACR_API_KEY, X-Custom: $MY_HEADER

Missing variables are errors. Interpolation never mutates the process environment. Header overrides may use the legacy comma-separated form above or a JSON object with string values.

Per-Invocation Overrides

Any setting can be overridden for a single run with --set KEY=VALUE (repeatable). Overrides apply on top of all other layers and are never persisted:

cgen --set model=gpt-4o --set one_liner=false
cgen --set provider=ollama        # try a different provider just this once

Keys are the setting names from the configuration table (case-insensitive; - and _ are interchangeable, e.g. one-liner). Every setting is overridable except auto_update (a persistent global preference). Unknown keys are rejected with the list of valid keys.

To refine which files are sent to the LLM for one run, use --diff-include/--diff-exclude (see Diff Exclusion Patterns).

Use --prompt TEXT (or -p) for additive guidance that should not become persistent configuration:

cgen --prompt "emphasize the compatibility impact on plugin authors"
cgen alter HEAD~2 --prompt "describe the migration path in the body"

Runtime guidance is delimited in the system prompt before cgen's mandatory Conventional Commit, locale, output-only, and safety instructions. It can refine content and style but cannot override those rules.

Generation flags are global and may appear before or after a subcommand. Always quote globs so your shell does not expand them: --diff-include "*.xml".

Diff Exclusion Patterns

ACR_DIFF_EXCLUDE_GLOBS filters files from the diff sent to the LLM while still committing them. This reduces noise and token usage for binary, generated, or data files. Default patterns:

*.json, *.xml, *.csv, *.pdf, *.lock, *.svg, *.png, *.jpg, *.jpeg, *.gif, *.ico, *.woff, *.woff2, *.ttf, *.eot, *.min.js, *.min.css

Override with a comma-separated list:

export ACR_DIFF_EXCLUDE_GLOBS="*.lock,*.svg,package-lock.json"

For a single run, adjust the effective filter without touching your config:

cgen --diff-include "*.xml"   # send .xml files to the LLM even though they're excluded
cgen --diff-exclude "*.sql"   # additionally drop .sql files from the LLM diff

--diff-include wins over any exclude pattern (allow-over-deny). Patterns with a slash match repository-relative paths; basename patterns such as *.lock match at any depth. Invalid patterns and filters that remove every changed file are errors. The same filters apply to normal generation and cgen alter.

Excluded files are still committed — the patterns only control what the LLM sees. To keep files out of the commit itself, don't stage them.

Safety & Workflow Controls

cgen is deliberately conservative: nothing is sent to a provider or written to history without either a default guard or an explicit override.

Pre-flight checks

Before generating, cgen prints the staged file count and names; files excluded from the LLM payload are marked (not sent to LLM). If staged files exceed ACR_WARN_STAGED_FILES_THRESHOLD, or the files actually sent to the LLM exceed ACR_WARN_LLM_FILES_THRESHOLD, cgen asks one merged confirmation (including the payload size in KB) before continuing. Each check can be disabled with its _ENABLED flag.

Sensitive data guard

Sensitive filenames (ACR_SENSITIVE_FILE_GLOBS, which defaults to .env files, private keys, and credential-like paths) and high-confidence credential patterns in the diff content are blocked before any provider request. Use --allow-sensitive only after reviewing the exact staged diff.

Large diff guard

Filtered diffs above ACR_MAX_DIFF_BYTES (default 200 KB) are blocked before any provider request; --allow-large-diff is an explicit one-run override.

Inspecting without acting

  • cgen --dry-run generates and prints the final commit message but does not create a commit.
  • cgen alter --dry-run generates and prints the rewritten message but does not rewrite history.
  • cgen --verbose prints the final system prompt sent to the LLM and never prints the diff payload.
  • cgen prompt prints the full LLM system prompt (based on current config) without running any LLM call or git operations.

Pushing

After a real commit, push behavior follows ACR_POST_COMMIT_PUSH:

  • never: never push
  • ask: prompt whether to push (default)
  • always: push automatically

Semantic version tags (--tag)

cgen --tag creates a semantic version tag after a successful commit:

  • no existing tag → 0.1.0
  • latest semver tag x.y.zx.(y+1).0
  • latest tag not in semantic versioning → error

If ACR_CONFIRM_NEW_VERSION=1 (default), cgen asks before creating the computed tag; if 0, it creates it directly. The tag is pushed explicitly after a successful branch push; partial failures report that the tag remains local.

Rewriting history (cgen alter)

  • cgen alter <hash> regenerates that commit's message from its own diff; cgen alter <old> <new> uses the old..new net diff as LLM input and rewrites only the <new> commit message.
  • If the target commit is already pushed, cgen requires explicit confirmation before rewriting.
  • For rewritten pushed history, cgen offers a separate, default-No git push --force-with-lease action; it never performs an unguarded force push.

Undoing (cgen undo)

cgen undo only undoes the latest commit, keeps its changes staged (including a repository's root commit), never pushes, and warns before undoing pushed commits.

Updating

cgen update

cgen update updates the installation that launched it:

  • Cargo installations run cargo install auto-commit-rs --version <release> --locked --force.
  • Release installations download the platform artifact and checksums.sha256, verify the SHA-256, then atomically replace (or, on Windows, schedule replacement of) that executable.

Automatic updates

On every run, cgen checks the latest GitHub release tag against the current version. The first time cgen runs, it asks whether to enable automatic updates and saves the preference to the global config (ACR_AUTO_UPDATE is global-only and never written to a project .env).

  • ACR_AUTO_UPDATE=1: cgen updates itself automatically when a newer version is found.
  • ACR_AUTO_UPDATE=0 (or unset after the prompt): a notice with the available version is shown at the end of the output instead.

Built-in Providers

Built-in providers work with just ACR_PROVIDER and (for cloud providers) ACR_API_KEY — the endpoint URL, request format, and headers are resolved automatically. Selecting a provider in cgen config also sets its default model, which you can override with ACR_MODEL.

Run cgen model (or edit Model inside cgen config) to fetch the provider's current model catalog, search it, and persist a selection. Discovery uses short timeouts and bounded responses, authenticates with the configured credentials, and filters non-generation models when the provider exposes that metadata. OpenAI, Anthropic, Gemini, Groq, Grok, DeepSeek, OpenRouter, Mistral, Together, Fireworks, LM Studio, and Ollama support live discovery. Perplexity uses the manual fallback because its current list endpoint is for a different API surface.

ProviderDefault ModelAPI key
groq (default)llama-3.3-70b-versatilerequired
openaigpt-4o-minirequired
anthropicclaude-sonnet-4-20250514required
geminigemini-2.0-flashrequired
grokgrok-3required
deepseekdeepseek-chatrequired
openrouteropenai/gpt-4o-minirequired
mistralmistral-small-latestrequired
togethermeta-llama/Llama-3.3-70B-Instruct-Turborequired
fireworksaccounts/fireworks/models/llama-v3p3-70b-instructrequired
perplexitysonarrequired
lm_studioqwen/qwen3.5-35b-a3bnot needed (local)
ollamallama3not needed (local)

lm_studio and ollama talk to a locally running server, so commit generation stays entirely on your machine.

Custom providers

Set ACR_PROVIDER to any other name and provide ACR_API_URL. Custom providers default to the OpenAI-compatible request format, and both the URL and ACR_API_HEADERS support variable interpolation:

export ACR_PROVIDER=vllm
export ACR_API_URL=http://localhost:8000/v1/chat/completions
export ACR_MODEL=meta-llama/Llama-3-8B

For custom providers, cgen model derives /models only when the configured URL ends in the recognizable /chat/completions path. Other endpoint shapes fall back to current/default/manual selection rather than guessing a URL.

To make a provider available to everyone as a built-in, see Adding a New Default Provider — it's usually a 5-line change.

Presets

A preset is a named snapshot of the five provider-related settings: provider, model, API key, API URL, and API headers. Presets make it cheap to switch between setups (say, a fast local Ollama model and a cloud model for tricky diffs) and they feed the fallback mechanism, which walks your presets when the primary provider fails.

Manage presets from the cgen config interactive menu, or directly with cgen preset:

  • Save current as preset: saves the current provider/model/key/url/headers as a named preset (offered only when no identical preset exists)
  • Load a preset: applies a saved preset to the current config session; if you then modify a loaded preset's fields, cgen offers to update the preset on save
  • Manage presets: create, rename, duplicate, delete, export, and import presets
  • Export/Import: export presets as TOML (optionally redacting API keys) for sharing or backup

Presets are stored in {config_dir}/cgen/presets.toml alongside the global config. Deduplication uses (provider, model, api_key, api_url) as the key.

Fallback Order

When ACR_FALLBACK_ENABLED=1 (default) and the primary LLM call has a transient failure, cgen tries your saved presets as fallbacks in a configurable order, so a rate-limited or briefly unavailable provider doesn't block the commit. All attempts share one total 120-second deadline.

  • Configure the order from the cgen config menu under "Configure fallback order...", or directly with cgen fallback
  • Presets matching the current config are skipped
  • Transport failures and HTTP 408/409/425/429/5xx may fall back
  • Authentication, invalid-request, configuration, and response-format errors stop immediately — retrying another provider can't fix those
  • A summary of all failures is shown if every provider fails

Prompt design

Rationale for the base prompts assembled in src/prompt.rs (build_system_prompt, build_user_prompt) and src/config.rs (DEFAULT_SYSTEM_PROMPT). Grounded in current vendor guidance and the empirical literature on LLM commit-message generation (reviewed July 2026). Revisit when models or the cited guidance change materially.

Principles applied

1. Constraint-focused instructions beat minimal prompts

An empirical study of commit-message generation via in-context learning (arXiv:2502.18904) compared four prompts and found the best performer was the one with explicit output constraints ("do not write explanations, reply with only the commit message"), with prompt choice mattering most in zero-shot settings — the regime this CLI runs in against small local models. The system prompt therefore states hard constraints (raw message only, exact header shape, one-line mode precedence) rather than relying on the model to infer them.

2. Few-shot examples steer format more reliably than rules

Anthropic's prompting guidance calls examples "one of the most reliable ways to steer output format"; the ICL study above measured double-digit metric gains from demonstrations; Google's prompt-engineering whitepaper recommends always including them. build_system_prompt injects a small <examples> block chosen to match the active output mode:

  • plain + one-liner → single-line headers only (CONVENTIONAL_EXAMPLES_ONE_LINER)
  • plain + full → one bare header plus one header-with-body (CONVENTIONAL_EXAMPLES_FULL)
  • gitmoji → the emoji header examples embedded in the gitmoji spec

Examples are selected per mode because a mismatched demonstration (e.g. a body example in one-liner mode, or an emoji-less example in gitmoji mode) is a stronger mis-steer than a missing rule — models copy patterns before they follow prose.

3. Delimit variable data; restate the task after it

OpenAI's guidance separates instructions from data with delimiters; Anthropic recommends wrapping variable input in XML-style tags and, for long inputs, placing the query after the data (measured up to ~30% quality improvement); the "lost in the middle" result (Liu et al., 2023) puts critical instructions first and last, never mid-prompt. build_user_prompt wraps the diff in a <diff> block and restates the task plus the raw-output constraint after it, so the last thing the model reads before generating is the output contract. The system prompt ends with the same closing constraints for the short-diff case.

4. The diff is data, not instructions

Diff content is untrusted: a staged README edit can contain imperative sentences. The default system prompt says to treat <diff> content strictly as data to describe — the standard mitigation for indirect prompt injection in data-processing prompts.

5. Anti-fabrication rules target known CMG failure modes

The empirical study arXiv:2404.14824 identifies the recurring failure modes of LLM-generated commit messages: fabricated or missing "why" (the diff rarely contains motivation), vague descriptions ("update code"), and omitted essentials. The prompts respond directly: "why" is requested only when the diff makes it evident, footers may only state facts the diff supports (the previous prompt's Reviewed-by: Name example actively invited hallucinated trailers), and the closing instruction demands concrete component names over generic phrases.

6. No contradictory instructions; explicit precedence

OpenAI's GPT-5 prompting guide stresses that contradictory prompt instructions measurably degrade strong instruction-followers. The one-liner rule used to coexist silently with "Body: OPTIONAL"; it now states "even where the rules above allow them", and the locale rule pins header tokens (type, scope, BREAKING CHANGE, gitmoji shortcodes) to English so translation cannot break the validate_commit_message regex, which only accepts ASCII lowercase types.

7. Motivate constraints

Anthropic's guidance: explaining why a constraint exists improves adherence ("Claude is smart enough to generalize from the explanation"). The raw-output rule carries its reason — "because your reply is passed verbatim to git commit".

8. Imperative mood, not "present tense"

"Use present tense" permits "adds login" ("adds" is present tense). Git and Conventional Commits convention is the imperative mood, so the prompt spells out the contrast: "add", not "added" or "adds".

9. Structure and economy

Anthropic's context-engineering guidance recommends delimited sections and the minimal set of information that fully specifies behavior; practitioner guidance converges on short, sectioned prompts (attention cost grows with length, and long prompts are harder to debug). The assembled system prompt stays a few hundred tokens — deliberate headroom for the small-context local models this tool supports via Ollama/LM Studio. Static content (system prompt) precedes variable content (diff), which is also the cache-friendly ordering for providers with prompt caching.

10. Self-correction on validation failure

Feedback-driven retry (Self-Refine / Reflexion-style) is the standard recovery for structured-output misses: instead of failing hard when validate_commit_message rejects the model's output, provider::generate_validated_message sends one corrective turn built by build_correction_prompt — the diff again, the rejected attempt in <previous_attempt> tags, the validator error in <error> tags, and the task restated last. One retry captures most format failures (the common case for small local models); a second rarely converges and doubles cost, so failure after the corrective turn is surfaced as an error.

11. Default-prompt upgrades reach existing configs

Config files persist llm_system_prompt verbatim, which would pin users who never customized it to whatever default text their config was first written with. base_prompt_is_default treats a blank value — the new persisted default — or any retired shipped default (whitespace-normalized comparison against LEGACY_SYSTEM_PROMPTS) as "not customized" and substitutes the current DEFAULT_SYSTEM_PROMPT at assembly time. Genuinely customized prompts are left untouched. When DEFAULT_SYSTEM_PROMPT changes, the outgoing text must be appended to LEGACY_SYSTEM_PROMPTS.

12. Deterministic decoding

Commit generation wants reproducibility, not creativity. OpenAI-compatible and Gemini bodies already pinned temperature: 0; the Anthropic body now does too (it previously inherited the API default of 1.0).

Sources

Commit History

When ACR_TRACK_GENERATED_COMMITS=1 (default), cgen records each AI-generated commit hash and message preview in a per-repository cache, so you can later tell which commits were machine-written and inspect them.

  • cgen history inside a git repo shows that repo's tracked commits
  • cgen history outside a git repo lists all tracked repos, then shows commits for the selected one
  • Selecting a commit runs git show on it

The cache is stored in {config_dir}/cgen/cache/, is concurrency-safe, deduplicates hashes rewritten by cgen alter, and retains the latest 200 entries per repository. Set ACR_TRACK_GENERATED_COMMITS=0 to disable tracking entirely.

Contributing

Contributing to cgen

First off, thank you for considering contributing to cgen! Every contribution helps — whether it's a bug report, a new provider, documentation improvement, or a feature implementation.

Table of Contents

Getting Started

  1. Fork the repository on GitHub
  2. Clone your fork locally:
    git clone https://github.com/YOUR_USERNAME/smart-commit-rs.git
    cd smart-commit-rs
    
  3. Add the upstream remote:
    git remote add upstream https://github.com/gtkacz/smart-commit-rs.git
    

Development Setup

Prerequisites

  • Rust (stable toolchain, 1.85+)
  • Git

Build & Run

# Debug build (fast compilation, slower binary)
cargo build

# Run directly
cargo run

# Run with arguments
cargo run -- config

# Release build (slow compilation, optimized binary)
cargo build --release

# Run tests
cargo test

# Check for warnings without building
cargo check

# Format code
cargo fmt

# Lint
cargo clippy

Testing Locally

To test the full flow you need a valid API key for at least one provider:

# Set a key for testing
export ACR_API_KEY=your-test-key

# Stage some changes and run
git add some_file
cargo run

To test the interactive config menu:

cargo run -- config          # Inside a repo: choose local (.env) or global (TOML)
cargo run -- config          # Outside a repo: opens global TOML directly

To test new commit workflow controls:

# Dry run (should NOT create a commit)
cargo run -- --dry-run

# Undo latest commit (soft reset, keeps changes staged)
cargo run -- undo

Suggested manual smoke checks:

  • ACR_POST_COMMIT_PUSH=never|ask|always behavior after commit creation
  • ACR_WARN_STAGED_FILES_ENABLED=1 + low ACR_WARN_STAGED_FILES_THRESHOLD to trigger confirmation
  • ACR_SUPPRESS_TOOL_OUTPUT=1 to confirm git output is hidden
  • undo on unpushed commit
  • undo on pushed commit (should warn and require confirmation)

Automated quality gates (same checks as CI):

# Full test suite
cargo test --locked

# Coverage gate for core logic
cargo llvm-cov --locked --lib --tests \
  --ignore-filename-regex '(main|cli|preset|update|cache|ui)\.rs' \
  --summary-only \
  --fail-under-lines 90

# Required formatting and lint gates
cargo fmt --all --check
cargo clippy --locked --all-targets --all-features -- -D warnings

Project Structure

src/
├── main.rs              # Entry point, CLI dispatch, main flow
├── cli.rs               # clap derive definitions + interactive config menu (inquire)
├── config.rs            # AppConfig struct, layered resolution, TOML/env I/O
├── persistence.rs       # Locked, owner-only atomic persistence
├── provider.rs          # Provider registry, API adapters, HTTP call, response parsing
├── prompt.rs            # System prompt assembly and final-message validation
├── git.rs               # Git operations, path-aware filtering, diff inspection
├── interpolation.rs     # Non-mutating $VAR template engine for URL/headers
├── editor.rs            # Shell-free external editor launcher
├── preset.rs            # Presets and fallback configuration
├── cache.rs             # Bounded per-repository generated-commit history
├── update.rs            # Provenance-aware, checksum-verified updater
└── workflow.rs          # Testable pre-provider workflow policy

.github/workflows/
├── test.yml             # Cross-platform test, quality, MSRV, audit, coverage
└── release.yml          # Locked multi-platform release builds and checksums

Design principles:

  • One file = one concern. No nested module directories.
  • Synchronous only — no async runtime (ureq instead of reqwest+tokio).
  • Minimal dependencies — every crate must justify its inclusion by binary size or maintenance burden.
  • std::process::Command for git operations — no git2/gitoxide for 3 shell commands.

Adding a New Default Provider

This is one of the easiest and most valuable ways to contribute. A default provider means users can just set ACR_PROVIDER=provider_name and ACR_API_KEY=... without needing to configure the URL or headers manually.

Step-by-step

  1. Open src/provider.rs and find the get_provider() function.

  2. Add a new match arm with the provider's API details:

    #![allow(unused)]
    fn main() {
    "your_provider" => Some(ProviderDef {
        api_url: "https://api.example.com/v1/chat/completions",
        api_headers: "Authorization: Bearer $ACR_API_KEY",
        default_model: "your-model",
        format: RequestFormat::OpenAiCompat, // or Gemini, Anthropic, LmStudio
        response_path: "choices.0.message.content",
    }),
    }
  3. Choose the right RequestFormat:

    • OpenAiCompat — Most providers use this (OpenAI-compatible chat completions). Request body: { model, messages: [{role, content}], max_tokens, temperature }.
    • Gemini — Google's format with system_instruction and contents arrays.
    • Anthropic — Similar to OpenAI but with system as a top-level string field.
    • LmStudio — LM Studio chat endpoint format. Request body: { model, system_prompt, input }.

    If the provider uses a completely different format, you may need to add a new variant to RequestFormat and a matching arm in build_request_body().

  4. Set response_path — this is a dot-separated path to the generated text in the JSON response. For example:

    • OpenAI-compatible: choices.0.message.content
    • Gemini: candidates.0.content.parts.0.text
    • LM Studio: output (the parser selects the item where type == "message" and returns its content)
    • Use numbers for array indices: results.0.text
  5. URL/header interpolation — you can use $ACR_API_KEY, $ACR_MODEL, or any environment variable in the api_url and api_headers strings. They get expanded at runtime.

  6. Update src/cli.rs — add the provider name to the choices list in the "PROVIDER" match arm of interactive_config():

    #![allow(unused)]
    fn main() {
    "PROVIDER" => {
        let choices = vec!["gemini", "openai", "anthropic", "your_provider", "(custom)"];
        // ...
    }
    }
  7. Update the README — add the provider to the "Built-in providers" line in the Providers section.

  8. Test it — if you have access to the provider's API, verify the full flow works. If not, mention this in your PR and someone will test it before merging.

Example: Adding Mistral

#![allow(unused)]
fn main() {
"mistral" => Some(ProviderDef {
    api_url: "https://api.mistral.ai/v1/chat/completions",
    api_headers: "Authorization: Bearer $ACR_API_KEY",
    default_model: "mistral-small-latest",
    format: RequestFormat::OpenAiCompat,
    response_path: "choices.0.message.content",
}),
}

Example: Adding LM Studio

#![allow(unused)]
fn main() {
"lm_studio" => Some(ProviderDef {
    api_url: "http://localhost:1234/api/v1/chat",
    api_headers: "Content-Type: application/json",
    default_model: "qwen/qwen3.5-35b-a3b",
    format: RequestFormat::LmStudio,
    response_path: "output",
}),
}

That's it — most OpenAI-compatible providers are short additions, while custom payload APIs (like LM Studio) need a dedicated request/response format branch.

Making Changes

  1. Create a feature branch from main:

    git checkout main
    git pull upstream main
    git checkout -b feature/your-feature-name
    
  2. Make your changes — keep commits focused and atomic.

  3. Ensure quality:

    cargo fmt --all --check
    cargo clippy --locked --all-targets --all-features -- -D warnings
    cargo test --locked
    cargo build --locked
    
  4. Commit with a descriptive message following Conventional Commits or just use cgen ;) :

    feat(provider): add Mistral as default provider
    fix(config): handle missing .env gracefully
    docs: add Mistral to provider list in README
    

Pull Request Process

  1. Push your branch to your fork:

    git push origin feature/your-feature-name
    
  2. Open a Pull Request against main on the upstream repository.

  3. In the PR description, include:

    • What the change does and why
    • How to test it (if applicable)
    • Screenshots for UI changes (config menu, spinner, etc.)
  4. Keep it small — one concern per PR. A provider addition + a bug fix should be two separate PRs.

  5. Be responsive — if changes are requested, push follow-up commits to the same branch.

What makes a good PR

  • Follows the existing code patterns and style
  • Doesn't introduce new dependencies without justification
  • Keeps the binary small (check cargo build --release size)
  • Includes documentation updates when behavior changes
  • Has a clear, concise title using conventional commit format

Code Style

  • Format: Always run cargo fmt before committing.
  • Lints: Fix all cargo clippy warnings.
  • Error handling: Use anyhow for errors. Use .context("description") to add context to errors. Use bail!() for early returns.
  • No unwrap() in production code — use ? or .context() instead. unwrap() is acceptable only in cases where failure is truly impossible (e.g., compiling a hardcoded regex).
  • Dependencies: Prefer crates that are lightweight and well-maintained. Always consider binary size impact. If a feature can be done in 20 lines of code, don't add a crate for it.
  • Comments: Only where the "why" isn't obvious from the code. No doc comments on private internals unless they're complex.

Reporting Bugs

Open an issue with:

  • cgen version (cgen --version)
  • OS and architecture (e.g., Windows 11 x64, macOS ARM)
  • What you expected vs what happened
  • Steps to reproduce — the minimum commands to trigger the bug
  • Error output — full terminal output including the error message

Suggesting Features

Open an issue with:

  • What problem it solves — describe the use case, not just the solution
  • Proposed behavior — how it would work from the user's perspective
  • Alternatives considered — other ways you thought about solving it

License

By contributing, you agree that your contributions will be licensed under the MIT License.

Changelog

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[1.5.0] - 2026-07-28

Added

  • --all / -a staging of tracked modifications and deletions before generation
  • Exact machine-readable output with --stdout, including non-rewriting alter
  • Sequential multi-candidate generation and selection with --generate N / -g N
  • Invocation-only prompt guidance with --prompt TEXT / -p TEXT
  • Repository-local prepare-commit-msg management through cgen hook
  • Authenticated live model discovery and searchable selection through cgen model and the configuration editor

[1.4.1] - 2026-07-23

Changed

  • Updated all Rust dependencies, including the ureq 3, toml 1.1, sha2 0.11, inquire 0.9, colored 3, and shlex 2 major releases
  • Raised the MSRV to Rust 1.85, the minimum required by the updated dependency graph
  • Updated GitHub Actions to their current supported major releases

Fixed

  • Try fallback presets when a provider rejects a request as too large (HTTP 413)
  • Include the path and line number for sensitive-content findings, and ignore the documented your-key-here API-key placeholder

[1.4.0] - 2026-07-23

Added

  • Sparse project .env overrides with an interactive “Inherit global value” action
  • Configurable diff byte/sensitive-path guards and explicit --allow-large-diff / --allow-sensitive overrides
  • Linux ARM64 and x86_64 musl release artifacts with SHA-256 manifests
  • Rust 1.74 MSRV declaration and CI gate
  • One-shot self-correction: when the LLM returns an invalid commit message, the validator error and rejected attempt are fed back to the model for a corrective retry before failing
  • ACR_WARN_LLM_FILES_ENABLED / ACR_WARN_LLM_FILES_THRESHOLD: warn on the count of files actually sent to the LLM (the token-relevant subset), merged with the staged-files warning into a single confirmation that reports the payload size
  • Staged-file listing marks files excluded from the LLM payload with (not sent to LLM)
  • ACR_MAX_OUTPUT_TOKENS to configure the LLM completion token cap (default 512, previously hard-coded)

Changed

  • Local .env updates preserve unrelated variables, comments, and absent inherited settings
  • Configuration parsing rejects invalid booleans, integers, push modes, gitmoji formats, templates, locales, headers, and globs
  • Provider fallbacks share one total deadline and run only for classified transient failures
  • Preset/config/cache writes are owner-only, lock-protected, and atomically replaced
  • History cache entries use stable identifiers, deduplicate rewrites, and retain the latest 200 commits
  • Self-update targets the current installation, pins the release version, and verifies checksums before replacement
  • Release CI builds with --locked, validates the requested tag, publishes checksums, and gates crates.io publication behind the release
  • CI now enforces formatting, strict Clippy, cross-platform tests, MSRV, dependency audit, installer syntax, and aligned coverage
  • Locked dependencies use patched TLS, error-handling, randomness, and test-serialization releases; terminal UI features avoid unnecessary unmaintained/yanked transitive paths while preserving Rust 1.74 support
  • Rewrote the base LLM prompts around current prompt-engineering research: enumerated commit types, imperative-mood and header-length rules, anti-fabrication constraints, mode-matched few-shot examples, and explicit one-liner precedence
  • Staged diffs are sent wrapped in <diff> tags as data to describe, with the task restated after the diff
  • A blank ACR_LLM_SYSTEM_PROMPT now selects the built-in default prompt, and configs still storing a retired shipped default are upgraded to the current default automatically
  • The config view shows (built-in default) for an uncustomized system prompt instead of the stored text
  • Anthropic requests pin temperature to 0, matching the other providers
  • Conventional Commit shape is enforced on LLM output only; templated and manually edited messages receive structural validation (empty/NUL)
  • Staged-file warnings are evaluated after diff filtering, so hard safety gates run before any confirmation prompt
  • cgen prompt and cgen undo no longer perform the startup update check

Fixed

  • Partial global TOML files no longer turn omitted default-true booleans off
  • Unicode API keys/prompts no longer panic during masking or truncation
  • Interpolation no longer mutates process environment or silently removes missing variables
  • Confirmation cancellation can no longer select an affirmative default
  • LM Studio receives the complete system instruction
  • Empty, malformed, fully excluded, oversized, or sensitive diffs are stopped before provider calls
  • Generated, regenerated, templated, and manually edited commit messages are validated before Git receives them
  • Non-HEAD/root rewrites record the rewritten commit identity rather than HEAD
  • Root-commit undo keeps changes staged
  • Pushed rewrites use an explicit force-with-lease path and created tags are pushed explicitly
  • Selected-repository history uses that repository for git show
  • Concurrent preset/cache writes no longer silently overwrite one another
  • The updater uses the published auto-commit-rs crate rather than a nonexistent package
  • Non-English locales no longer instruct the LLM to translate Conventional Commit types, which produced headers that failed message validation
  • Fallback presets that differ from the active configuration only by API headers are attempted instead of silently skipped

Removed

  • High-MSRV edit dependency; editor launching is handled internally
  • Unused enabled flag inside the presets file's [fallback] section (existing files containing it still parse)

[1.3.2] - 2026-06-25

Added

  • --set KEY=VALUE flag to override any setting for a single run (ephemeral, never persisted); auto_update excepted
  • --diff-include GLOB / --diff-exclude GLOB flags to refine which files are sent to the LLM for a single run (allow-over-deny precedence)

[1.3.1] - 2026-04-09

Fixed

  • Fixed oLLaMa streaming content issue

[1.3.0] - 2026-04-06

Added

  • Added oLLaMa as a built-in provider

[1.2.2] - 2026-03-05

Fixed

  • Fixed prompt issue where smaller LLMs weren't grasping single-line commits

[1.2.1] - 2026-03-02

Fixed

  • Gitmoji spec no longer overrides the Conventional Commit spec
  • When editing a LLM-generated message you start editing from the existing message

[1.2.0] - 2026-03-02

Added

  • ACR_DIFF_EXCLUDE_GLOBS configuration: exclude files from LLM analysis by glob pattern while still committing them
  • Default exclusion patterns for common binary/generated files: *.json, *.xml, *.csv, *.pdf, *.lock, images, fonts, minified assets
  • Seven new built-in LLM providers: Grok, DeepSeek, OpenRouter, Mistral, Together, Fireworks, Perplexity
  • LLM presets: save, load, rename, duplicate, delete, export/import reusable provider configurations via cgen config
  • Fallback order: automatic retry with alternate LLM presets when the primary provider returns an HTTP error
  • ACR_FALLBACK_ENABLED configuration flag (default: enabled) to toggle LLM fallback behavior
  • Per-repository commit cache: track which commits were AI-generated
  • cgen history subcommand to browse AI-generated commits per repository (with git show integration)
  • ACR_TRACK_GENERATED_COMMITS configuration flag (default: enabled) to toggle commit tracking
  • Preset management menu in cgen config (save current as preset, load preset, manage presets, configure fallback order)
  • Preset change tracking: warns when loaded preset fields are modified and offers to update on save
  • Export/import presets as TOML (with optional API key redaction)
  • cgen preset standalone subcommand to manage LLM presets directly
  • cgen fallback standalone subcommand to configure fallback order directly
  • Config view: "Show descriptions [?]" toggle to display help text for each setting
  • Config view: "Search settings [/]" to find settings by name (auto-expands matching groups)
  • Config view: improved color variance with bright white for groups, bright cyan for subgroups

Changed

  • ACR_AUTO_UPDATE is now a global-only setting and will not be written to local .env files
  • call_llm now uses call_llm_with_fallback internally, enabling automatic provider retry
  • generate_final_message reports which fallback preset was used (if any)
  • Config menu now includes preset and fallback management entries
  • All (y/N) confirmation prompts replaced with interactive Select menus showing "Yes"/"No" options
  • Config view: selected item header now strips tree-drawing characters for cleaner display
  • Preset management: restructured menu - select a preset first via "Manage existing preset...", then choose action (Rename/Duplicate/Delete)

Fixed

  • Cursor no longer resets to top of view when collapsing headers on the cgen config view

[1.1.0] - 2026-02-24

Added

  • cgen update subcommand to manually update to the latest version
  • ACR_AUTO_UPDATE configuration flag (defaults to unset; prompts on first run)
  • Automatic version checking against GitHub releases on every run
  • Auto-update support when ACR_AUTO_UPDATE=1 (updates silently before proceeding)
  • Update warning displayed at the end of output when a newer version is available and auto-update is off
  • cgen prompt subcommand to print the LLM system prompt without running anything
  • cgen config now auto-detects git repo: prompts for global vs local scope inside a repo, opens global directly outside one

Changed

  • Staged files display now uses tree-style characters (├──, └──) instead of bullet points
  • Boolean config fields display "enabled"/"disabled" instead of "1 (yes)"/"0 (no)" in the interactive config UI
  • Interactive config groups settings into collapsible tree sections (Basic expanded, Advanced collapsed with subgroups)
  • cgen config --global flag removed; scope selection is now interactive when inside a git repo

[1.0.0] - 2026-02-23

  • Initial release of the tool