Skip to content

ci: scope the write permissions to the jobs that use them - #142

Merged
bgard68 merged 1 commit into
mainfrom
claude/new-session-muubr7
Aug 25, 2026
Merged

ci: scope the write permissions to the jobs that use them#142
bgard68 merged 1 commit into
mainfrom
claude/new-session-muubr7

Conversation

@bgard68

@bgard68 bgard68 commented Aug 25, 2026

Copy link
Copy Markdown
Owner

GHA002 flags three write scopes. None of them can be dropped — each is used by exactly one step, and removing it breaks the workflow that declares it:

Workflow Scope Used by
dependabot-lockfix.yml contents: write "Push the fix if anything changed" — git push origin HEAD:$BRANCH
dependabot-lockfix.yml actions: write "Approve the pull-request checks on the fixed commit" — POST .../actions/runs/{id}/approve
cleanup-runs.yml actions: write "Delete old runs of the high-frequency workflows" — DELETE .../actions/runs/{id}

Dropping contents: write leaves the lockfix job unable to push the repair it exists to make — the repair PR #137 just landed. Dropping actions: write on cleanup-runs.yml leaves a workflow whose only job is deleting runs, unable to delete runs.

So this takes the other half of the remediation, and goes further than a comment

The scanner's suggestion is "drop it, or state which step requires it." Both files did already say so inline, which the rule doesn't read. What they also did was declare those scopes at the workflow level, where they apply to every job the file will ever contain.

They now sit on the single job that needs them, with the workflow level dropped to contents: read — the shape container-build.yml already uses in this repo. A job added to either file tomorrow starts read-only instead of silently inheriting a push token.

That's the real finding underneath the false positive: not that the permission is excessive, but that its scope was.

Behaviour

Unchanged. The one existing job in each file resolves to exactly the permissions it had before — verified by parsing both files:

dependabot-lockfix.yml   workflow: {contents: read}   job: {contents: write, actions: write}
cleanup-runs.yml         workflow: {contents: read}   job: {actions: write, contents: read}

Whether the scanner is satisfied by a job-level grant is its business; least privilege at the workflow scope is worth having either way.

Also normalises a stray CRLF at the end of dependabot-lockfix.yml, which .gitattributes (* text=auto eol=lf) already asks for.

Also

dapper carries its own cleanup-runs.yml with the identical shape — same change in #143. It has no dependabot-lockfix.yml by design: workflow_run fires only from the default branch's copy, and that one already serves Dependabot branches based on either branch.

🤖 Generated with Claude Code

https://claude.ai/code/session_019h7znwAftvs36vD4YwKMRf


Generated by Claude Code

GHA002 flags contents: write and actions: write on dependabot-lockfix.yml, and
actions: write on cleanup-runs.yml, with the remediation "drop it, or state
which step requires it". Dropping any of the three breaks the workflow that
declares it — each is used by exactly one step:

  dependabot-lockfix  contents: write  "Push the fix if anything changed"
                                       (git push origin HEAD:$BRANCH)
  dependabot-lockfix  actions:  write  "Approve the pull-request checks on the
                                       fixed commit" (POST .../runs/{id}/approve)
  cleanup-runs        actions:  write  "Delete old runs of the high-frequency
                                       workflows" (DELETE .../runs/{id})

So this takes the second half of the remediation, and takes it further than a
comment. Both files declared their write scopes at the workflow level, where
they apply to every job the file will ever contain. They now sit on the single
job that needs them, with the workflow level dropped to contents: read — the
shape container-build.yml already uses. A job added to either file tomorrow
starts read-only instead of inheriting a push token.

Behaviour is unchanged: the one existing job in each file resolves to exactly
the permissions it had before. Whether the scanner is satisfied by a job-level
grant is its business; least privilege at the workflow scope is worth having
either way.

Also normalises a stray CRLF at the end of dependabot-lockfix.yml, which
.gitattributes (* text=auto eol=lf) already asks for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019h7znwAftvs36vD4YwKMRf
@bgard68
bgard68 merged commit 4842156 into main Aug 25, 2026
10 checks passed
@bgard68
bgard68 deleted the claude/new-session-muubr7 branch August 25, 2026 06:19
bgard68 added a commit that referenced this pull request Aug 25, 2026
Both scopes are used by exactly one step each — contents: write by "Push the
fix if anything changed" and actions: write by "Approve the pull-request checks
on the fixed commit" — so GHA002 has nothing to act on. The sentinel:accept
markers say that to the scanner in its own syntax, with the reason on the line
it flags.

Returns the declaration to the workflow level, where the markers sit on the
lines GHA002 reports, and drops the job-level block added in #142 so there is
one place to read the answer instead of two. Effective permissions are
unchanged: the file has a single job, which inherits exactly what it used to
override with.


Claude-Session: https://claude.ai/code/session_019h7znwAftvs36vD4YwKMRf

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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