Resource not accessible by personal access token in CI
Resource not accessible by personal access token is GitHub telling you that your fine-grained token reached the right repository and lacks the one permission this call needs. The wording is chosen to distinguish it from the same refusal aimed at a workflow token.

What this error means
A step using a fine-grained personal access token fails with 403 while other steps using the same token succeed. Nothing names a permission. When the call went through REST the response carries a documentation link, and when it went through GraphQL the message carries the mutation name in parentheses. Those two details are the only things in the failure that narrow it, and both are easy to read past.
403 {"message":"Resource not accessible by personal access token","documentation_url":"https://docs.github.com/rest/pulls/pulls#list-pull-requests","status":"403"}
GraphQL: Resource not accessible by personal access token (enablePullRequestAutoMerge)The wording tells you which credential was used
GitHub has three refusals that look alike and are not interchangeable. Resource not accessible by integration means the caller was a GitHub App installation, which includes the automatic GITHUB_TOKEN. Resource not accessible by personal access token means the caller was a fine-grained personal access token. A classic token with insufficient scopes tends to produce a 404 or a scope-specific message instead.
That distinction is worth two minutes at the start. If you believed a step was using the workflow token and the message names a personal access token, the step is reading a secret you forgot was set, and the permission you are about to edit belongs to the wrong credential entirely.
Common causes
The token is missing the one permission this endpoint needs
The common case, and the reason the error is so narrow. Fine-grained permissions are per resource type, so a token that can read contents cannot open a pull request, add a label or write a check without each of those being granted separately. Everything else in the workflow keeps working.
The permission was granted at read when the call is a write
Each permission has levels, and the levels are easy to leave at the default while ticking the box. A token with Pull requests set to read passes the existence check and fails the write, which looks identical to not having the permission at all.
The step is using a different credential from the one you edited
The message names a personal access token, so if you have been editing the job permissions block you have been editing the wrong thing. A secret set long ago, or a default input on a third-party action, is supplying the credential that is actually failing.
The token belongs to an organization that must approve it
Worth ruling out when the permissions look correct in the token settings. A fine-grained token targeting an organization's repositories can require approval from an owner before it takes effect, and until then it behaves as though the permissions were never granted.
How to fix it
Use the response to name the permission
- If the failure carries a
documentation_url, open it and read the permissions that endpoint requires. - If it carries a mutation name in parentheses, look that mutation up instead.
- If it carries neither, find the call in the action's source, then use one of the two steps above.
- Grant that permission at the level the call needs, not at read by default.
Confirm which credential the step is using
Before changing any token, establish that the failing step reads the token you think it does. The wording already tells you the class of credential: a message naming an integration is the workflow token, and this one is a personal access token. They are configured in entirely different places.
Prefer the workflow token when the call is in its own repository
A fine-grained token is the right tool for reaching another repository and an unnecessary liability inside this one. If the call targets the repository running the workflow, deleting the token input and declaring the permission on the job removes an expiring secret and a second place to configure.
jobs:
label:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- run: gh pr edit "$NUMBER" --add-label triage
env:
GH_TOKEN: ${{ github.token }}
NUMBER: ${{ github.event.pull_request.number }}Treat a change from 404 to 403 as progress
If adding the repository to the token turned a not-found into this message, the repository gate is now open and only the permission gate remains. Do not revert the repository change while investigating the permission.
Read the clue the route left you
Neither form of the message names the missing permission, but both name the operation, and the operation maps to one permission in GitHub's reference. That mapping is the shortest path from this error to the checkbox.
| Route the call took | Detail carried with the refusal | How to turn it into a permission |
|---|---|---|
| REST | A documentation_url naming the endpoint, such as the list-pull-requests page | Open that page and read the permissions its own reference section requires |
| GraphQL | The mutation or query in parentheses after the message, as in the second line above | Look the mutation up and read the fine-grained permission it lists |
| A third-party action | Usually neither, because the action prints only the message | Identify the call the step makes, then use one of the rows above |
Two independent gates, and only one of them is the permission
A fine-grained token has a repository list and a permission set, and both have to allow the call. The repository list is checked first, and failing it produces a 404 rather than this message, for the same reason any invisible resource does. So seeing this 403 at all is informative: it means the repository is on the list, and only the permission is wrong.
That narrows the search considerably. It also explains a common sequence in which someone adds the repository, sees the error change from 404 to this one, and concludes nothing improved. The change from 404 to 403 was progress: one of the two gates opened.
Why there is no recorded run on this page
Recording this would mean minting a personal access token, deliberately withholding one permission from it, and publishing the log. The token would be ours, its repository list would be ours, and the interesting fields, the endpoint in the link and the mutation in the parentheses, vary with the call you made rather than with anything we could stage. What generalizes is the relationship between the wording and the credential type, which is established from GitHub's own responses rather than from a run. The two lines above are quoted from public issues: one where the tool printed the full REST response body, and one where gh printed the GraphQL refusal with the mutation name still attached.
How to prevent it
- Grant fine-grained permissions from the endpoint the workflow calls, rather than from a guess about what sounds related.
- Record the token's expiry somewhere the team sees it, since a fine-grained token stops working on a date rather than on a change.
- Keep one token per purpose, so a refusal identifies the workflow that needs attention.
- Use the workflow token for calls inside the repository and reserve personal tokens for crossing out of it.
Frequently asked questions
How is this different from "not accessible by integration"?
GITHUB_TOKEN is, so that version points at the job's permissions block. This version names a personal access token, so it points at the token's own permission settings. Editing the block will not affect a call made with a personal token.Why does the message not say which permission is missing?
documentation_url naming the endpoint on REST, or the mutation in parentheses on GraphQL. Each of those has a documented permission requirement, so the operation is one lookup away from the answer. An action that prints only the message throws that clue away.I granted the permission and it still fails. What else is there?
Why did I get 404 before and 403 now?
Related guides
References
- GitHub REST API: permissions required for fine-grained tokens
- GitHub: managing fine-grained personal access tokens and organization approval
- Crackedcoder5TH/remembrance-oracle-toolkit#380: the full REST response body
- vorburger/nixfiles#33: the GraphQL form, with the mutation name in parentheses
- GitHub Actions documentation