CI/CD

Error: Resource not accessible by integration

The workflow's GITHUB_TOKEN lacks a permission. How to read which one from the API call that failed, and grant it without widening the whole workflow.

medium fix6 min read

the ci/cd error
RequestError [HttpError]: Resource not accessible by integration

Error: Resource not accessible by integration
    at /home/runner/work/_actions/actions/github-script/v7/dist/index.js:8053:21

remote: Permission to acme/myapp.git denied to github-actions[bot].
fatal: unable to access 'https://github.com/acme/myapp/': The requested URL returned error: 403

Do this first3 steps

Run these in order. Each one tells you what its output means before you change anything.

  1. 1

    Identify which API call failed

    echo "check the step name and the action in the stack trace"

    The permission needed follows from the endpoint. Commenting on a PR needs pull-requests write, pushing needs contents write, and publishing a package needs packages write. Grant the specific one rather than guessing.

  2. 2

    Declare the permission on the job

    echo "permissions:\n  contents: read\n  pull-requests: write"

    Declaring permissions at job level rather than workflow level keeps the elevated scope on the one job that needs it. Any permission you do not list defaults to none once the block exists.

  3. 3

    Check whether the run came from a fork

    echo "a pull_request event from a fork always gets a read-only token"

    No permissions block can grant write to a fork's pull_request run. That is a deliberate security boundary, and the workflow has to be restructured rather than reconfigured.

All 9 sections

The GITHUB_TOKEN your workflow is using does not have the permission the API call needs. The message never says which one, so work backwards from the call that failed.

Which permission?

The step name and the failing endpoint tell you:

What the job was doingPermission
Commenting on a PR or issuepull-requests: write or issues: write
Pushing a commit or tagcontents: write
Creating a releasecontents: write
Pushing to GHCRpackages: write
Updating a check or statuschecks: write or statuses: write
Uploading a SARIF filesecurity-events: write
Deploying to Pagespages: write and id-token: write
Requesting an OIDC tokenid-token: write

Note that a PR comment needs pull-requests: write, not issues: write, even though the REST endpoint is under /issues. That one catches people regularly.

Declare it on the job

jobs:
  comment:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: 'Build passed'
            })

Once you declare a permissions block, everything you do not list becomes none. That is the opposite of what people expect, and it produces a second failure immediately after fixing the first. Always include contents: read unless the job genuinely needs nothing from the repository.

Job-level is better than workflow-level. A workflow-level block applies the elevated scope to every job in the file, including ones that only run tests.

Why it changed

GitHub moved the default for new repositories to a read-only GITHUB_TOKEN. Older repositories may still default to permissive, which is why a workflow copied from one repository fails in another with no visible difference.

Check: Settings → Actions → General → Workflow permissions.

Leave it on read-only and declare permissions per job. Switching the repository to permissive fixes the error and gives every workflow write access to everything, which is the wrong trade.

Forks: no permissions block will help

A pull_request event from a fork always receives a read-only token, regardless of what the workflow declares, and repository secrets are not available. This is a deliberate boundary: a pull request is untrusted code, and it must not be able to write to your repository or read your secrets.

remote: Permission to acme/myapp.git denied to github-actions[bot].

Three ways to work with it:

Split the workflow. The untrusted part runs on pull_request with no permissions and uploads an artifact. A second workflow on workflow_run has the permissions and reads the artifact:

# .github/workflows/comment.yml
on:
  workflow_run:
    workflows: ["CI"]
    types: [completed]
permissions:
  pull-requests: write

Use pull_request_target carefully. It runs in the context of the base repository with a writable token and access to secrets. It checks out the base commit by default, which is the point. Explicitly checking out the PR head under pull_request_target runs untrusted code with your secrets available, which is a well-known and serious vulnerability:

# dangerous, do not do this
on: pull_request_target
steps:
  - uses: actions/checkout@v4
    with:
      ref: ${{ github.event.pull_request.head.sha }}    # untrusted code

Accept it. For a comment on a fork PR, workflow_run is the correct shape and worth the extra file.

Pushing back to the repository

jobs:
  release:
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
      - run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git commit -am "chore: bump version"
          git push

actions/checkout stores the token for you, so no extra credential setup is needed.

One thing to know: a push made with GITHUB_TOKEN does not trigger further workflows. That is deliberate, to prevent infinite loops. If you need the push to trigger a build, use a PAT or a GitHub App token, and think carefully about the loop.

Protected branches and rulesets

contents: write is not enough if the branch is protected. github-actions[bot] must also be in the bypass list for the ruleset, or the push is rejected with a 403 that looks identical to a permissions problem.

GitHub Apps and organisation policies

For a GitHub App token, the App's installation permissions cap what any workflow can do. Widening the workflow's permissions cannot exceed them.

Organisation settings can also restrict the default token across every repository, which produces this error in a repository whose own settings look correct.

A checklist

  1. Identify the failing API call, then the permission it needs.
  2. Add a permissions block at job level.
  3. Remember that declaring the block sets everything unlisted to none. Include contents: read.
  4. PR comments need pull-requests: write, despite the /issues endpoint.
  5. Fork PR → the token is read-only whatever you declare. Use workflow_run.
  6. Never check out the PR head under pull_request_target.
  7. Pushing with GITHUB_TOKEN does not trigger other workflows, by design.
  8. Still 403 with the right permission → check branch protection bypass and org policy.

Frequently Asked Questions

Why did my workflow start failing when I copied it to another repository?

Because the default GITHUB_TOKEN permission differs between repositories. GitHub changed the default for newly created repositories to read-only, while older ones may still be set to permissive, so an identical workflow behaves differently. Check Settings → Actions → General → Workflow permissions. The right fix is to leave the repository on read-only and declare an explicit permissions block on the jobs that need more, which also makes the workflow portable rather than dependent on a repository setting.

Why did adding a permissions block break a different step?

Because declaring a permissions block switches every permission you do not list to none, rather than adding to the defaults. So a block containing only pull-requests: write removes the job's ability to read the repository, and actions/checkout then fails. Always include contents: read unless the job genuinely needs nothing from the repository. This catches almost everyone the first time, because the behaviour is the opposite of what an additive block would do.

Can I give a fork's pull request write access?

No, and you should not want to. A pull_request event from a fork always gets a read-only token with no access to secrets, regardless of what the workflow declares, because the pull request contains untrusted code. The supported pattern is to split the work: the untrusted job runs with no permissions and uploads an artifact, and a separate workflow_run workflow with the necessary permissions consumes it. That keeps the privileged half running only code from your own default branch.

Is pull_request_target safe to use?

It is safe when used as designed and dangerous when misused. It runs in the base repository's context with a writable token and access to secrets, and it checks out the base commit rather than the pull request's code, which is precisely what makes it safe. The vulnerability appears when a workflow explicitly checks out github.event.pull_request.head.sha under pull_request_target, because that executes a contributor's code with your secrets in the environment. That pattern has been exploited in the wild and should never be used.

Why does my push succeed but not trigger the next workflow?

Because events created using GITHUB_TOKEN deliberately do not trigger further workflow runs, which prevents a workflow that pushes from triggering itself indefinitely. If you genuinely need the push to start another workflow, authenticate with a personal access token or a GitHub App installation token instead, and think carefully about the loop you are creating. A workflow_run trigger on the current workflow is often a cleaner alternative to chaining through a push.

Reference and practice

Learn the underlying concept

Other CI/CD errors