Generating Commit Messages and PR Descriptions with AI
AI generated
Claude
>_
Claude Code · Git · Commit Messages · Pull Requests
Generating Commit Messages and PR Descriptions with AI
From the diff to the summary, from the what to the why

Claude Code turns a diff into a structured commit message or PR description in seconds, saving writing effort. The summary only describes what changed, not why. This article shows how developers can use AI suggestions productively while keeping ownership of the content.

13 min read Git · Conventional Commits · Pull Requests Claude Code CLI · CI Integration

1. Why commit messages are more than a formality

A commit message is the only piece of documentation that stays inseparably tied to a code change. While comments go stale and tickets eventually get archived, the commit message stays permanently bound, via git blame and git log, to the exact lines it describes. Anyone trying to find out a year from now why a particular price calculation in Magento was implemented in a specific way often reaches the answer faster through git blame than through a search in the ticket system.

That is exactly why the temptation to delegate writing a good commit message to an AI is so strong. Claude Code sees the diff, knows conventions such as Conventional Commits, and formulates a suggestion within seconds that is grammatically correct and names the changed files cleanly. The problem is not the language, it is the knowledge: a language model only sees the code difference, not the discussion in standup, the client conversation, or the reason a particular approach was chosen over another.

2. How Claude Code derives a summary from a diff

Technically, when generating a commit message Claude Code works in a purely text-based way: the diff from git diff --staged is passed as context, the model recognizes patterns such as changed function signatures, new files, deleted code, or adjusted configuration values, and derives a plausible description from them. Given a new method on a repository class, the model reliably recognizes that a method was added, and usually also what it technically does, such as filtering a collection by an attribute.

What the model cannot infer is the trigger behind the change. Whether the new filter method exists because of a customer ticket, a performance regression, or a planned feature is not stated anywhere in the diff. Claude Code can partially close this gap if extra context is provided, such as a ticket number, a short note on motivation, or the content of a linked issue. Without that context, the generated message stays technically correct but shallow in substance: it describes the diff surface, not the decision behind it.


#!/usr/bin/env bash
# Generate a commit message from the staged diff with Claude Code
set -euo pipefail

# Stage the relevant changes first
git add src/app/code/Mironsoft/SeoSuite/Model/CanonicalUrlResolver.php

# Ask Claude Code to draft a message, but provide the "why" explicitly
claude -p "Write a Conventional Commits message for the staged diff.
Context: fixes duplicate canonical URLs on paginated category pages
reported in ticket MIRO-482. Keep the subject line under 72 chars." \
  --allowedTools "Bash(git diff --staged)" "Bash(git log -5 --oneline)"

