Summary
27 Scorecard callers across the estate omit actions: read from their job-level
permissions: block. They are green only because their standards pin predates
2026-09-15. Each becomes startup_failure — silently, with no check run at all —
on its next pin bump.
metadatastician/burble is the index case: it bumped past the line in #230 and its
OSSF Scorecard is now startup_failure on main. Cure in
metadatastician/burble#231.
Mechanism
Two GitHub rules combine:
- A job-level
permissions: block replaces the workflow-level map rather than
merging with it.
- A called reusable workflow may not request more permission than its calling job
holds.
So a caller that grants actions: read at workflow level and then declares a job
block without it passes actions: none to the callee. If the callee's own job
declares actions: read, that is an escalation and GitHub rejects the run at
startup: zero jobs, no logs, no annotations, and no check run is created. The
workflow reads as absent from the checks list, not as red — which is why this has
gone unnoticed.
.github/workflows/scorecard-reusable.yml gained job-level actions: read at
a423e48a, 2026-09-15, "fix(ci): grant actions: read in the Scorecard
reusable" — Scorecard's Packaging check calls Actions.ListWorkflowRunsByFileName.
That commit is the tripwire.
The predictor is a conjunction, not the omission alone
The caller omits actions: read AND the pinned callee declares it. Measured
controls:
| repo |
caller |
pinned callee actions |
Scorecard |
betlang |
has all four |
@bd0df9ea None |
success |
laniakea |
omits actions |
@8f2ee508 None |
success |
manifesto |
omits actions |
@8f2ee508 None |
success |
burble (pre-fix) |
omits actions |
@0f2d9b54 read |
startup_failure |
And for burble specifically, the callee declares it at both its old and new pin,
which is why the pin bump alone could not have cured it:
| standards SHA |
job=scorecard actions |
0f2d9b54 |
read |
e977cc67 |
read |
4d104d32 |
None |
Honest counter-example
hyperpolymath/modshells is in the 27 and is currently startup_failure, but its
pinned callee (@81dbf2dd) does not declare actions, so this mechanism does not
explain it. It also has no actions.lock entry at all for scorecard.yml. Its
failure has a different, unidentified cause and needs its own diagnosis. Listed here
for completeness, not claimed as cured by this fix.
Population — 27 repos
Census over all 36 Scorecard callers in the estate clone set. Effective actions
scope resolved as job block if present, else workflow-level map.
| repo |
job |
pinned standards |
source of perms |
hyperpolymath/cloudflare-dns-terraform |
analysis |
8f2ee508 |
job |
hyperpolymath/ephapax |
scorecard |
571cc734 |
job |
hyperpolymath/feedback-o-tron |
scorecard |
8f2ee508 |
job |
hyperpolymath/julia-professional-registry |
scorecard |
84355587 |
job |
hyperpolymath/kitchenspeak |
analysis |
571cc734 |
job |
hyperpolymath/laniakea |
analysis |
8f2ee508 |
job |
hyperpolymath/lithoglyph |
scorecard |
84355587 |
job |
hyperpolymath/llm-grace |
analysis |
8f2ee508 |
job |
hyperpolymath/maa-framework |
scorecard |
84355587 |
job |
hyperpolymath/manifesto |
analysis |
8f2ee508 |
job |
hyperpolymath/methodologies |
scorecard |
84355587 |
job |
hyperpolymath/modshells |
analysis |
81dbf2dd |
job |
hyperpolymath/ochrance-framework |
scorecard |
81dbf2dd |
job |
hyperpolymath/phantom-metal-taste |
scorecard |
81dbf2dd |
job |
hyperpolymath/plasma-parser-writer |
scorecard |
d7c22711 |
job |
hyperpolymath/rpa-elysium |
scorecard |
84355587 |
job |
hyperpolymath/safe-brute-force |
scorecard |
81dbf2dd |
job |
hyperpolymath/seamstress |
scorecard |
84355587 |
job |
hyperpolymath/session-sentinel |
analysis |
8f2ee508 |
job |
hyperpolymath/twingate-helm-deploy |
analysis |
81dbf2dd |
job |
hyperpolymath/vcl-ut |
scorecard |
5a93d9d5 |
job |
hyperpolymath/volumod |
scorecard |
81dbf2dd |
job |
hyperpolymath/wokelang |
scorecard |
81dbf2dd |
job |
metadatastician/chronicles-of-slavia |
scorecard |
fcb86691 |
job |
metadatastician/first-post |
scorecard |
bd0df9ea |
job |
metadatastician/harvard-dehallucinator |
scorecard |
bd0df9ea |
job |
metadatastician/marid |
scorecard |
da2c748a |
job |
Not affected — 9 repos: betlang, my-lang, scripts,
universal-chat-extractor (explicit actions: read in the job block);
hybrid-automation-router, pons-asinorum, proven-tests-and-benches,
metadatastician-governance (no job block — they inherit a workflow-level map that
already contains it, verified); burble (cured by #231).
What does not catch it
actionlint returns rc=0 on an affected file. It does not model
reusable-workflow permission ceilings.
- There is nothing to read after the fact.
GET .../actions/runs/<id>/logs
returns 404 on a startup_failure, /jobs returns 0, and there are no
annotations.
- A failing-checks list will not show it, because no check run is created.
Acceptance criteria
Note on scope
This is preventive, not a live outage: 26 of the 27 are green today. Filing under the
standing stopping rule — a new finding becomes an issue with acceptance criteria
rather than blocking anything in flight.
🤖 Generated with Claude Code
https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm
Summary
27 Scorecard callers across the estate omit
actions: readfrom their job-levelpermissions:block. They are green only because theirstandardspin predates2026-09-15. Each becomes
startup_failure— silently, with no check run at all —on its next pin bump.
metadatastician/burbleis the index case: it bumped past the line in #230 and itsOSSF Scorecard is now
startup_failureonmain. Cure inmetadatastician/burble#231.
Mechanism
Two GitHub rules combine:
permissions:block replaces the workflow-level map rather thanmerging with it.
holds.
So a caller that grants
actions: readat workflow level and then declares a jobblock without it passes
actions: noneto the callee. If the callee's own jobdeclares
actions: read, that is an escalation and GitHub rejects the run atstartup: zero jobs, no logs, no annotations, and no check run is created. The
workflow reads as absent from the checks list, not as red — which is why this has
gone unnoticed.
.github/workflows/scorecard-reusable.ymlgained job-levelactions: readata423e48a, 2026-09-15, "fix(ci): grant actions: read in the Scorecardreusable" — Scorecard's Packaging check calls
Actions.ListWorkflowRunsByFileName.That commit is the tripwire.
The predictor is a conjunction, not the omission alone
The caller omits
actions: readAND the pinned callee declares it. Measuredcontrols:
actionsbetlang@bd0df9eaNonelaniakeaactions@8f2ee508Nonemanifestoactions@8f2ee508Noneburble(pre-fix)actions@0f2d9b54readAnd for burble specifically, the callee declares it at both its old and new pin,
which is why the pin bump alone could not have cured it:
job=scorecardactions0f2d9b54e977cc674d104d32Honest counter-example
hyperpolymath/modshellsis in the 27 and is currentlystartup_failure, but itspinned callee (
@81dbf2dd) does not declareactions, so this mechanism does notexplain it. It also has no
actions.lockentry at all forscorecard.yml. Itsfailure has a different, unidentified cause and needs its own diagnosis. Listed here
for completeness, not claimed as cured by this fix.
Population — 27 repos
Census over all 36 Scorecard callers in the estate clone set. Effective
actionsscope resolved as job block if present, else workflow-level map.
hyperpolymath/cloudflare-dns-terraformanalysis8f2ee508hyperpolymath/ephapaxscorecard571cc734hyperpolymath/feedback-o-tronscorecard8f2ee508hyperpolymath/julia-professional-registryscorecard84355587hyperpolymath/kitchenspeakanalysis571cc734hyperpolymath/laniakeaanalysis8f2ee508hyperpolymath/lithoglyphscorecard84355587hyperpolymath/llm-graceanalysis8f2ee508hyperpolymath/maa-frameworkscorecard84355587hyperpolymath/manifestoanalysis8f2ee508hyperpolymath/methodologiesscorecard84355587hyperpolymath/modshellsanalysis81dbf2ddhyperpolymath/ochrance-frameworkscorecard81dbf2ddhyperpolymath/phantom-metal-tastescorecard81dbf2ddhyperpolymath/plasma-parser-writerscorecardd7c22711hyperpolymath/rpa-elysiumscorecard84355587hyperpolymath/safe-brute-forcescorecard81dbf2ddhyperpolymath/seamstressscorecard84355587hyperpolymath/session-sentinelanalysis8f2ee508hyperpolymath/twingate-helm-deployanalysis81dbf2ddhyperpolymath/vcl-utscorecard5a93d9d5hyperpolymath/volumodscorecard81dbf2ddhyperpolymath/wokelangscorecard81dbf2ddmetadatastician/chronicles-of-slaviascorecardfcb86691metadatastician/first-postscorecardbd0df9eametadatastician/harvard-dehallucinatorscorecardbd0df9eametadatastician/maridscorecardda2c748aNot affected — 9 repos:
betlang,my-lang,scripts,universal-chat-extractor(explicitactions: readin the job block);hybrid-automation-router,pons-asinorum,proven-tests-and-benches,metadatastician-governance(no job block — they inherit a workflow-level map thatalready contains it, verified);
burble(cured by #231).What does not catch it
actionlintreturns rc=0 on an affected file. It does not modelreusable-workflow permission ceilings.
GET .../actions/runs/<id>/logsreturns 404 on a
startup_failure,/jobsreturns 0, and there are noannotations.
Acceptance criteria
actions: readin the effective permission set fortheir Scorecard caller job.
LATENT-MISSING.successto any other state as aresult of the change.
caller scopes in-file, or a gate reports a caller whose effective permissions
are a strict subset of the pinned callee's job-level declaration.
(AC4 is the durable cure; AC1 alone is a one-time sweep that the next callee
permission change will defeat.)
modshellshas a separate diagnosis recorded; it is not closed byAC1 evidence alone.
Note on scope
This is preventive, not a live outage: 26 of the 27 are green today. Filing under the
standing stopping rule — a new finding becomes an issue with acceptance criteria
rather than blocking anything in flight.
🤖 Generated with Claude Code
https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm