The gap
CLAUDE.md line 28 requires a version bump on every content change, in both
SKILL.md frontmatter and .claude-plugin/plugin.json, kept in lockstep.
Nothing enforces it. CLAUDE.md line 10 says so directly: validate.yml "also
doesn't enforce a bump on every SKILL.md change, but one is needed anyway."
So the rule is written down, known, and skippable.
It already happened
#82 retired four pattern groups from rules/taxonomy.md and added three
rules to rules/learned.md, and left the version at 1.1.0. It merged clean:
every check passed, both automated reviewers passed, and the pre-merge analysis
passed.
The merge changed nothing for any install. /plugin update treats an unchanged
version as nothing to pull, so the cache kept serving the pre-consolidation
rules. Measured after that merge, on the machine that did it:
- Live cache was
~/.claude/plugins/cache/personify/personify/1.1.0/, marked
.in_use.
- Its
rules/taxonomy.md still had all four retired groups. Expected 0, found
4.
- Its
rules/learned.md had none of the three new rules. Expected 3, found 0.
#83 was then needed to bump 1.1.0 to 1.2.0 and ship what #82 had already
merged. That is the whole failure: a content change that merges green and
reaches nobody, with no signal anywhere that it did not land.
Worth noting what did not catch it. Two blocking AI reviewers read #82 and
passed it. The failure is invisible in a diff, because the defect is the
absence of a line in a file the diff does not touch.
Proposed check
In .github/workflows/validate.yml, which already runs on pull_request and
already parses plugin.json. Roughly:
- If the PR changes no versioned content path, pass.
- Otherwise require that
version in SKILL.md frontmatter differs from the
value on the base branch.
- Require that
SKILL.md's version and .claude-plugin/plugin.json's version
are equal. This half is worth running unconditionally, since a lockstep
break is wrong whether or not content changed in the same PR.
Needs fetch-depth: 0 or an explicit base fetch to read the base version;
validate.yml currently does a default shallow checkout.
Which paths count
The set is the point. Too wide and every typo fix demands a bump, the rule gets
resented, and someone adds a skip label that becomes the new default.
Content that installs reach, and that a stale cache would serve wrongly:
SKILL.md
rules/ (taxonomy.md, hard.md, learned.md)
VOICE.example.md
commands/
reviewer/
Explicitly out:
mcp-server/ is versioned independently. CLAUDE.md line 12 says the
lockstep rule "applies only to the skill content, not to this bridge."
tests/, scripts/, .github/, docs/
README.md, CLAUDE.md, AGENTS.md, LICENSE
VOICE.md is gitignored and cannot appear in a diff.
commands/ and reviewer/ deserve a second opinion. They ship with the
plugin, so a stale cache serves stale copies of both, but a bump for an edit to
either is arguably noise. I would rather include them and be wrong toward
safety than discover the same silent-staleness failure from a different
direction.
What "differs" should mean
Weakest useful rule: the version string is not equal to the base branch's. Not
"is greater" and not semver-aware.
Comparing semver correctly means parsing it, which means deciding what a
prerelease or a build-metadata suffix does, and that is more machinery than
this needs. A wrong-direction bump is a mistake a human notices in review; an
absent bump is the thing that already shipped and nobody noticed. Catch the
second and let review handle the first. Revisit only if a backwards bump
actually happens.
Why not a hook instead
A local pre-commit hook would catch it earlier, but only on a machine with the
hook installed, and it cannot see the base branch's version on a first commit
to a new branch without extra work. CI sees both sides by construction and
applies to every PR regardless of who opened it. Local hooks are a reasonable
addition later, not the primary enforcement.
Success criteria
Reconstruct #82 as it was, with no version change, and CI fails with a
message naming both the file to bump and the current value. Re-run with the
bump applied and it passes. A PR touching only README.md passes untouched.
This is the case to test against, not a synthetic one: it is the exact shape
that got through.
Also worth doing
CLAUDE.md line 10 currently documents the absence of this check. Update it
when the check lands, or it becomes a stale claim about CI behavior.
The gap
CLAUDE.mdline 28 requires a version bump on every content change, in bothSKILL.mdfrontmatter and.claude-plugin/plugin.json, kept in lockstep.Nothing enforces it.
CLAUDE.mdline 10 says so directly: validate.yml "alsodoesn't enforce a bump on every SKILL.md change, but one is needed anyway."
So the rule is written down, known, and skippable.
It already happened
#82retired four pattern groups fromrules/taxonomy.mdand added threerules to
rules/learned.md, and left the version at 1.1.0. It merged clean:every check passed, both automated reviewers passed, and the pre-merge analysis
passed.
The merge changed nothing for any install.
/plugin updatetreats an unchangedversion as nothing to pull, so the cache kept serving the pre-consolidation
rules. Measured after that merge, on the machine that did it:
~/.claude/plugins/cache/personify/personify/1.1.0/, marked.in_use.rules/taxonomy.mdstill had all four retired groups. Expected 0, found4.
rules/learned.mdhad none of the three new rules. Expected 3, found 0.#83was then needed to bump 1.1.0 to 1.2.0 and ship what#82had alreadymerged. That is the whole failure: a content change that merges green and
reaches nobody, with no signal anywhere that it did not land.
Worth noting what did not catch it. Two blocking AI reviewers read
#82andpassed it. The failure is invisible in a diff, because the defect is the
absence of a line in a file the diff does not touch.
Proposed check
In
.github/workflows/validate.yml, which already runs onpull_requestandalready parses
plugin.json. Roughly:versioninSKILL.mdfrontmatter differs from thevalue on the base branch.
SKILL.md's version and.claude-plugin/plugin.json's versionare equal. This half is worth running unconditionally, since a lockstep
break is wrong whether or not content changed in the same PR.
Needs
fetch-depth: 0or an explicit base fetch to read the base version;validate.ymlcurrently does a default shallow checkout.Which paths count
The set is the point. Too wide and every typo fix demands a bump, the rule gets
resented, and someone adds a skip label that becomes the new default.
Content that installs reach, and that a stale cache would serve wrongly:
SKILL.mdrules/(taxonomy.md,hard.md,learned.md)VOICE.example.mdcommands/reviewer/Explicitly out:
mcp-server/is versioned independently.CLAUDE.mdline 12 says thelockstep rule "applies only to the skill content, not to this bridge."
tests/,scripts/,.github/,docs/README.md,CLAUDE.md,AGENTS.md,LICENSEVOICE.mdis gitignored and cannot appear in a diff.commands/andreviewer/deserve a second opinion. They ship with theplugin, so a stale cache serves stale copies of both, but a bump for an edit to
either is arguably noise. I would rather include them and be wrong toward
safety than discover the same silent-staleness failure from a different
direction.
What "differs" should mean
Weakest useful rule: the version string is not equal to the base branch's. Not
"is greater" and not semver-aware.
Comparing semver correctly means parsing it, which means deciding what a
prerelease or a build-metadata suffix does, and that is more machinery than
this needs. A wrong-direction bump is a mistake a human notices in review; an
absent bump is the thing that already shipped and nobody noticed. Catch the
second and let review handle the first. Revisit only if a backwards bump
actually happens.
Why not a hook instead
A local pre-commit hook would catch it earlier, but only on a machine with the
hook installed, and it cannot see the base branch's version on a first commit
to a new branch without extra work. CI sees both sides by construction and
applies to every PR regardless of who opened it. Local hooks are a reasonable
addition later, not the primary enforcement.
Success criteria
Reconstruct
#82as it was, with no version change, and CI fails with amessage naming both the file to bump and the current value. Re-run with the
bump applied and it passes. A PR touching only
README.mdpasses untouched.This is the case to test against, not a synthetic one: it is the exact shape
that got through.
Also worth doing
CLAUDE.mdline 10 currently documents the absence of this check. Update itwhen the check lands, or it becomes a stale claim about CI behavior.