## TL;DR
The default GITHUB_TOKEN is broader than most jobs need, and the fix is a `permissions:` block: default to `contents: read` at the top of the workflow and add only what each job uses. Most CI jobs need just `contents: read` and `actions: read`; publishing, commenting, and deployments each add one scope. Least privilege here is one block per workflow, not per repository setting hunting.

## Error / query
```text
GITHUB_TOKEN permissions: the minimal set for CI
```

## Use this skill when
- Workflows fail with `Resource not accessible by integration`
- You want least-privilege tokens for CI
- Auditing what a workflow can actually do
- A job needs one extra scope and you want the exact name

## Not for this skill when
- You need a personal access token (different credential)
- Configuring a GitHub App's permissions (different surface)
- Authenticating to cloud providers (use OIDC, not the token)

## Steps

### Step 1: Set restrictive defaults at the workflow top level
```yaml
permissions:
  contents: read
```
Expected: every job in the workflow inherits read-only contents access unless overridden. This is the baseline; jobs that need nothing else are done here.

### Step 2: Add per-job scopes only where needed
```yaml
jobs:
  build:
    permissions:
      contents: read
      actions: read
  release:
    permissions:
      contents: write
      packages: write
```
Expected: the build job can read code and artifacts; the release job can additionally push tags and publish packages. Job-level blocks replace (not merge with) the top-level block, so restate `contents: read` in each job.

### Step 3: Map common CI tasks to their scopes
```text
checkout code            -> contents: read
download/upload artifacts -> actions: read / write
comment on PRs           -> pull-requests: write
create releases          -> contents: write
push container images    -> packages: write
request OIDC tokens      -> id-token permission set to write
cache access             -> actions: write
```
Expected: you can translate any `Resource not accessible` error into the missing scope by matching the action to this table.

### Step 4: Verify by running and reading the error
```bash
gh run view [run-id] --log-failed | grep -i "resource not accessible" | head -3
```
Expected: if a scope is still missing, the error names the resource. Add exactly that scope to the job, re-run, and confirm green. Iterate until the minimal set passes.

## Variant phrasings

### "github token resource not accessible by integration"
A missing scope. Step 3 maps the failing action to the permission to add.

### "least privilege github actions token"
Steps 1-2: restrictive top-level defaults plus per-job additions.

## Why it happens
The automatic token's permissions come from a matrix of repository settings, enterprise policies, and workflow-declared blocks, and the effective set is the intersection. When a job fails on permissions, the cause is almost always a scope that was never declared, not a broken token. Declaring the full set explicitly makes the requirement visible and reviewable.

## Edge cases and pitfalls
- Organization and enterprise settings can cap the token below what the workflow declares; if a declared scope still fails, check the org's workflow permission policy.
- `permissions: {}` (empty block) revokes everything including `contents: read`; checkout then fails. Always include at least `contents: read`.
- Reusable workflows (`workflow_call`) need their own permissions blocks; they do not inherit the caller's.
- Third-party actions may need scopes beyond your job's own (e.g. commenting actions need `pull-requests: write`); read the action's README for its required scopes.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_HMTq2n4Hr_LrySudwRmg0Q
