Summary
gh actions-lock --verify (v0.1.6) cannot see job-level uses: — reusable-workflow
calls. It sees only step-level refs. It is therefore wrong in both directions on the
same ref class, and scripts/reconcile-scorecard-actions-lock.rb turns the false-positive
direction into a red OSSF Scorecard for repos that have no defect.
Index case: metadatastician/burble. Its Scorecard has been startup_failure (separate
cause, cured in metadatastician/burble#231); the moment it could actually run, it failed at
reconcile with:
Scorecard reconciliation failed: Native action-lock verification failed for
.github/workflows/release.yml
The tool's claim, and why it is false
stale — lockfile pins slsa-framework/slsa-github-generator@f7dd8c54… but no
uses: in this workflow references it
release.yml:144 references it plainly:
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a # v2.1.0
The lock entry is correct as written. The lock records owner/repo@ref with the action
subpath normalised away — that is the lock format's own spelling and it is what GitHub
accepts. Deleting the entry, as the tool's remediation advises, would break the lock.
Root cause — four-arm control, run locally against the real tree
| arm |
tree |
verdict |
| A |
release.yml as committed (step refs + 1 job-level slsa ref) |
valid:false stale |
| B |
trailing # v2.1.0 comment stripped |
valid:false — comment refuted |
| C |
slsa dep record given a uses: [] closure |
valid:false — missing closure refuted |
| D |
scorecard.yml job-level ref repointed to a SHA absent from the lock |
valid:true, findings [] |
Arm D is the proof and the alarming half. A workflow whose only ref is a job-level
reusable call verifies vacuously — zero refs extracted, so nothing to compare. The tool
passed a workflow pinning a SHA its lock does not record.
So burble's governance.yml, hypatia-scan.yml and scorecard.yml verify green for the
wrong reason, not because the tool handled them.
The exact trigger for the false stale
Mixed workflow (step-level refs the tool does see) AND the per-workflow lock list
contains an entry for the job-level ref (which it does not see) — that entry is then left
unmatched and reported stale.
Census over the 40-repo estate clone set: 8 mixed workflows, of which the tool actually
rejects 2. Not 8 — the other 6 differ on the second condition.
| repo |
workflow |
job-level ref in per-workflow lock list? |
tool |
metadatastician/burble |
release.yml |
yes (slsa-framework/…) |
valid:false stale |
hyperpolymath/ephapax |
rust-ci.yml |
yes (hyperpolymath/standards@571cc734) |
valid:false stale |
hyperpolymath/kitchenspeak |
release.yml |
no |
valid:true |
hyperpolymath/session-sentinel |
release.yml |
no |
valid:true |
hyperpolymath/llm-grace |
release.yml |
no |
valid:true |
hyperpolymath/methodologies |
release.yml |
no |
valid:true |
hyperpolymath/hybrid-automation-router |
release.yml |
no |
valid:true |
hyperpolymath/universal-chat-extractor |
secret-scanner.yml |
n/a |
valid:false unreachable-pin (different defect) |
sha-as-ref findings do not set valid:false; only stale does. That is why 6 other
burble workflows carry findings and still pass.
A second, separate question this surfaced — NOT answered here
The five repos marked "no" above each have a release.yml whose lock key exists but
whose list omits the slsa job-level ref the workflow uses. Whether GitHub enforces the
lock for job-level reusable refs, and therefore whether those runs would be rejected at
startup, is untested:
- Four of the five have never run
release.yml — it is tag-triggered.
hybrid-automation-router did run, and did not startup-fail — but that run
(2bc990a8, 2026-08-27T05:02:50Z) predates its own lock commit 6f900c86
(05:11:24Z) by nine minutes, and its current lock 816c2bfb by three weeks. It is not
evidence either way.
So this is a latent risk on 5 repos whose next tag is the first real test. It needs its
own controlled experiment and is not claimed as a defect here.
Impact on this repo
scripts/reconcile-scorecard-actions-lock.rb (line ~58) shells out to:
Open3.capture3('gh', 'actions-lock', relative, '--verify', '--no-interactive',
'--json=valid,findings', chdir: root)
raise "Native action-lock verification failed for #{relative}" unless status.success? && ...
and raises on valid:false. The reusable then fails by design. So a known-false verdict
from a blind verifier becomes a red security check on a correct repository — and, per arm D,
the same verifier cannot detect the real desync class this estate's lock campaign exists to
fix.
Acceptance criteria
Do not "repair" the accused repos
The accusation is the bug. burble's and ephapax's locks are correct; removing the flagged
entry would introduce the very desync the lock exists to prevent.
🤖 Generated with Claude Code
https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm
Summary
gh actions-lock --verify(v0.1.6) cannot see job-leveluses:— reusable-workflowcalls. It sees only step-level refs. It is therefore wrong in both directions on the
same ref class, and
scripts/reconcile-scorecard-actions-lock.rbturns the false-positivedirection into a red OSSF Scorecard for repos that have no defect.
Index case:
metadatastician/burble. Its Scorecard has beenstartup_failure(separatecause, cured in metadatastician/burble#231); the moment it could actually run, it failed at
reconcile with:
The tool's claim, and why it is false
release.yml:144references it plainly:The lock entry is correct as written. The lock records
owner/repo@refwith the actionsubpath normalised away — that is the lock format's own spelling and it is what GitHub
accepts. Deleting the entry, as the tool's
remediationadvises, would break the lock.Root cause — four-arm control, run locally against the real tree
release.ymlas committed (step refs + 1 job-level slsa ref)valid:falsestale# v2.1.0comment strippedvalid:false— comment refuteduses: []closurevalid:false— missing closure refutedscorecard.ymljob-level ref repointed to a SHA absent from the lockvalid:true, findings[]Arm D is the proof and the alarming half. A workflow whose only ref is a job-level
reusable call verifies vacuously — zero refs extracted, so nothing to compare. The tool
passed a workflow pinning a SHA its lock does not record.
So burble's
governance.yml,hypatia-scan.ymlandscorecard.ymlverify green for thewrong reason, not because the tool handled them.
The exact trigger for the false
staleMixed workflow (step-level refs the tool does see) AND the per-workflow lock list
contains an entry for the job-level ref (which it does not see) — that entry is then left
unmatched and reported stale.
Census over the 40-repo estate clone set: 8 mixed workflows, of which the tool actually
rejects 2. Not 8 — the other 6 differ on the second condition.
metadatastician/burblerelease.ymlslsa-framework/…)valid:falsestalehyperpolymath/ephapaxrust-ci.ymlhyperpolymath/standards@571cc734)valid:falsestalehyperpolymath/kitchenspeakrelease.ymlvalid:truehyperpolymath/session-sentinelrelease.ymlvalid:truehyperpolymath/llm-gracerelease.ymlvalid:truehyperpolymath/methodologiesrelease.ymlvalid:truehyperpolymath/hybrid-automation-routerrelease.ymlvalid:truehyperpolymath/universal-chat-extractorsecret-scanner.ymlvalid:falseunreachable-pin(different defect)sha-as-reffindings do not setvalid:false; onlystaledoes. That is why 6 otherburble workflows carry findings and still pass.
A second, separate question this surfaced — NOT answered here
The five repos marked "no" above each have a
release.ymlwhose lock key exists butwhose list omits the slsa job-level ref the workflow uses. Whether GitHub enforces the
lock for job-level reusable refs, and therefore whether those runs would be rejected at
startup, is untested:
release.yml— it is tag-triggered.hybrid-automation-routerdid run, and did not startup-fail — but that run(
2bc990a8, 2026-08-27T05:02:50Z) predates its own lock commit6f900c86(05:11:24Z) by nine minutes, and its current lock
816c2bfbby three weeks. It is notevidence either way.
So this is a latent risk on 5 repos whose next tag is the first real test. It needs its
own controlled experiment and is not claimed as a defect here.
Impact on this repo
scripts/reconcile-scorecard-actions-lock.rb(line ~58) shells out to:and raises on
valid:false. The reusable then fails by design. So a known-false verdictfrom a blind verifier becomes a red security check on a correct repository — and, per arm D,
the same verifier cannot detect the real desync class this estate's lock campaign exists to
fix.
Acceptance criteria
stalefinding whosedependencyis referenced by a job-level
uses:in that workflow. Either pre-filter suchfindings, or stop treating
staleas fatal.not be reported stale, and an arm-D tree (job-level ref pinned to a SHA absent from
the lock) that must not be reported valid. (AC2 is the durable half — AC1 alone
re-breaks as soon as the tool changes shape.)
metadatastician/burbleandhyperpolymath/ephapaxreach a green Scorecardwithout any change to their
actions.lockor workflow YAML, since neither isdefective.
gh-actions-lockwith the four-arm control.(This is item (e) of the seven upstream defects already ruled to be filed separately.)
or explicitly recorded as open. It must not be closed by AC1–AC4 evidence.
Do not "repair" the accused repos
The accusation is the bug. burble's and ephapax's locks are correct; removing the flagged
entry would introduce the very desync the lock exists to prevent.
🤖 Generated with Claude Code
https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm