# 🔒 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

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.

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