Skip to content

Refuse to resolve a ref under a trigger that carries secrets - #262

Merged
aywrite merged 1 commit into
masterfrom
claude/ci-actions-extraction-design-gvi4ht
Sep 16, 2026
Merged

aywrite merged 1 commit into
masterfrom
claude/ci-actions-extraction-design-gvi4ht

Conversation

@aywrite

@aywrite aywrite commented Sep 16, 2026

Copy link
Copy Markdown
Owner

resolve_ref.sh takes a pull request number, fetches refs/pull/<n>/head and
builds it. Here that has always been safe: the match workflows are dispatch
only, declare contents: read and name no secret. Nothing enforced any of
that, and the tools this script sits beside are now used outside this
repository, where a pull_request_target wiring would hand a fork arbitrary
code execution with a write token.

The script now refuses unless the trigger is one where the ref was chosen by
somebody who can already push: workflow_dispatch, push, schedule or
release. It also runs when GITHUB_EVENT_NAME is unset, which means it is
not on a runner at all.

pull_request is deliberately not on the list. A fork's pull_request run
gets a read-only token and no secrets, so it is arguably safe, but allowing it
would break the rule the guard states and nothing here needs it.

What it does not check, and why

Not permissions, and not secrets. Neither is readable from inside a job: the
permissions: block is exposed to nothing running there, and a secret is in
the environment only when a step above passes it, so its absence proves nothing
about what else the job could be handed. A script that tested for
GITHUB_TOKEN and called that a permission check would be describing its own
environment and naming it something it is not.

The trigger is readable, and it is the part that carries the risk. The runner
sets GITHUB_EVENT_NAME on every job, an env: block cannot override a
GITHUB_ name, and a workflow entered through workflow_call sees the
caller's event, so the check holds when the release workflow calls
strength.yml.

So the guard does not say the job is unprivileged. A workflow_dispatch
workflow that granted contents: write and passed a secret would pass it.
That is the workflow's to declare, and docs/DEVELOPMENT.md now says so, and
names the permissions: contents: read at the top of both match workflows as
the part a reader has to write for themselves.

288 tests pass, seven of them new. The existing helper now sets
GITHUB_EVENT_NAME explicitly rather than inheriting it, without which the
file would fail in CI, where the tests themselves run under pull_request.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XhA13EsiXpMQ5vea2P3Fc9


Generated by Claude Code

resolve_ref.sh takes a pull request number, fetches refs/pull/<n>/head, and
the job builds that commit and plays it. Here that is safe: both match
workflows run from the actions tab, which needs write access, they declare
contents: read at the top and neither is given a secret. Wired instead to
pull_request_target, to workflow_run or to a comment event, the same script
would run a fork's branch in a job holding this repository's secrets and a
token that can write to it.

The script now runs under workflow_dispatch, push, schedule and release, and
exits non-zero under anything else. Those are the triggers where the ref was
chosen by somebody who can already push here. With GITHUB_EVENT_NAME unset it
runs, since that is a machine and not a runner, and a workflow called with
workflow_call sees the event of the workflow that called it, so the release
workflow's call is checked as well. The tests name the trigger rather than
inheriting it, because they run inside a workflow themselves and would
otherwise check whatever started them.

That is the whole of what a script can check, and the documentation says so
rather than a comment saying it. A job's permissions block is exposed to
nothing running inside the job, and the repository's secrets cannot be
enumerated from a step, so the check says the ref came from somebody with
write access and says nothing about how privileged the job is. The
contents: read at the top of strength.yml and calibrate.yml is what it does
not replace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhA13EsiXpMQ5vea2P3Fc9
@github-actions

Copy link
Copy Markdown
Contributor

Speed against 69da4704

Both sides built and run on this runner in this job, the way
scripts/speed.sh measures a perf commit. The change is between
medians, and the spread beside it is what the change has to be
read against. The node counts are the search's: they move when
the search does, and a speed change leaves them alone.

round     base nps  candidate nps
    1      7535788        7711769
    2      7632628        7682643
    3      7679929        7674840
    4      7721465        7683177
    5      7736009        7743223
    6      7699415        7681499
    7      7693337        7664638
    8      7667980        7695234
    9      7710908        7672725

             nodes    time  median nps  fastest nps
base       3868987  0.50 s     7693337      7736009
candidate  3868987  0.50 s     7682643      7743223
change                           -0.1%        +0.1%

the change is inside the spread, so the medians make no claim; the
fastest column is the steadier comparison when the machine was not
quiet

Speed: -0.1% (bench nps, 9 interleaved rounds vs 69da4704, spread 2.6%)

@aywrite
aywrite merged commit 4b1b80f into master Sep 16, 2026
19 of 20 checks passed
@aywrite
aywrite deleted the claude/ci-actions-extraction-design-gvi4ht branch September 16, 2026 11:32
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.

2 participants