๐Ÿ”’ Workflow token-permissions hardening

Resolves 6 high-severity 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.

The fix pattern

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: ...

Per-file changes

WorkflowBeforeAfterWhy
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.

Why this is safe (no behavior change)

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.

Verification

$ 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.