GitHub Actions "Resource not accessible by integration" when commenting on PRs and pushing tags
After moving repositories into a new GitHub organization, our coverage-comment and release-tag steps failed with 403 Resource not accessible by integration. The GITHUB_TOKEN defaulted to read-only. A scoped permissions block fixed it, and fork PRs needed a different design.
In November we moved twelve repositories from a personal account into a new GitHub organization
for our internal tooling team. Builds kept passing, but two steps started failing in every
repository that used them: the job that posts a test coverage summary as a pull request comment, and
the job that creates a version tag after a merge to main.
The coverage step uses actions/github-script@v7 and failed with:
RequestError [HttpError]: Resource not accessible by integration
at /home/runner/work/_actions/actions/github-script/v7/dist/index.js:9537:21
{
status: 403,
response: {
url: 'https://api.github.com/repos/example-org/ledger-cli/issues/218/comments',
status: 403,
headers: {
'x-accepted-github-permissions': 'issues=write; pull_requests=write',
...
},
data: {
message: 'Resource not accessible by integration',
documentation_url: 'https://docs.github.com/rest/issues/comments#create-an-issue-comment',
status: '403'
}
}
}
Error: Unhandled error: HttpError: Resource not accessible by integration
The tagging step, which runs plain git push, failed differently:
remote: Permission to example-org/ledger-cli.git denied to github-actions[bot].
fatal: unable to access 'https://github.com/example-org/ledger-cli/': The requested URL returned error: 403
Error: Process completed with exit code 128.
Reproducing it
Any workflow that writes with the default token reproduces it in a repository whose default token permissions are read-only:
name: repro
on: pull_request
jobs:
comment:
runs-on: ubuntu-24.04
steps:
- uses: actions/github-script@v7
with:
script: |
await github.rest.issues.createComment({
...context.repo,
issue_number: context.issue.number,
body: 'hello from CI',
});
What we ruled out
- A broken or expired secret. Neither step used a personal token; both used the automatic
GITHUB_TOKEN, which is minted fresh for every job. - Branch protection on
main. Tags are not covered by our branch rules, and the error appeared on the PR comment too, which has nothing to do with branches. - A GitHub outage. Other repositories still in the personal account ran the same workflow successfully the same hour.
Root cause
The "Set up job" log of every run prints the token's permissions. In the old repositories:
GITHUB_TOKEN Permissions
Actions: write
Contents: write
Issues: write
PullRequests: write
...
In the new organization:
GITHUB_TOKEN Permissions
Contents: read
Metadata: read
Packages: read
GitHub changed the default for new enterprises, organizations and personal repositories in
February 2023: the workflow token now gets read-only access to contents and packages unless the
workflow asks for more. The old repositories were created long before that and kept the permissive
default; the new organization got the restricted one, and transferred repositories inherit the
organization setting. Our workflows never declared a permissions block because they had never
needed to.
What is happening under the hood
GITHUB_TOKEN is an installation token for the GitHub Actions app, scoped to the repository that
runs the workflow and valid only for the job. Its permissions come from three layers:
- The enterprise, organization or repository default ("Read and write" or "Read repository contents and packages permissions"), under Settings, Actions, General, Workflow permissions.
- A
permissions:block at the workflow level, which replaces the default for all jobs. - A
permissions:block at the job level, which replaces the workflow-level one for that job.
When you set permissions explicitly, every scope you do not list becomes none (except
metadata, which is always read). So adding pull-requests: write alone also removes contents: read, which breaks actions/checkout in private repositories. That catches people the first time.
"Resource not accessible by integration" is the generic 403 for an app token missing a permission.
The x-accepted-github-permissions response header lists the permission sets that would have
worked. A PR comment goes through the Issues API, so either issues: write or pull-requests: write is accepted.
There is one more layer that no setting overrides: workflows triggered by pull_request from a
fork always get a read-only token and no access to secrets, so untrusted code cannot write to
your repository. Dependabot pull requests are treated the same way.
Environment
| Component | Version / value |
|---|---|
| Platform | GitHub.com, Team plan, new organization created November 2024 |
| Runner | ubuntu-24.04 hosted |
| Actions | actions/checkout@v4, actions/github-script@v7 |
| Org setting | Workflow permissions: Read repository contents and packages permissions |
| Triggers | pull_request (comment job), push to main (tag job) |
Minimal reproduction
The tag job, as it was:
on:
push:
branches: [main]
jobs:
tag:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: |
version=$(jq -r .version package.json)
git tag "v$version"
git push origin "v$version"
Who is affected
Organizations and accounts created after the February 2023 change, any organization whose admins switched the default to read-only, repositories transferred into such an organization, and every workflow run from a fork or Dependabot regardless of settings.
The fix
We added explicit, minimal permissions to each workflow instead of flipping the organization
default back to read-write. Read-only at the top, and write only on the job that needs it:
name: ci
on: pull_request
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test -- --coverage
coverage-comment:
needs: test
runs-on: ubuntu-24.04
if: github.event.pull_request.head.repo.full_name == github.repository
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/github-script@v7
with:
script: |
await github.rest.issues.createComment({
...context.repo,
issue_number: context.issue.number,
body: 'Coverage: 87.4% (+0.6)',
});
The if condition skips the comment for fork PRs, where it would fail anyway.
For tagging:
jobs:
tag:
runs-on: ubuntu-24.04
+ permissions:
+ contents: write
steps:
- uses: actions/checkout@v4
actions/checkout stores the job token in the local git config by default, so the later git push
uses it with the new contents: write scope.
Fork pull requests
Two of the repositories are public and get outside contributions. For those we split the work into
two workflows: the untrusted pull_request run uploads the coverage summary as an artifact, and a
workflow_run workflow, which runs in the context of the base repository with a write token, reads
it and posts the comment. It never checks out or executes the PR's code.
We deliberately did not use pull_request_target with a checkout of the PR head. That trigger runs
with a write token and secrets; combining it with untrusted code is a well-known way to get a
repository compromised.
Alternatives and trade-offs
- Set the organization default back to read and write. One click, fixes everything, and gives every job in every repository write access it mostly does not need. We rejected it.
- A GitHub App token via
actions/create-github-app-token. Needed when the push must trigger other workflows: events created withGITHUB_TOKENdo not start new workflow runs, so a tag pushed with it will not trigger apush: tagsrelease workflow. We switched the tag job to an app token for that reason a week later. - A personal access token. Works, but ties automation to one person's account. Avoid.
How to test the fix
Open a pull request from a branch in the same repository and check the "Set up job" log of the comment job:
GITHUB_TOKEN Permissions
Contents: read
Metadata: read
PullRequests: write
Then verify the token from a temporary step with the GitHub CLI preinstalled on hosted runners:
- name: permission check
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" -f body="permission check"
Confirming in production
- All twelve repositories passed on their next pull request, and coverage comments appeared again.
- The tag job pushed
v2.14.0on the next merge, and with the app token the release workflow triggered from it. - A fork PR to one public repository showed the comment job as skipped, and the
workflow_runfollow-up posted the comment.
Regression guard
actionlintruns on.github/workflows/**in pre-commit.- A small check in our shared CI fails any workflow file without a top-level
permissionsblock. - The organization default stays read-only, so a new workflow that forgets to ask fails loudly instead of silently getting write access.
Security implications
The restricted default is a security improvement, and the fix keeps it. Every job now has the least
privilege it needs, so a compromised third-party action in the test job can read code but cannot
push, tag or comment. Keep fork PRs on pull_request, never check out untrusted code in
pull_request_target or workflow_run jobs, and pin third-party actions to a full commit SHA.
Prevention checklist
- Declare
permissionsat workflow level as read-only and widen per job. - Remember that listing any permission sets all unlisted ones to
none. - Read the "GITHUB_TOKEN Permissions" section of the job log when a 403 appears.
- Treat fork and Dependabot runs as read-only by design.
- Use an app token when a push must trigger further workflows.
References
- GitHub Docs: Automatic token authentication — https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication
- GitHub Docs: Workflow syntax, permissions — https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#permissions
- GitHub Changelog: Updating the default GITHUB_TOKEN permissions to read-only (2023-02-02) — https://github.blog/changelog/2023-02-02-github-actions-updating-the-default-github_token-permissions-to-read-only/
- GitHub Docs: Events that trigger workflows, pull_request_target and workflow_run — https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows
- GitHub Docs: Choosing permissions for a GitHub App (x-accepted-github-permissions) — https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/choosing-permissions-for-a-github-app
Add a label
Embed this post
<iframe src="https://hibug.me/github-actions-resource-not-accessible-by-integration" width="700" height="420" frameborder="0" loading="lazy"></iframe>
I have spent most of my career on the side of software that pages people at night: payment integrations, web platforms with real traffic, CI/CD pipelines, Kubernetes clusters and the databases underneath them. Somewhere along the way I started keeping a notebook of every bug that cost more than an hour — the exact error, what we ruled out, the real root cause and how we proved the fix. Hibug is where those notes live. Every post follows the same path: Investigate, Explain, Fix, Verify — with the environment spelled out, the fix tested, and the security angle checked.
Comments
0No comments yet. Be the first to comment!
Sign In to post a comment.