# Review before committing, never pipe the suggestion directly into commit
git commit -e -F <(echo "fix(seo): resolve duplicate canonical URLs on paginated category pages

Pagination parameters were included in the canonical URL builder,
causing page 2+ of a category to self-canonicalize instead of
pointing back to page 1. Fixes MIRO-482.")

3. The what and the why: the central gap

A commit message ideally has two layers: the subject line briefly describes what changed, the body explains why the change was necessary and which alternatives were discarded. Claude Code reliably delivers the first layer, because it can be read directly off the diff. The second layer requires knowledge only the developer has: context from sprint planning, the assessment that a simpler fix would have been riskier, or the information that a workaround is deliberately temporary and will be resolved properly in a follow-up ticket.

An example from Magento practice makes the difference concrete: a diff shows that a plugin was added to Magento\Catalog\Model\Product::getPrice(). Claude Code correctly describes that a new price plugin was added. Only the developer knows that a preference was deliberately avoided at this point, because another third-party module already overrides the same class, and a plugin conflict would otherwise be unavoidable. That reasoning belongs in the commit body, but only appears in an AI-generated suggestion if it is explicitly communicated.

4. A practical example: from diff to finished commit message

A realistic workflow starts by looking at the diff before requesting an AI suggestion at all. For a change that fixes an N+1 query bug in a custom collection, git diff --staged shows only a new joinLeft() statement. A naive AI suggestion then often reads simply "Update ProductCollection.php" or at best "Add joinLeft to ProductCollection." Neither is technically wrong, but both are worthless to anyone reading the commit later.

Once context is provided, such as "fixes N+1 queries when loading custom attributes on the product list, measured via New Relic with 340 extra queries per page load," a considerably more useful message emerges. The decisive step remains with the developer: check the generated subject line, add or correct the motivation in the body, and only then commit. Claude Code delivers the first draft and clean formatting according to the Conventional Commits convention here, not the finished, approved statement.


{
  "commit_draft": {
    "type": "fix",
    "scope": "catalog",
    "subject": "eliminate N+1 queries in product collection custom attribute loading",
    "body_ai_generated": "Added joinLeft() call to ProductCollection to fetch custom attributes in a single query instead of per-product lookups.",
    "body_after_human_review": "Custom attribute values were loaded per product inside a loop, causing 340 extra queries per category page (measured via New Relic APM). Replaced the per-product lookup with a single joinLeft() against eav_attribute_value, matching the pattern already used in OrderGridCollection. Trade-off: slightly larger result set per row, acceptable given the query count reduction.",
    "footer": "Refs: MIRO-511"
  }
}

5. Structuring PR descriptions: summary, context, test plan

A pull request description serves a different purpose than a single commit message: it summarizes several commits into a story reviewers can follow, and it must additionally explain how the change was tested. Claude Code works well for producing a first structure with the sections summary, changes in detail, and test plan, drawn from several commit messages and the overall diff of a branch. This structure saves time, because simply listing the changed files and functions is a mechanical task.

The test plan section is the most critical part, because it cannot be derived from the diff. It has to describe what was actually verified, manually or automatically. A generated suggestion such as "Tested locally" is worthless to a reviewer, because it makes no statement about the scope of the check. Only a concrete addition such as "Tested with three cart scenarios including a coupon combination, checkout test suite regression run locally" gives the reviewer a solid basis for approval.


#!/usr/bin/env python3
"""ci-pr-description.py: draft a PR description from commit history.
Runs in CI, produces a template that a human reviews and completes
before opening the pull request. Does not auto-submit the PR.
"""
import subprocess
import sys

def get_commit_log(base_branch: str) -> str:
    """Return commit subjects and bodies since the base branch diverged."""
    result = subprocess.run(
        ["git", "log", f"{base_branch}..HEAD", "--pretty=format:%s%n%b%n---"],
        capture_output=True, text=True, check=True,
    )
    return result.stdout

def build_template(commit_log: str) -> str:
    """Build a PR description skeleton with mandatory sections."""
    return f"""## Summary
<!-- One or two sentences: what does this PR do and why now? -->

## Changes
{commit_log}

## Test plan
<!-- REQUIRED: describe what was actually verified, not just "tested locally" -->
- [ ] Manual test steps:
- [ ] Automated tests run:
- [ ] Edge cases considered:
"""

if __name__ == "__main__":
    base = sys.argv[1] if len(sys.argv) > 1 else "main"
    log = get_commit_log(base)
    print(build_template(log))

6. Common pitfalls of generic AI summaries

The most common problem with AI-generated commit messages is genericness: phrases like "Update files," "Fix bug," "Improve code," or "Refactor logic" are not technically wrong, but empty of content. They arise when a model faces a large, cluttered diff without extra context, one that mixes several independent changes, for example a bugfix alongside a formatting change and a removed debug statement. The larger and more heterogeneous the diff, the more generic the summary turns out, because no single thread remains recognizable.

A second trap is the overconfidence of generated phrasing. A model confidently writes "Fixes the checkout timeout issue," even if the change only resolves the problem in one of three known scenarios. This overgeneralization is dangerous because it gives the reviewer, and later the team, the impression that a topic was fully resolved when it was only partially addressed. The third common mistake concerns PR descriptions that imply test coverage that never happened, because the model invents a plausible-sounding test plan instead of leaving a gap open.


#!/usr/bin/env bash
# commit-msg hook: reject generic subject lines before they enter history
set -euo pipefail

COMMIT_MSG_FILE="$1"
SUBJECT="$(head -n 1 "$COMMIT_MSG_FILE")"

# Common generic phrases produced by unreviewed AI drafts
GENERIC_PATTERNS=(
  "^(fix|feat|chore): (update|fix|improve) (files?|code|bug)$"
  "^update files?$"
  "^wip$"
  "^misc changes?$"
)

for pattern in "${GENERIC_PATTERNS[@]}"; do
  if [[ "$SUBJECT" =~ $pattern ]]; then
    echo "[REJECTED] Subject line is too generic: '$SUBJECT'" >&2
    echo "Describe what changed and, if relevant, why." >&2
    exit 1
  fi
done

exit 0

7. Why the author must always verify the summary

Responsibility for the content and accuracy of a commit message or PR description always lies with the author, regardless of who originally drafted the text. A reviewer reading a PR description relies on the fact that the described test plan was actually carried out and that the stated motivation is correct. If that assumption is broken by an uncritically adopted AI summary, not only does that individual PR lose credibility, future PRs from the same author get scrutinized more strictly too.

The practical consequence is a fixed verification step before every commit: read the generated subject line against the actual diff, check whether the stated motivation is accurate, and only adopt the test plan if the described steps were genuinely carried out. For a short commit message this step rarely takes more than a minute, but it systematically prevents false or exaggerated claims from entering the permanent history of a repository. Skipping this step effectively shifts responsibility onto a tool that cannot know the actual motivation in the first place.

8. Automation: hooks, aliases, and CI integration

Sensible automation generates suggestions but never forces an unreviewed adoption at any point. A Git alias or a prepare-commit-msg hook can call Claude Code and write the suggestion into the commit message file that the editor then opens for editing, instead of finalizing the commit directly. This intermediate step ensures that every generated message is read at least once before it enters the history.

In CI pipelines the same approach works well for PR descriptions: a script generates a draft with the mandatory sections summary, changes, and test plan when a pull request is opened, but without automatically publishing the PR or pre-filling the test plan section. This keeps the mechanical grunt work automated while the substantive decision about what was actually tested stays with a human. The same caution applies to changelogs generated automatically from commit messages: they are only as good as the underlying messages, and generic entries add up to a change log that is useless to customers.


// generate-pr-draft.js: create a PR description draft via the Claude API
// Runs in a GitHub Action, posts the draft as a PR comment for human review
// instead of writing directly into the PR body.
const Anthropic = require('@anthropic-ai/sdk');
const { execSync } = require('node:child_process');

const anthropic = new Anthropic();

async function draftPrDescription(baseBranch) {
  const diff = execSync(`git diff ${baseBranch}...HEAD`, { maxBuffer: 10 * 1024 * 1024 }).toString();
  const log = execSync(`git log ${baseBranch}..HEAD --pretty=format:%s`).toString();

  const message = await anthropic.messages.create({
    model: 'claude-sonnet-4-5',
    max_tokens: 800,
    messages: [{
      role: 'user',
      content: `Draft a PR description with sections Summary, Changes, Test plan.
Mark the Test plan section as "NEEDS HUMAN INPUT", do not invent test steps.
Commit log:\n${log}\n\nDiff:\n${diff.slice(0, 8000)}`,
    }],
  });

  return message.content[0].text;
}

draftPrDescription('main').then(draft => {
  console.log('--- Draft for human review, not auto-posted ---');
  console.log(draft);
});

9. Commit messages in direct comparison

The difference between an unreviewed and a verified AI summary shows most clearly in a direct comparison of concrete examples. The table below sets typical generic suggestions against the revised, context-rich versions that should emerge after a brief human review.

Situation Generic AI suggestion Verified version
Checkout bugfix fix: fix checkout bug fix(checkout): prevent double order creation on slow network retries
New plugin class feat: add plugin feat(pricing): add tier-price plugin, avoids conflict with third-party discount module
PR test plan Tested locally Tested with 3 cart scenarios, checkout test suite green locally, no regression test for guest checkout
Performance fix perf: improve performance perf(catalog): reduce category page queries from 340 to 12 via joinLeft
Configuration change chore: update config chore(deploy): raise PHP memory_limit to 1G, resolves OOM on large CSV imports

Mironsoft

Magento and Hyva development with AI-assisted workflows

A clean commit history despite AI-assisted development?

We set up Git workflows, commit conventions, and PR templates so that Claude Code saves time without your project history losing its meaning.

Workflow audit

Review your existing commit and PR practice for substance

Conventions

Roll out Conventional Commits and PR templates team-wide

CI integration

Set up hooks and scripts for PR drafts without auto-publish

10. Summary

AI-assisted commit messages and PR descriptions save real writing effort, because they take over the mechanical task of naming the changed files and functions from a diff. Claude Code reliably recognizes what changed in the code, but it cannot know why a change was necessary, which alternatives were discarded, or what test scope was actually covered. This gap between the what and the why is the central point where human verification stays not optional, but necessary.

In practice, a fixed routine works well: treat the AI suggestion as a draft, actively supply extra context such as ticket numbers or motivation, consistently replace generic phrases like "Update files" or "Tested locally," and read every summary against the actual diff before committing. Automation through hooks and CI scripts works best when it produces drafts instead of automatically publishing finished claims.

Generating Commit Messages and PR Descriptions with AI, the essentials

What AI does well

Recognizing diff patterns, following the Conventional Commits format, producing mechanical summaries.

What AI cannot do

Know the motivation behind a change, name discarded alternatives, confirm actual test scope.

Most common mistake

Adopting generic phrases like "Update files" or invented test plans without review.

Practical rule

Actively supply context, treat the suggestion as a draft, read it against the diff before every commit.

11. FAQ: Generating Commit Messages and PR Descriptions with AI

1Can Claude Code write a commit message completely on its own?
Technically yes, based on the diff. In substance it only covers the what, not the why, and should be extended before committing.
2Why are AI-generated commit messages sometimes so generic?
For large, heterogeneous diffs the model finds no clear thread and produces general statements like "Update files."
3What is the difference between the what and the why?
The what can be read off the diff. The why explains motivation and discarded alternatives and must be actively communicated.
4Should I adopt an AI suggestion for a PR description directly?
No, the test plan in particular should never be adopted without review, because invented test steps are possible.
5How do I give Claude Code enough context?
With a short note on motivation in the prompt, such as a ticket number, a measured effect, or the reason for a technical solution.
6What is Conventional Commits and does it help with AI messages?
A format with type and scope that Claude Code follows reliably. It does not replace the substantive review.
7How do I automate this without losing control?
Through a prepare-commit-msg hook that opens the draft for editing instead of finalizing the commit automatically.
8Can generic commit messages become a team-wide problem?
Yes, automatically generated changelogs often rely on commit messages and become worthless with generic entries.
9Who is responsible for false claims?
Always the author of the PR, regardless of who originally drafted the text.
10Is AI assistance worth it despite the need for verification?
Yes, because the mechanical work saves real time and verifying a good draft usually takes only seconds.