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:
- Inspect the staging area — staged files are listed up front; files
excluded from the LLM payload are marked
(not sent to LLM). - 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).
- 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).
- Review — the proposed message is shown for you to accept, regenerate,
edit in your editor, or cancel (configurable via
ACR_REVIEW_COMMIT). - Commit and push — the commit is created via
git commit, and post-commit push behavior follows yourACR_POST_COMMIT_PUSHsetting.
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.
| cgen | opencommit | |
|---|---|---|
| Install size | ~2 MB | ~100 MB |
| Runtime deps | None | Node.js |
| Startup time | Instant | ~300ms (Node cold start) |
| Generation time | ~800ms | ~4s |
| Distribution | Single binary | npm install |
Reading this book
- New users: start with Installation and the Quick Start.
- Usage is the full command and flag reference.
- The Configuration chapters document every setting, starting from the complete settings table.
- The LLM Providers chapters cover built-in providers, presets, and fallback.
- The Internals chapters explain how the prompt is designed and how commit history tracking works.
⚠️ 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_64cgen-linux-amd64-musl, portable Linux x86_64 (static musl build, works on any distro)cgen-linux-arm64, Linux ARM64cgen-macos-amd64, macOS Intelcgen-macos-arm64, macOS Apple Siliconcgen-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.
| Flag | Effect |
|---|---|
-a, --all | Run git add --update first, staging tracked modifications and deletions but never untracked files |
--stdout | Write exactly one final, templated message and a newline; never commit, push, tag, update, review, or track history |
-g N, --generate N | Generate N independently validated candidates and choose one (counts above five require confirmation) |
-p TEXT, --prompt TEXT | Add invocation-only content/style guidance without overriding format, locale, or safety rules |
--dry-run | Generate and print the message without committing (with alter: without rewriting) |
--verbose | Print the final system prompt sent to the LLM (never the diff payload) |
--tag | Create the next semantic version tag after a successful commit (details) |
--set KEY=VALUE | Override any setting for this run only, repeatable (details) |
--diff-include GLOB | Force-include matching files in the LLM diff, repeatable (details) |
--diff-exclude GLOB | Exclude additional files from the LLM diff this run, repeatable |
--allow-large-diff | Allow a payload over the ACR_MAX_DIFF_BYTES budget |
--allow-sensitive | Allow 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
| Variable | Default | Description |
|---|---|---|
ACR_PROVIDER | groq | LLM provider (groq, openai, anthropic, gemini, grok, deepseek, openrouter, mistral, together, fireworks, perplexity, lm_studio, ollama, or custom) |
ACR_MODEL | llama-3.3-70b-versatile | Model name |
ACR_API_KEY | unset | API key (required by cloud providers) |
ACR_API_URL | auto | API endpoint (auto-resolved from provider) |
ACR_API_HEADERS | auto | Custom headers (Key: Value, Key2: Value2) |
ACR_LOCALE | en | Commit message language |
ACR_ONE_LINER | 1 | Single-line commits (1/0) |
ACR_COMMIT_TEMPLATE | $msg | Template, $msg is replaced with LLM output |
ACR_LLM_SYSTEM_PROMPT | (built-in) | Base system prompt |
ACR_USE_GITMOJI | 0 | Enable gitmoji (1/0) |
ACR_GITMOJI_FORMAT | unicode | Gitmoji style (unicode/shortcode) |
ACR_REVIEW_COMMIT | 1 | Review message before committing (1/0) |
ACR_POST_COMMIT_PUSH | ask | Post-commit push behavior (never/ask/always) |
ACR_SUPPRESS_TOOL_OUTPUT | 0 | Suppress git subprocess output (1/0) |
ACR_WARN_STAGED_FILES_ENABLED | 1 | Warn when staged file count exceeds threshold (1/0) |
ACR_WARN_STAGED_FILES_THRESHOLD | 20 | Staged files warning threshold (warn when count is greater) |
ACR_WARN_LLM_FILES_ENABLED | 1 | Warn when the count of files sent to the LLM exceeds threshold (1/0) |
ACR_WARN_LLM_FILES_THRESHOLD | 20 | LLM-analyzed files warning threshold (warn when count is greater) |
ACR_CONFIRM_NEW_VERSION | 1 | Ask before creating the computed --tag version (1/0) |
ACR_AUTO_UPDATE | unset | Enable automatic updates (1/0); prompts on first run if unset |
ACR_FALLBACK_ENABLED | 1 | Try fallback presets when primary LLM fails (1/0) |
ACR_TRACK_GENERATED_COMMITS | 1 | Track 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_BYTES | 200000 | Maximum filtered diff bytes accepted without --allow-large-diff |
ACR_MAX_OUTPUT_TOKENS | 512 | Maximum 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:
.envin 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-rungenerates and prints the final commit message but does not create a commit.cgen alter --dry-rungenerates and prints the rewritten message but does not rewrite history.cgen --verboseprints the final system prompt sent to the LLM and never prints the diff payload.cgen promptprints 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 pushask: 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.z→x.(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 theold..newnet 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-leaseaction; 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.
| Provider | Default Model | API key |
|---|---|---|
groq (default) | llama-3.3-70b-versatile | required |
openai | gpt-4o-mini | required |
anthropic | claude-sonnet-4-20250514 | required |
gemini | gemini-2.0-flash | required |
grok | grok-3 | required |
deepseek | deepseek-chat | required |
openrouter | openai/gpt-4o-mini | required |
mistral | mistral-small-latest | required |
together | meta-llama/Llama-3.3-70B-Instruct-Turbo | required |
fireworks | accounts/fireworks/models/llama-v3p3-70b-instruct | required |
perplexity | sonar | required |
lm_studio | qwen/qwen3.5-35b-a3b | not needed (local) |
ollama | llama3 | not 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 configmenu under "Configure fallback order...", or directly withcgen 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
- Anthropic — Prompting best practices: https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices
- Anthropic — Effective context engineering for AI agents: https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents
- OpenAI — Best practices for prompt engineering: https://help.openai.com/en/articles/6654000-best-practices-for-prompt-engineering-with-the-openai-api
- OpenAI — GPT-5 prompting guide: https://developers.openai.com/cookbook/examples/gpt-5/gpt-5_prompting_guide
- An Empirical Study on Commit Message Generation using LLMs via In-Context Learning: https://arxiv.org/abs/2502.18904
- Automated Commit Message Generation with Large Language Models — An Empirical Study and Beyond: https://arxiv.org/abs/2404.14824
- Liu et al., Lost in the Middle — How Language Models Use Long Contexts: https://arxiv.org/abs/2307.03172
- Conventional Commits 1.0.0: https://www.conventionalcommits.org/en/v1.0.0/
- Gitmoji: https://gitmoji.dev/
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 historyinside a git repo shows that repo's tracked commitscgen historyoutside a git repo lists all tracked repos, then shows commits for the selected one- Selecting a commit runs
git showon 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
- Development Setup
- Project Structure
- Adding a New Default Provider
- Making Changes
- Pull Request Process
- Code Style
- Reporting Bugs
- Suggesting Features
Getting Started
- Fork the repository on GitHub
- Clone your fork locally:
git clone https://github.com/YOUR_USERNAME/smart-commit-rs.git cd smart-commit-rs - 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|alwaysbehavior after commit creationACR_WARN_STAGED_FILES_ENABLED=1+ lowACR_WARN_STAGED_FILES_THRESHOLDto trigger confirmationACR_SUPPRESS_TOOL_OUTPUT=1to confirm git output is hiddenundoon unpushed commitundoon 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 (
ureqinstead ofreqwest+tokio). - Minimal dependencies — every crate must justify its inclusion by binary size or maintenance burden.
std::process::Commandfor git operations — nogit2/gitoxidefor 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
-
Open
src/provider.rsand find theget_provider()function. -
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", }), } -
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 withsystem_instructionandcontentsarrays.Anthropic— Similar to OpenAI but withsystemas 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
RequestFormatand a matching arm inbuild_request_body(). -
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 wheretype == "message"and returns itscontent) - Use numbers for array indices:
results.0.text
- OpenAI-compatible:
-
URL/header interpolation — you can use
$ACR_API_KEY,$ACR_MODEL, or any environment variable in theapi_urlandapi_headersstrings. They get expanded at runtime. -
Update
src/cli.rs— add the provider name to thechoiceslist in the"PROVIDER"match arm ofinteractive_config():#![allow(unused)] fn main() { "PROVIDER" => { let choices = vec!["gemini", "openai", "anthropic", "your_provider", "(custom)"]; // ... } } -
Update the README — add the provider to the "Built-in providers" line in the Providers section.
-
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
-
Create a feature branch from
main:git checkout main git pull upstream main git checkout -b feature/your-feature-name -
Make your changes — keep commits focused and atomic.
-
Ensure quality:
cargo fmt --all --check cargo clippy --locked --all-targets --all-features -- -D warnings cargo test --locked cargo build --locked -
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
-
Push your branch to your fork:
git push origin feature/your-feature-name -
Open a Pull Request against
mainon the upstream repository. -
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.)
-
Keep it small — one concern per PR. A provider addition + a bug fix should be two separate PRs.
-
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 --releasesize) - Includes documentation updates when behavior changes
- Has a clear, concise title using conventional commit format
Code Style
- Format: Always run
cargo fmtbefore committing. - Lints: Fix all
cargo clippywarnings. - Error handling: Use
anyhowfor errors. Use.context("description")to add context to errors. Usebail!()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/-astaging of tracked modifications and deletions before generation- Exact machine-readable output with
--stdout, including non-rewritingalter - Sequential multi-candidate generation and selection with
--generate N/-g N - Invocation-only prompt guidance with
--prompt TEXT/-p TEXT - Repository-local
prepare-commit-msgmanagement throughcgen hook - Authenticated live model discovery and searchable selection through
cgen modeland the configuration editor
[1.4.1] - 2026-07-23
Changed
- Updated all Rust dependencies, including the
ureq3,toml1.1,sha20.11,inquire0.9,colored3, andshlex2 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-hereAPI-key placeholder
[1.4.0] - 2026-07-23
Added
- Sparse project
.envoverrides with an interactive “Inherit global value” action - Configurable diff byte/sensitive-path guards and explicit
--allow-large-diff/--allow-sensitiveoverrides - 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_TOKENSto configure the LLM completion token cap (default 512, previously hard-coded)
Changed
- Local
.envupdates 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_PROMPTnow 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
temperatureto 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 promptandcgen undono 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-rscrate 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
editdependency; editor launching is handled internally - Unused
enabledflag inside the presets file's[fallback]section (existing files containing it still parse)
[1.3.2] - 2026-06-25
Added
--set KEY=VALUEflag to override any setting for a single run (ephemeral, never persisted);auto_updateexcepted--diff-include GLOB/--diff-exclude GLOBflags 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_GLOBSconfiguration: 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_ENABLEDconfiguration flag (default: enabled) to toggle LLM fallback behavior- Per-repository commit cache: track which commits were AI-generated
cgen historysubcommand to browse AI-generated commits per repository (withgit showintegration)ACR_TRACK_GENERATED_COMMITSconfiguration 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 presetstandalone subcommand to manage LLM presets directlycgen fallbackstandalone 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_UPDATEis now a global-only setting and will not be written to local.envfilescall_llmnow usescall_llm_with_fallbackinternally, enabling automatic provider retrygenerate_final_messagereports 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 configview
[1.1.0] - 2026-02-24
Added
cgen updatesubcommand to manually update to the latest versionACR_AUTO_UPDATEconfiguration 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 promptsubcommand to print the LLM system prompt without running anythingcgen confignow 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 --globalflag removed; scope selection is now interactive when inside a git repo
[1.0.0] - 2026-02-23
- Initial release of the tool