Skip to content

Commit ce1d7c7

Browse files
CI has never validated the plugin manifests
The real cause of the last three red runs, and of every green one before them. `npm install -g @anthropic-ai/claude-code` installs a shim that fetches a platform-native binary in its postinstall. On CI that binary never landed, so every `claude plugin validate` exited non-zero with "claude native binary not installed" — and check 19's old output parsing read that error as "no findings" and printed ok. The step has been in the workflow for as long as the check has, and it has never once validated anything. CI now runs the postinstall and asserts `claude --version` before the gate, so a broken install stops the run where it happens instead of resurfacing later as a check that measures nothing. The control runs both directions now, because each catches a different lie. A validator that rejects everything is indistinguishable from a broken repo, which is exactly the state CI was in. A validator that accepts everything is indistinguishable from a healthy one. It has to accept a known-good manifest and reject a known-bad one before this check will repeat what it says. The good control carries an author field: without it --strict warns about missing attribution and the control fails itself, which reads precisely like the broken validator it exists to detect. Proven against two stubs, one erroring and one exiting 0 on everything: both produce a skip naming the reason, and the accounting still balances. verify.sh 22/22, gate-falsifiability.sh 23/23.
1 parent 056fe81 commit ce1d7c7

2 files changed

Lines changed: 36 additions & 10 deletions

File tree

‎.claude/verify.sh‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -600,17 +600,30 @@ fi
600600
#
601601
# So hand the validator a manifest that must be rejected, and if it accepts it, say the check
602602
# cannot measure here rather than reporting a pass on its behalf.
603+
# The control runs both ways, because each direction catches a different lie. A validator that
604+
# rejects everything looks identical to a broken repo: on CI the `claude` shim was on PATH while
605+
# its native binary was missing, so every invocation exited non-zero with an installation error
606+
# and nothing here could tell that apart from a genuinely bad manifest. A validator that accepts
607+
# everything looks identical to a healthy one. Require it to accept a good manifest AND reject a
608+
# bad one before believing a word it says about this repo's.
609+
ctl_state=usable
603610
if command -v claude >/dev/null 2>&1; then
604-
ctl=$(mktemp -d); mkdir -p "$ctl/.claude-plugin"
605-
printf '{"name":42,"version":"nope"}\n' > "$ctl/.claude-plugin/plugin.json"
606-
ctl_ok=1
607-
claude plugin validate --strict "$ctl" >/dev/null 2>&1 && ctl_ok=0
611+
ctl=$(mktemp -d)
612+
mkdir -p "$ctl/good/.claude-plugin" "$ctl/bad/.claude-plugin"
613+
# The good manifest carries author too: without it --strict warns about missing attribution
614+
# and the control fails itself, which reads exactly like a broken validator.
615+
printf '{"name":"probe","version":"0.0.1","description":"control","author":{"name":"control"}}\n' \
616+
> "$ctl/good/.claude-plugin/plugin.json"
617+
printf '{"name":42,"version":"nope"}\n' > "$ctl/bad/.claude-plugin/plugin.json"
618+
if ! ctl_out=$(claude plugin validate --strict "$ctl/good" 2>&1); then
619+
ctl_state="cannot run: $(printf '%s' "$ctl_out" | grep -v '^$' | head -1)"
620+
elif claude plugin validate --strict "$ctl/bad" >/dev/null 2>&1; then
621+
ctl_state="accepts anything: it passed a manifest with name:42"
622+
fi
608623
rm -rf "$ctl"
609-
else
610-
ctl_ok=1
611624
fi
612-
if command -v claude >/dev/null 2>&1 && [ "$ctl_ok" = 0 ]; then
613-
skip "plugin manifests valid" "validator accepted a manifest with name:42 — it is not validating here, so this check would only be reporting its own silence"
625+
if command -v claude >/dev/null 2>&1 && [ "$ctl_state" != usable ]; then
626+
skip "plugin manifests valid" "validator $ctl_state — reporting its answer would be reporting its own silence"
614627
elif command -v claude >/dev/null 2>&1; then
615628
errs=""
616629
# Exactly one warning is expected: claude/CLAUDE.md is the source for the global and overlay

‎.github/workflows/verify.yml‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,23 @@ jobs:
1616
- name: Install jq
1717
run: sudo apt-get update -qq && sudo apt-get install -y -qq jq
1818

19-
# Check 19 runs `claude plugin validate --strict`; without the CLI it skips, and the
19+
# Check 19 runs `claude plugin validate --strict`; without a working CLI it skips, and the
2020
# falsifiability suite skips its case 19 with it. Install it so CI proves the real thing.
21+
#
22+
# `npm install -g` alone was not enough and produced a false green for as long as the step
23+
# has existed. The npm package is a shim that fetches a platform-native binary in its
24+
# postinstall; that did not land here, so every `claude plugin validate` exited non-zero
25+
# with "claude native binary not installed" — and check 19's old output parsing read that
26+
# error as no findings and printed ok. CI has never once validated these manifests.
27+
#
28+
# So install it, then prove the binary answers before trusting the step. `claude
29+
# --version` failing here is a setup failure and should stop the run, not be discovered
30+
# later as a check that silently measures nothing.
2131
- name: Install Claude Code CLI
22-
run: npm install -g @anthropic-ai/claude-code
32+
run: |
33+
npm install -g @anthropic-ai/claude-code
34+
node "$(npm root -g)/@anthropic-ai/claude-code/install.cjs" || true
35+
claude --version
2336
2437
- name: Run the verification gate
2538
run: ./.claude/verify.sh

0 commit comments

Comments
 (0)