Skip to content

Harden CI: install the Claude Code CLI from a lockfile - #5571

Merged
vivekchand merged 1 commit into
mainfrom
harden/pin-claude-cli-lockfile
Sep 5, 2026
Merged

Harden CI: install the Claude Code CLI from a lockfile#5571
vivekchand merged 1 commit into
mainfrom
harden/pin-claude-cli-lockfile

Conversation

@vivekchand

Copy link
Copy Markdown
Owner

Product record: No-PRD: CI-only supply-chain hardening under .github/. No product behaviour, flag, default or endpoint changes — closes a zizmor adhoc-packages finding on three workflows.

Risk: Low, and contained to CI. The three affected jobs are the daily harness audit and the two i18n translation bots; none is on the release or deploy path, and none ships anything to PyPI or the cloud. If the pinned CLI were wrong the symptom is those three jobs failing loudly at the install step, not a bad artifact — and the revert is this diff. npm ci was run from the committed lock in a clean directory before pushing, so the install path is exercised rather than assumed. Nothing in clawmetry/, routes/ or dashboard.py is touched.


Summary

  • Three workflows installed the Claude Code CLI with npm install -g @anthropic-ai/claude-code — no version, no lockfile — so each run resolved the registry's moving latest and verified nothing about what it got. zizmor reports each as adhoc-packages (installs a package outside of a lockfile).

  • None of the three is a bystander to what it installs, and each then runs that binary over repository content:

    workflow token the job holds
    harness-observability-audit issues: write + CLAUDE_CODE_OAUTH_TOKEN
    i18n-autotranslate contents: write, pull-requests: write + CLAUDE_CODE_OAUTH_TOKEN
    i18n-docs-autotranslate contents: write, pull-requests: write + CLAUDE_CODE_OAUTH_TOKEN
  • Replaced with npm ci against a committed .github/claude-cli/package-lock.json, which installs exactly the recorded tree and checks every tarball against its integrity hash. A bare @version pin does neither — it re-resolves transitive deps on each run and verifies nothing. One shared manifest rather than three, because all three consume the same CLI.

Why 2.1.251 and not latest

.github/dependabot.yml puts a 7-day cooldown on every ecosystem, on the stated reasoning that a compromised release is usually yanked within a day or two. Latest is 2.1.261 (published yesterday); 2.1.251 is the newest release that clears that window, so the initial pin honours the same rule the automated bumps already follow. The new npm entry for /.github/claude-cli is what keeps the lock moving rather than frozen — the pin/updater pairing that file already documents for the SHA pins.

One behaviour change worth flagging

The two i18n jobs set up Python but never Node, so they ran on whatever the runner image happened to ship. The CLI declares engines: node >=22, so both now pin setup-node 22 (SHA-pinned, same ref already used elsewhere in the repo), matching harness-observability-audit. That makes npm ci reproducible there rather than image-dependent.

This is the same fix clawmetry-pro merged for its own adhoc-packages finding, applied to this repo's three call sites.

Test plan

  • zizmor: 53 findings → 50, and the three that went are exactly these adhoc-packages on the three workflows. No new finding appears.
  • npm ci from the committed lock in a clean directory exits 0, and claude --version reports 2.1.251.
  • All 36 workflow files and dependabot.yml parse under yaml.safe_load; both manifests parse as JSON.
  • scripts/check_action_refs.py: 17 distinct references, all SHA-pinned (the added setup-node included).
  • Step order and if: guards verified per job — setup-node precedes the install step, and every new step in the i18n jobs carries the existing if: env.SKIP != '1' guard so the token-absent skip path is unchanged.
  • node_modules/ under the new directory is already covered by .gitignore (verified with git check-ignore).

🤖 Generated with Claude Code

https://claude.ai/code/session_01SGHzpWqT9vqseYSeaebJvw


Generated by Claude Code

Three workflows installed the CLI with `npm install -g
@anthropic-ai/claude-code` -- no version, no lockfile. Each run resolved the
registry's moving `latest`, so the job took whatever the tag pointed at the
moment it ran, and verified nothing about it. zizmor reports each as
`adhoc-packages` (installs a package outside of a lockfile).

None of the three is a bystander to what it installs:

  harness-observability-audit  issues: write        + CLAUDE_CODE_OAUTH_TOKEN
  i18n-autotranslate           contents/PRs: write  + CLAUDE_CODE_OAUTH_TOKEN
  i18n-docs-autotranslate      contents/PRs: write  + CLAUDE_CODE_OAUTH_TOKEN

All three then run that binary over repository content, so an unverified
tree executes with the job's token.

Replaced with `npm ci` against a committed .github/claude-cli/package-lock.json,
which installs exactly the recorded tree and checks every tarball against its
integrity hash. A bare `@version` pin does neither -- it re-resolves transitive
deps each run and verifies nothing. One shared manifest rather than three
because all three consume the same CLI.

Pinned 2.1.251 rather than latest (2.1.261): .github/dependabot.yml sets a
7-day cooldown on every ecosystem, on the reasoning that a compromised release
is usually yanked within a day or two. 2.1.251 is the newest release that
clears that window, so the initial pin honours the same rule the automated
bumps do. The new npm entry for /.github/claude-cli keeps the lock moving
rather than frozen -- the pin/updater pairing that file already documents for
the SHA pins.

The two i18n jobs set up Python but never Node, so they relied on whatever the
runner image shipped. The CLI declares `engines: node >=22`, so both now pin
setup-node 22, matching harness-observability-audit.

Verified:
- zizmor: 53 findings -> 50, and the three that went are exactly these
  `adhoc-packages`. No new finding appears.
- `npm ci` from the committed lock in a clean directory exits 0 and
  `claude --version` reports 2.1.251.
- All 36 workflow files and dependabot.yml parse under yaml.safe_load;
  both manifests parse as JSON.
- scripts/check_action_refs.py: 17 references, all SHA-pinned.

No-PRD: CI-only supply-chain hardening under .github/. No product behaviour,
flag, default or endpoint changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SGHzpWqT9vqseYSeaebJvw
@8090-software-factory

Copy link
Copy Markdown

✅ Drift Bot (ClawMetry): no drift detected

Drift Bot analyzed the changed files against this project's blueprints and requirements and found no drift.

Copy link
Copy Markdown
Owner Author

Non-mergeable PR sweeper — run summary (2026-09-05)

Scanned all three repos (clawmetry, clawmetry-cloud, clawmetry-landing) for open, non-draft PRs authored by vivekchand with merge conflicts (mergeable_state: dirty).

Bucket Count PRs
rebased 0
comment-only (complex conflict) 0
skipped (bot/draft/label) 36 bots: #5567, #5461, #5511, #5403, #5376, #5261, #5260, #5259, #5258, #5257, #5256, #5255, #5245, #5244, #5242, #5241, #5158, #5157, #5156, #5155, #5154, #5153; drafts: #5569, #5561
skipped (external author) 0
already clean 2 clawmetry#5571, clawmetry-cloud#2298
blocked (CI/reviews, no git conflict) 7 #5564, #5382, #5305, #5216, #5204, #5114, #5055

Result: No merge conflicts to resolve across any repo. All blocked PRs are held by CI status checks or review requirements, not by a dirty merge base.


Generated by Claude Code

@vivekchand
vivekchand merged commit d70a1b9 into main Sep 5, 2026
37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant