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.
Table of contents
- 1. Why commit messages are more than a formality
- 2. How Claude Code derives a summary from a diff
- 3. The what and the why: the central gap
- 4. A practical example: from diff to finished commit message
- 5. Structuring PR descriptions: summary, context, test plan
- 6. Common pitfalls of generic AI summaries
- 7. Why the author must always verify the summary
- 8. Automation: hooks, aliases, and CI integration
- 9. Commit messages in direct comparison
- 10. Summary
- 11. FAQ
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.