Skip to content

Fall back to the pure-node CLI so check 19 degrades to a visible skip #49

Fall back to the pure-node CLI so check 19 degrades to a visible skip

Fall back to the pure-node CLI so check 19 degrades to a visible skip #49

Workflow file for this run

name: verify
on:
push:
branches: [main]
pull_request:
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# The gate needs jq for the JSON checks and the install dry run. plutil does not exist
# on Linux, so the launchd check skips itself and says so.
- name: Install jq
run: sudo apt-get update -qq && sudo apt-get install -y -qq jq
# Check 19 runs `claude plugin validate --strict`; without a working CLI it skips, and the
# falsifiability suite skips its case 19 with it. Install it so CI proves the real thing.
#
# `npm install -g` alone was not enough and produced a false green for as long as the step
# has existed. The npm package is a shim that fetches a platform-native binary in its
# postinstall; that did not land here, 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. CI has never once validated these manifests.
#
# So install it, then prove the binary answers before trusting the step. `claude
# --version` failing here is a setup failure and should stop the run, not be discovered
# later as a check that silently measures nothing.
- name: Install Claude Code CLI
run: |
npm install -g @anthropic-ai/claude-code || true
root="$(npm root -g)/@anthropic-ai/claude-code"
node "$root/install.cjs" || true
# The native package resolves as an optional dependency and does not always arrive on
# a runner. The package ships a documented pure-node fallback for exactly this, so
# prefer it over failing the job: put a shim ahead of the shim.
if ! claude --version >/dev/null 2>&1 && [ -f "$root/cli-wrapper.cjs" ]; then
printf '#!/bin/sh\nexec node "%s/cli-wrapper.cjs" "$@"\n' "$root" | sudo tee /usr/local/bin/claude >/dev/null
sudo chmod +x /usr/local/bin/claude
fi
# Report, do not fail. Check 19 runs its own control and skips with a reason when the
# validator is unusable, so an unavailable CLI degrades to a visible skip in the gate
# output rather than a red run over something this repo does not control.
claude --version || echo "::warning::claude CLI unusable on this runner; check 19 will skip"
- name: Run the verification gate
run: ./.claude/verify.sh
# A gate that cannot fail proves nothing. This used to break one skill description and
# call it done, which left the other checks unproven — and two of them were not enforcing
# anything at the time. Every declared check now gets its own mutation, and check 16 of
# the gate fails if a check is added without one.
- name: Prove every check is falsifiable
run: ./tests/gate-falsifiability.sh
- name: Confirm the tree is restored
run: git diff --exit-code
# Everything above runs the gate. This installs for real, on Linux, into a disposable HOME.
# It exists because the worst bug this repo has shipped was Linux-only and invisible on the
# machine it was written on: hooks called /usr/bin/jq by absolute path, which is a macOS
# path, so off macOS the verify gate silently stopped blocking and the session hook injected
# nothing at all. A dry run cannot catch that. Only installing and firing the hooks can.
install-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install jq
run: sudo apt-get update -qq && sudo apt-get install -y -qq jq
- name: install.sh, for real
run: ./install.sh
- name: The installed tree matches the repo
run: ./bin/doctor --drift
# Expected counts are derived from the repo at run time, not hardcoded — a literal here
# went stale the first time a skill was added and failed a green tree.
- name: Everything landed
run: |
set -e
count() { ls -d $1 2>/dev/null | wc -l | tr -d ' '; }
for spec in "$HOME/.claude/skills/*/:claude/skills/*/" \
"$HOME/.claude/agents/*.md:claude/agents/*.md" \
"$HOME/.claude/commands/*.md:claude/commands/*.md" \
"$HOME/.claude/hooks/*.sh:claude/hooks/*.sh"; do
pat="${spec%:*}"; src="${spec##*:}"
got=$(count "$pat"); want=$(count "$src")
[ "$got" = "$want" ] && [ "$want" != 0 ] || { echo "$pat: got $got, repo has $want"; exit 1; }
echo "ok $pat -> $got (repo: $want)"
done
test -x "$HOME/.claude/hooks/verify-gate.sh"
- name: The Stop gate blocks on Linux, with and without jq on PATH
run: |
set -e
mkdir -p /tmp/g/repo/.claude /tmp/g/home/.config/agents /tmp/g/tmp /tmp/g/bin
printf '#!/usr/bin/env bash\necho "seeded failure"\nexit 1\n' > /tmp/g/repo/.claude/verify.sh
chmod +x /tmp/g/repo/.claude/verify.sh
printf '%s %s\n' "$(sha256sum /tmp/g/repo/.claude/verify.sh | cut -d' ' -f1)" \
/tmp/g/repo/.claude/verify.sh > /tmp/g/home/.config/agents/verify-trust
for t in bash sh cat cut grep sed awk tr rm mkdir env dirname basename sha256sum; do
ln -sf "$(command -v $t)" /tmp/g/bin/$t
done
out=$(printf '{"session_id":"ci"}' | env HOME=/tmp/g/home TMPDIR=/tmp/g/tmp \
CLAUDE_PROJECT_DIR=/tmp/g/repo bash claude/hooks/verify-gate.sh)
echo "$out" | jq -e '.decision=="block"' >/dev/null || { echo "did not block with jq"; exit 1; }
echo "ok blocks with jq"
sed 's#/usr/bin/jq#/nonexistent/jq#' claude/hooks/verify-gate.sh > /tmp/g/nojq.sh
out=$(printf '{"session_id":"ci2"}' | env PATH=/tmp/g/bin HOME=/tmp/g/home \
TMPDIR=/tmp/g/tmp CLAUDE_PROJECT_DIR=/tmp/g/repo bash /tmp/g/nojq.sh)
echo "$out" | jq -e '.decision=="block"' >/dev/null || { echo "did not block without jq"; exit 1; }
echo "ok blocks with no jq reachable"
- name: The session hook injects the routing block on Linux
run: |
set -e
n=$(printf '{"hook_event_name":"SessionStart"}' | bash claude/hooks/inject-session-context.sh | wc -c)
[ "$n" -gt 2000 ] || { echo "session hook injected only $n bytes"; exit 1; }
echo "ok session hook injected $n bytes"
- name: uninstall.sh --dry-run
run: ./uninstall.sh --dry-run