How to Build a Claude-Powered Changelog Summarizer
If you ship software regularly, you already know the pain of writing changelogs. Commit messages are inconsistent, PR titles range from detailed to "fix bug," and someone always has to translate dozens of git log lines into something a user or stakeholder can actually read. This article walks through building a Claude-powered changelog summarizer tool: a small pipeline that pulls raw commit or PR data, sends it to Claude with a structured prompt, and returns clean, categorized release notes.
The core idea is simple: Claude is good at taking messy, unstructured technical text and turning it into consistent, well-organized prose. You don't need a complex app for this — a script, a CI step, or a lightweight internal tool is enough. The rest of this guide covers the architecture, the prompt design that actually produces usable output, and how to wire it up to run automatically on every release.
What the tool actually needs to do
Before writing any code, define the job precisely. A changelog summarizer needs to:
- Collect raw input: git commits since the last tag, merged PR titles/descriptions, or closed ticket summaries
- Filter noise: merge commits, CI bumps, dependency updates that don't matter to end users
- Classify changes: Added, Changed, Fixed, Removed, Security
- Write in a consistent voice: present tense, no internal jargon, no ticket IDs unless requested
- Output in a predictable format: Markdown, ready to paste into a GitHub release or docs site
Claude handles the classification and writing steps well. The collection and filtering steps are plain code — grep, git log formatting, or a GitHub API call.
Step 1: Collect the raw data
For a git-based project, the simplest source is commit messages since the last tag:
git log $(git describe --tags --abbrev=0)..HEAD --pretty=format:"- %s (%h)"
If your team relies more on PRs than commit hygiene, pull merged PR titles and descriptions from GitHub's API instead — PR descriptions usually contain more context than a one-line commit message.
Either way, you end up with a block of raw text. Don't over-clean it in code. Claude is better at filtering "bump lodash to 4.17.21" out of a changelog than a regex is, because it understands intent, not just keywords.
Step 2: Design the prompt
This is where most changelog summarizers succeed or fail. A vague prompt like "summarize these commits" produces vague output. Be explicit about format, tone, and what to exclude:
You are generating a public changelog from raw commit messages.
Rules:
- Group entries under: Added, Changed, Fixed, Removed, Security
- Omit dependency bumps, CI config changes, and typo fixes unless user-facing
- Write each entry as one sentence, present tense, no ticket IDs
- Omit empty categories
- Output valid Markdown only, no preamble
Raw commits:
{{commits}}
This level of specificity matters more than model choice. Claude follows structural instructions reliably when they're stated as rules rather than suggestions, so treat the prompt like a spec, not a request.
Step 3: Call Claude via SubToAPI
Rather than managing raw Claude API credentials, keys, and billing separately, you can route this through SubToAPI, which turns your existing Claude access into a standard HTTPS API with application-level keys. That's useful here because a changelog summarizer is often a shared internal tool — multiple repos, multiple CI pipelines, maybe multiple team members triggering it manually. SubToAPI gives you per-app keys and usage visibility without each integration needing its own setup.
A basic call looks like this:
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4",
"max_tokens": 1024,
"messages": [
{
"role": "user",
"content": "You are generating a public changelog... [rules + commits here]"
}
]
}'
Or in JavaScript, if you're wiring this into a Node-based CI script:
const res = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "claude-sonnet-4",
max_tokens: 1024,
messages: [{ role: "user", content: prompt }]
})
});
const data = await res.json();
const changelog = data.content[0].text;
Full request and response shapes are documented at /docs/messages, and /docs/quickstart walks through getting an API key set up in a few minutes.
Step 4: Automate it in CI
The real value of this tool comes from running it automatically on every tagged release. A GitHub Actions step might:
- Checkout the repo on tag push
- Run the git log command to collect commits since the previous tag
- Call the summarizer script
- Write the output to
CHANGELOG.mdor post it as the GitHub release body
- name: Generate changelog
run: node scripts/generate-changelog.js
env:
SUBTOAPI_KEY: ${{ secrets.SUBTOAPI_KEY }}
This removes the manual step entirely — no one has to remember to write release notes, and the output is consistent every time because the prompt enforces the format.
Step 5: Add a human review step (optional but recommended)
Even a well-tuned prompt occasionally miscategorizes an entry or over-summarizes something that needed more detail. For public-facing products, pipe the generated changelog into a draft PR or a Slack message for a quick human glance before it goes live, rather than auto-publishing directly. This keeps the speed benefit without losing editorial control.
Where this fits for teams
If you're building this as a shared tool across several repositories or a growing team, the per-seat structure matters. A single developer running this from a personal script works fine on the Solo plan, but once multiple people or multiple CI pipelines are hitting the same integration, Team and Scale plans add seat-based access and usage tracking so you can see which pipeline or person is generating the most calls. You can try the whole setup free by starting at /signup.
questions
Does the summarizer need access to full diffs, or just commit messages? Commit messages or PR titles/descriptions are usually enough. Full diffs add token cost without much improvement in output quality unless your commit messages are extremely sparse.
Can this work with Jira tickets instead of git commits? Yes — the input source doesn't matter to Claude. Pull ticket titles and resolution notes via the Jira API, format them as a text block, and feed them into the same prompt structure.
How do I keep the tone consistent across releases? Keep the prompt's rules section identical every run and avoid letting contributors inject freeform commit text into the final output unedited — the rules in the prompt, not the model's defaults, are what keep tone stable over time.