TokenPermissionsID CodeQL alerts. Pattern: top-level
permissions: contents: read (default-deny), per-job overrides only where a step truly needs writes. Defense in depth โ if a step somehow leaks GITHUB_TOKEN, the read-only default limits the blast radius.
before after โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ permissions: permissions: contents: write contents: read pull-requests: write jobs: jobs: watch: watch: runs-on: ubuntu-latest runs-on: ubuntu-latest steps: ... permissions: contents: write pull-requests: write steps: ...
| Workflow | Before | After | Why |
|---|---|---|---|
contract-parity.yml |
no top-level | contents: read | Add explicit default-deny. Job already had its own contents: read. |
hook-contract-py.yml |
no top-level | contents: read | Same โ explicit default-deny. |
cc-support-window-bump.yml |
contents+pr: write (top) | contents: read (top) writes scoped to job |
peter-evans/create-pull-request needs writes โ but only the single job that runs it. |
labs-version-watch.yml |
contents+pr: write (top) | contents: read (top) writes scoped to job |
gh pr create step needs pull-requests:write โ scoped to that job. |
claude-release-watch.yml |
contents+issues+pr: write (top) | contents: read (top) writes scoped to job |
Single watch job needs all three (commit snapshots, file issues, dedupe PRs). |
publish-hook-contract-py.yml |
contents: read (top) | contents: read (top) + inline comment on github-release |
github-release job has contents: write for gh release create. Minimum required. Annotated. |
Runtime privileges for any given step are the same. A job's effective GITHUB_TOKEN scope is its permissions block โ top-level is just the default for jobs that don't declare their own. Moving the write declaration from top-level to job-level changes the default for non-existent jobs (there are none) but preserves the writes for the jobs that need them.
If a new job is added in the future without its own permissions block, it now defaults to read-only โ forcing the author to think about what scope it actually needs.
$ actionlint .github/workflows/{contract-parity,hook-contract-py,cc-support-window-bump,labs-version-watch,claude-release-watch,publish-hook-contract-py}.yml
(no errors on YAML structure or permissions)
Pre-existing shellcheck info-level notes in script bodies (SC2129, SC2016)
are unrelated to this change โ they sit inside run: | blocks
this PR does not touch.