| name | changelog-keeper |
| description | Keeps CHANGELOG.md up to date by generating categorized entries from git commit history. Use when: (1) user asks to update the changelog, (2) before committing changes that should be documented, (3) preparing a release and need changelog entries, (4) user says 'update changelog' or 'what changed since last release', (5) a commit is about to be pushed and the changelog hasn't been updated. |
| metadata | {"version":"1.1.1"} |
Changelog Keeper
Generates and maintains CHANGELOG.md entries from git commit history. Categorizes changes automatically using conventional commit prefixes and file path patterns.
Quick Reference
SCRIPT=~/.claude/skills/changelog-keeper/scripts/update-changelog.sh
$SCRIPT --dry-run
$SCRIPT
$SCRIPT --version 2.0.0
$SCRIPT --since v1.0.0
$SCRIPT --dry-run /path/to/repo
Workflow
When to Run
Run the changelog update script in these situations:
- Before a commit — when the user has staged changes and is about to commit, generate a changelog entry to include in the same commit
- Before a release — use
--version X.Y.Z to promote unreleased changes to a versioned entry
- On demand — when the user asks "what changed?" or "update the changelog"
- After multiple commits — to catch up the changelog with recent work
Step 1: Generate Changelog Entry
~/.claude/skills/changelog-keeper/scripts/update-changelog.sh --dry-run
The script:
- Auto-detects the last version tag or changelog entry as the starting point
- Reads all commits since that point
- Categorizes by conventional commit prefix (
feat: → Added, fix: → Fixed, etc.)
- Falls back to file-path categorization for non-conventional commits
- Outputs a Keep-a-Changelog formatted entry
Step 2: Review and Refine
After the script generates the raw entry:
- Consolidate related entries (e.g., multiple fix commits for the same issue → one bullet)
- Improve wording — commit messages are developer-facing; changelog entries should be user-facing
- Remove noise — drop entries for internal refactoring, CI changes, or typo fixes that don't affect users
- Add context — link to issues/PRs if relevant
Step 3: Write to CHANGELOG.md
~/.claude/skills/changelog-keeper/scripts/update-changelog.sh
~/.claude/skills/changelog-keeper/scripts/update-changelog.sh --version 1.2.0
Step 4: Include in Commit
Stage the updated CHANGELOG.md with the rest of the changes:
git add CHANGELOG.md
Categorization Rules
The script categorizes commits using conventional commit prefixes:
| Prefix | Category | Example |
|---|
feat, add | Added | feat: add dark mode toggle |
fix | Fixed | fix: resolve login timeout |
docs | Documentation | docs: update API reference |
test | Testing | test: add unit tests for auth |
refactor, perf, style, chore, build, ci | Changed | refactor: simplify auth flow |
revert | Removed | revert: remove experimental flag |
| (no prefix) | File-path fallback | Categorized by which files changed |
File-Path Fallback
When commits don't use conventional prefixes, the script examines changed files:
src/, lib/, scripts/ → Changed
*.test.*, tests/, __tests__/ → Testing
*.md, docs/, README* → Documentation
Format
The script follows Keep a Changelog format:
# Changelog
## [Unreleased]
### Added
- New feature description
### Fixed
- Bug fix description
## [1.0.0] - 2026-02-24
### Added
- Initial release features
Multi-Script CHANGELOG Coordination
When multiple scripts modify the same CHANGELOG (e.g., a sync script + a release script), they must recognize each other's entry format to avoid clobbering.
The Problem
Script A generates ## [2026-02-24] — Monorepo sync entries.
Script B promotes them to ## [1.1.0] - 2026-02-24 entries.
If Script A runs after Script B, it doesn't recognize [1.1.0] as "already handled" and prepends a new generic entry, burying or duplicating the release entry.
The Fix: Format-Aware Detection
Each script must detect the other's entry format before writing:
FIRST_ENTRY=$(awk '/^## \[/{print; exit}' CHANGELOG.md)
if echo "$FIRST_ENTRY" | grep -q "Monorepo sync"; then
...
elif echo "$FIRST_ENTRY" | grep -qE '## \[[0-9]+\.[0-9]+\.[0-9]+\]'; then
...
else
...
fi
Bash Newline Pitfall
When building CHANGELOG content via string concatenation, bash $() command substitution always strips trailing newlines. This causes:
Format: Monorepo-level events only.## [1.1.0] - 2026-02-24 ← MISSING BLANK LINE
This also affects printf: HEADER=$(printf '%s\n\n' "$HEADER") still loses the trailing newlines because $() strips them after printf outputs them.
Fix: Never rely on trailing newlines in variables. Add blank lines at the concatenation point instead:
HEADER=$(echo "$EXISTING" | awk '/^## \[/{exit} {print}')
HEADER=$(printf '%s\n\n' "$HEADER")
NEW_CHANGELOG="${HEADER}${NEW_ENTRY}"
HEADER=$(echo "$EXISTING" | awk '/^## \[/{exit} {print}')
NEW_CHANGELOG="${HEADER}
${NEW_ENTRY}"
Semver Tag Filtering
git describe --tags --abbrev=0 picks up ANY tag (including non-semver like sync-2026-02-24). For version detection, filter explicitly:
git describe --tags --abbrev=0
git tag -l 'v[0-9]*' --sort=-v:refname | head -1
Key Decisions
| Decision | Choice | Rationale |
|---|
| Conventional commits first | Parse prefixes before file paths | More accurate when available |
| Keep-a-Changelog format | Standard sections (Added/Changed/Fixed/...) | Widely recognized, machine-parseable |
| Dry-run default in workflow | Always preview first | Prevents accidental overwrites |
| No AI dependency | Pure git + sed/awk | Works offline, deterministic, fast |
| Scope stripping | fix(auth): → body only | Scopes are for commits, not changelogs |