Harden CI: stop the build and publish jobs persisting the job credential - #5331
Conversation
actions/checkout writes the job's GITHUB_TOKEN into .git/config as an
http.extraheader and leaves it there for the rest of the job. Everything
that runs afterwards -- a build script, an installer signer, an installed
dependency -- can read it, and it is trivially captured into an uploaded
artifact. zizmor reports this as `artipacked`.
Six sites across four workflows, all of them jobs that never touch the
remote after checkout:
desktop-artifacts.yml macos / windows / linux -- the file contains no
git invocation at all; the `release` job that
does hold contents: write has no checkout and
attaches assets through the Releases API.
publish.yml builds the wheel and uploads with twine using
PYPI_API_TOKEN. No git, no Contents write.
moat-keystone-... spends its `issues: write` scope through the gh
CLI via GH_TOKEN, which this does not affect.
auto-deploy-cloud.yml the OSS checkout only. Every git command in that
job runs after `cd clawmetry-cloud`, against the
second checkout and its CLOUD_REPO_PAT; that one
is deliberately left alone and now says so.
This changes no `permissions:` block. persist-credentials governs only
whether the credential is left in .git/config; a scope spent through the
API, the gh CLI, an action's `token:` input or an explicitly built remote
URL is untouched. The release pipeline's write paths are all of that kind,
which is why these six had no consumer to lose.
Verified: 35 workflow + composite-action files parse; artipacked drops from
23 sites to 17 with no new site introduced (the PAT checkout still reports,
correctly); the SHA-pinning ratchet still holds at 17 references; the repo's
workflow guards report 192 passed, 2 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018oDZctSrHcpCUP8onVhJKR
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
|
✨ auto-fixed: merged latest main into branch to bring it up to date Generated by Claude Code |
|
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
|
✨ auto-fixed: merged latest main into branch (branch was BEHIND) Generated by Claude Code |
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
|
✨ auto-fixed: merged main (14572bc) into branch to resolve BEHIND state Generated by Claude Code |
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
|
✨ auto-fixed: merged main into branch (was BEHIND at 14572bc) Generated by Claude Code |
Product record: No-PRD: CI-only change confined to
.github/, a PRD-exempt path.Risk: Low. Six
with:additions toactions/checkoutsteps, no logic and nopermissions:change. Each of the six is a job that issues nogitcommand after checkout, so the credential being dropped has no consumer. Undone by reverting one commit.Summary
actions/checkoutwrites the job'sGITHUB_TOKENinto.git/configas anhttp.extraheaderand leaves it there for the rest of the job. Anything running afterwards — a build script, an installer signer, an installed dependency — can read it, and it is trivially captured into an uploaded artifact. zizmor reports this asartipacked. This continues the pass started in #5327 (read-only jobs) and #5330 (gate, nightly and audit jobs); this batch is the build and publish side.The six sites, and why each is safe
desktop-artifacts.ymlmacos,windows,linuxgitinvocation at all. Thereleasejob — the one holdingcontents: write— has no checkout and attaches assets through the Releases API.publish.ymlpublishPYPI_API_TOKEN. Itsid-token: write/attestations: writescopes are spent through the API.moat-keystone-drive-nightly.ymldriveissues: writescope through theghCLI via theGH_TOKENenv var, whichpersist-credentialsdoes not affect.auto-deploy-cloud.ymlpin-versiongitcommand in that job runs aftercd clawmetry-cloud, against the second checkout and itsCLOUD_REPO_PAT. That checkout is deliberately left as-is, and an inline comment now says so, so a later pass does not "finish the job" and break the force-push.The distinction this rests on
Four of these are workflows the release pipeline depends on, so it is worth being explicit: this changes no
permissions:block.persist-credentialsgoverns only whether the credential is left sitting in.git/config. A scope spent through the REST API, theghCLI, an action's owntoken:input, or an explicitly constructed remote URL is untouched by it. Every write path in these four workflows is one of those kinds — which is precisely why these six credentials had no consumer to lose.Test plan
Originally measured against
mainat0bec119(artipacked 23 → 17). #5330 has since merged, removing 10 sites frommain, andmainwas merged into this branch twice by the mergeability sweep. The numbers below are re-measured on the current head2114415againstmainat69682bc. The delta this PR is responsible for is unchanged at −6.yaml.safe_loadover all 35 workflow + composite-action files → all parse (re-verified after each merge)zizmor --offline --persona regular—artipackeddrops 13 → 7 sites, with a diff of before/after site lists confirming no new site appears. Theauto-deploy-cloudPAT checkout still reports, correctly, and only shifts line numbermainre-checked after both merges: still exactly these 4 files / 18 insertions, byte-identical to the original commitpython3 scripts/check_action_refs.py→ exit 0, 17 references, SHA-pinning ratchet still holdingpytest tests/test_workflow_yaml_valid.py tests/test_ci_workflow_invocations_are_real.py tests/test_e2e_nightly_workflow.py→ 192 passed, 2 skippedgit/gh/push/peter-evansto establish the "no consumer" claim per job rather than per fileNot in this batch, deliberately
The remaining
artipackedsites are checkouts whose job does push through the persisted credential —auto-quarantine.yml,release-on-merge.yml(tag + version-bump push),auto-deploy-cloud.yml's cloud checkout, and the twopeter-evans/create-pull-requestworkflows. Those need a different fix (an explicit token on the push), not this one, and are worth their own change.Generated by Claude Code