| name | codeql-permission-classification |
| description | Permission requirements for GitHub Actions |
CodeQL Permission Classification
Single source of truth for GitHub Actions permission requirements.
Permission Types
GitHub Actions provides these permission scopes:
contents # Read/write repository content (checkout, tags, releases)
pull-requests # Read/write PR comments, reviews, assignments
issues # Read/write issue comments, labels, projects
deployments # Read/write deployment status
packages # Read/write packages
actions # Read/write GitHub Actions (runners, artifacts, caches)
checks # Read/write check runs and annotations
statuses # Read/write commit statuses
security-events # Read/write code scanning and secret scanning results
Common Actions → Permissions Matrix
| Action | Required Permissions | Use Case |
|---|
actions/checkout@v6 | contents: read | Clone repository |
actions/upload-artifact@v6 | None (usually) | Store build artifacts |
actions/download-artifact@v6 | None (usually) | Retrieve artifacts |
actions/setup-node@v6 | None | Install Node.js |
actions/github-script@v6 | Depends on script | Usually contents: read minimum |
actions/create-release@v1 | contents: write | Create GitHub release |
github/codeql-action/upload-sarif@v2 | security-events: write | Upload CodeQL results |
Decision Tree
Q1: Does your job use actions/checkout?
- YES → Add
contents: read
- NO → Continue to Q2
Q2: Does your job modify repository (create PR, tag, release)?
- YES → Add
contents: write
- NO → Continue to Q3
Q3: Does your job post comments to PR or issues?
- YES → Add
pull-requests: write (PR) or issues: write (issues)
- NO → Continue to Q4
Q4: Does your job modify deployments?
- YES → Add
deployments: write
- NO → Continue to Q5
Q5: Does your job use github.rest API?
- YES → Add permission for the API endpoint you're calling
- NO → Continue to Q6
Q6: Does your job call a reusable workflow?
- YES → Add union of ALL permissions needed by that workflow's jobs
- NO → Continue to Q7
Q7: Is your job just aggregating results with no API access?
- YES → Add
permissions: {} (empty - no token needed)
- NO → You probably missed something. Review again.
Examples
Example 1: Simple Checkout & Test
test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- run: npm test
Example 2: Create Release
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- uses: actions/create-release@v1
with:
tag_name: v1.0.0
Example 3: Post PR Comment
comment:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v6
- run: npm run lint | tee lint-result.txt
- uses: actions/github-script@v6
with:
script: |
github.rest.pulls.createReview({
pull_number: context.issue.number,
body: 'Lint check passed'
})
Example 4: Reusable Workflow Call
If .github/workflows/_validate.yml contains:
jobs:
validate:
permissions:
contents: read
pull-requests: write
steps: ...
Then the caller must declare:
validate:
permissions:
contents: read
pull-requests: write
uses: ./.github/workflows/_validate.yml
Example 5: No Permissions Needed
gate:
runs-on: ubuntu-latest
permissions: {}
steps:
- uses: re-actors/alls-green@release/v1
with:
allowed-skips: job1, job2
jobs: ${{ toJSON(needs) }}
Reusable Workflow Caller Contract
Important: When a job calls a reusable workflow via uses:, the caller job must declare permissions for all jobs within the reusable workflow.
Why: The reusable workflow's nested jobs inherit the permissions from the caller's GITHUB_TOKEN, so the caller must declare everything the callee needs.
Example mismatch (WRONG):
validate:
uses: ./.github/workflows/_validate.yml
jobs:
check:
permissions:
contents: read
pull-requests: write
steps: ...
Correct:
validate:
permissions:
contents: read
pull-requests: write
uses: ./.github/workflows/_validate.yml
GitHub Token Default Behavior
Without explicit permissions:
- Public repos: Defaults to
GITHUB_TOKEN: read-all (dangerous!)
- Private repos: Defaults to
GITHUB_TOKEN: read-write (very dangerous!)
With explicit permissions:
- Uses minimum required scope (safe, follows least-privilege)
- Auditable - others can see exactly what tokens can access
- CodeQL approves (no "missing permissions" alert)
Common Mistakes
❌ Mistake 1: Forgetting PR/issues permissions
comment:
steps:
- uses: actions/github-script@v6
with:
script: github.rest.issues.createComment({...})
✅ Correct:
comment:
permissions:
issues: write
steps:
- uses: actions/github-script@v6
with:
script: github.rest.issues.createComment({...})
❌ Mistake 2: Not declaring union of reusable workflow permissions
my-job:
permissions:
contents: read
uses: ./.github/workflows/_validate.yml
✅ Correct:
my-job:
permissions:
contents: read
pull-requests: write
uses: ./.github/workflows/_validate.yml
❌ Mistake 3: Excessive permissions
build:
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- run: npm build
✅ Correct (least-privilege):
build:
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- run: npm build
Testing Your Permissions
Run CodeQL scan locally:
codeql database create my_db --language=actions --source-root=.
codeql database analyze my_db actions/security-and-quality.qls --format=sarif-latest --output=results.sarif
Or wait for GitHub to scan and check:
gh pr checks
References
Related Skills
- github-workflow-security-patterns (codeql-resolver)