Skip to content

LLM Commit Messages

Worktrunk generates commit messages by building a templated prompt and piping it to an external command. This integrates with wt merge, wt step commit, and wt step squash.

LLM commit message generation demo

Any command that reads a prompt from stdin and outputs a commit message works. Add to ~/.config/worktrunk/config.toml:

[commit.generation]
command = "MAX_THINKING_TOKENS=0 claude -p --no-session-persistence --model=haiku --tools='' --safe-mode --setting-sources='user' --system-prompt=''"

--no-session-persistence prevents the commit conversation from polluting claude --continue. --safe-mode keeps the run hermetic — no hooks, plugins, MCP, skills, or CLAUDE.md — while leaving authentication working normally, so setups that authenticate via apiKeyHelper (not just OAuth or ANTHROPIC_API_KEY) still get a key. --setting-sources='user' scopes settings to your user config so a project .claude/settings.json can’t override auth. The remaining flags disable tools, system prompt, and thinking for fast text-only output. --safe-mode requires Claude Code ≥ 2.1.169. See Claude Code docs for installation.

[commit.generation]
command = "codex exec -m gpt-5.6-luna -c model_reasoning_effort='low' -c system_prompt='' --sandbox=read-only --json - | jq -sr '[.[] | select(.item.type? == \"agent_message\")] | last.item.text'"

Uses the fast, low-cost variant of the current Codex model family with low reasoning effort and an empty system prompt for faster output. Requires jq for JSON parsing. See Codex CLI docs.

# opencode — use a fast model variant
command = "opencode run -m anthropic/claude-haiku-4.5 --variant fast"
# llm
command = "llm -m claude-haiku-4.5"
# aichat
command = "aichat -m claude:claude-haiku-4.5"

These examples assume a feature worktree with changes to commit.

Squashes all changes (uncommitted + existing commits) into one commit with an LLM-generated message, then merges to the default branch:

wt merge
Squashing 3 commits into a single commit (5 files, +16)...
Generating squash commit message...
feat(auth): Implement JWT authentication system
Add comprehensive JWT token handling including validation, refresh
logic, and authentication tests.
Squashed @ a1b2c3d
Merging 1 commit to main @ a1b2c3d (no rebase needed)
* a1b2c3d feat(auth): Implement JWT authentication system
auth.rs | 2 ++
auth_test.rs | 2 ++
integration_test.rs | 6 ++++++
jwt.rs | 3 +++
jwt_test.rs | 3 +++
5 files changed, 16 insertions(+)
Merged to main (1 commit, 5 files, +16)
Removing feature worktree & branch in background (same commit as main, _)
Switched to worktree for main @ ~/repo

Stages and commits with LLM-generated message:

wt step commit
Generating commit message and committing changes... (2 files, +26)
feat(validation): add input validation utilities
Committed changes @ a1b2c3d

Squashes branch commits into one with LLM-generated message:

wt step squash
Squashing 3 commits into a single commit (5 files, +16)...
Generating squash commit message...
feat(auth): Implement JWT authentication system
Add comprehensive JWT token handling including validation, refresh
logic, and authentication tests.
Squashed @ a1b2c3d

See wt merge and wt step for full documentation.

With summary = true and a [commit.generation] command configured, Worktrunk generates LLM branch summaries — one-line descriptions of each branch’s changes since the default branch.

Summaries appear in:

Enable in user config:

[list]
summary = true

Summaries are cached and regenerated only when the diff changes.

Worktrunk uses minijinja templates (Jinja2-like syntax) to build prompts.

Override the defaults with inline templates:

[commit.generation]
command = "llm -m claude-haiku-4.5"
template = """
Write a commit message for this diff. One line, under 50 chars.
Branch: {{ branch }}
Diff:
{{ git_diff }}
"""
squash-template = """
Combine these {{ commit_details | length }} commits into one message:
{% for c in commit_details %}
- {{ c.subject }}
{% endfor %}
Diff:
{{ git_diff }}
"""
VariableDescription
{{ git_diff }}The diff (staged changes or combined diff for squash)
{{ git_diff_stat }}Diff statistics (files changed, insertions, deletions)
{{ branch }}Current branch name
{{ repo }}Repository name
{{ recent_commits }}Recent commit subjects (for style reference)
{{ commit_details }}Commits being squashed (squash template only); each renders as its subject and exposes .subject / .body
{{ target_branch }}Merge target branch (squash template only)
{{ user_guidance }}Rendered user template-append fragment (see below)
{{ project_guidance }}Rendered project template-append fragment (see below)

Templates use minijinja, which supports:

  • Variables: {{ branch }}, {{ repo | upper }}
  • Filters: {{ commit_details | length }}, {{ repo | upper }}
  • Conditionals: {% if recent_commits %}...{% endif %}
  • Loops: {% for c in commit_details %}{{ c.subject }}{% endfor %}
  • Loop variables: {{ loop.index }}, {{ loop.length }}
  • Whitespace control: {%- ... -%} strips surrounding whitespace

See wt config create --help for the full default templates.

template-append adds to the commit and squash prompts instead of replacing them. It lives in both user config (personal preferences) and project config (.config/wt.toml, shared so every teammate’s LLM sees the same style guide). Each fragment is itself a minijinja template — Worktrunk renders it with the same variables as the main template ({{ branch }}, {{ git_diff }}, …), then appends the result after <style>. The user fragment renders into a <user-guidance> block and the project fragment into a <project-guidance> block, so the LLM can tell personal preference from shared convention:

.config/wt.toml
[commit.generation]
template-append = """
- Use conventional commits (feat:, fix:, docs:, …)
- Reference the related issue ID in the body
"""

When both the user and project set template-append, the <user-guidance> block comes first, then <project-guidance>.

The user fragment needs no approval — it’s the developer’s own config. For the project fragment, the first time the rendered text is sent to the LLM, Worktrunk shows the raw fragment in an approval prompt — the same one-shot gate as project-defined hooks. Subsequent commits don’t re-prompt unless the fragment changes. Declining is non-fatal: the LLM runs with just the user fragment (if any).

Custom user templates that don’t reference {{ user_guidance }} / {{ project_guidance }} opt out of the appended blocks — the rendered values are injected only where the template places them.

When no LLM is configured, worktrunk generates deterministic messages based on changed filenames (e.g., “Changes to auth.rs & config.rs”).