peter-evans/create-pull-request 403 and the setting behind it
A peter-evans/create-pull-request 403 is worth reading twice, because the step does two different privileged things and only one of them usually fails. If the branch appears in the repository, the push succeeded and what was refused was the pull request itself.

What this error means
The action runs, the commit is made, and the branch exists afterwards. The step still fails, and the message is a complete sentence about GitHub Actions not being permitted to create or approve pull requests. The workflow already declares contents: write and pull-requests: write, so the permissions block looks correct and is, which makes the sentence read like a bug in the action.
Error: GitHub Actions is not permitted to create or approve pull requests.One step, two privileged calls
The action pushes a branch and then asks GitHub to open a pull request from it. Those are separate operations against separate parts of the permission model, and treating the step as a single unit is what sends people to the wrong setting. The branch is the evidence: if it is in the repository after the failure, the push was allowed and only the second call was refused.
That distinction also tells you what to stop doing. Adding contents: write cannot help a refused pull request creation, and it is the most common first response because it is the permission everyone remembers.
Common causes
The repository or organization setting for Actions creating pull requests is off
The common case when the branch exists. It is off by default in many organizations, it is not visible from the workflow file, and it produces a message that sounds like a permission so the permissions block gets edited instead.
The job did not grant pull-requests: write
Still worth checking, because it is necessary even when the setting is on. The distinguishing feature is the wording: a missing scope produces a resource-not-accessible refusal rather than the sentence about not being permitted to create or approve.
The run is a fork pull request, so the push failed too
When the branch is absent, the earlier call is the one that was refused. Write scopes are downgraded for fork pull requests after the block is read, so nothing the workflow declares restores them and the action never reaches the creation step.
The head branch belongs to a fork and the token cannot modify it
A narrower case the action handles specially. When maintainer edits are requested on a branch the token has no write access to, the action warns about that explicitly and suggests turning the option off. The warning names the situation, so it is not easily confused with the setting.
How to fix it
Check for the branch, then choose the repair
- Look for the branch the action was pushing. Its presence separates the two failures completely.
- Branch present: change the setting, in the repository and possibly in the organization.
- Branch absent: fix the token, either by granting
contents: writeor by changing how the workflow is triggered.
Turn the setting on where it is held
Enable Allow GitHub Actions to create and approve pull requests under Workflow permissions. Check the organization settings as well as the repository ones, because the organization can hold it off for every repository it owns. Nothing in the workflow file substitutes for this.
Declare both scopes on the job
The setting and the scopes are both required, so grant what the two calls need and keep them on the job rather than the workflow.
jobs:
update:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v5
- run: ./scripts/regenerate.sh
- uses: peter-evans/create-pull-request@v7
with:
branch: chore/regenerate
title: 'chore: regenerate'Use an app installation token when the setting cannot be changed
The setting governs GITHUB_TOKEN. A GitHub App installation token is a different credential and is not covered by it, so passing one to the action is the durable answer in organizations where the toggle stays off. A pull request opened this way is also attributed to the app rather than to the Actions bot.
Which failure the branch tells you about
Look for the branch before reading anything else. The three outcomes have nothing in common except the step they occur in.
| Branch present afterwards | What was refused | Where the repair lives |
|---|---|---|
| Yes | The pull request creation, by a repository or organization setting | Settings, under Workflow permissions, not the workflow file |
| No | The push, by the token's permissions or the fork rule | The job's permissions block, or the trigger |
| Yes, and a pull request already exists | Nothing; the action reports it and continues | No repair needed |
Why the permissions block cannot reach it
The permissions key describes what scopes the token carries. This setting describes whether the token is allowed to perform one particular action at all, regardless of scope, and it exists because a workflow that can open and approve its own pull requests can route around review. Those are different mechanisms, which is why a job holding pull-requests: write is still refused.
The practical consequence is that the fix may not be yours to make. On an organization-owned repository the toggle may be held at the organization level, and the person who can change it is not necessarily the person reading the failure.
Why there is no recorded run on this page
The deciding input is a checkbox in a settings page. We could record a run against a repository of ours with that box cleared, and the log would contain the same single sentence that is already quoted above, proving only that we had cleared it. Nothing about the runner participates: the push and the creation are both server-side decisions, made before and after the step's own work. The sentence and the setting's exact name are both published, and those are what the page rests on.
How to prevent it
- Record the Workflow permissions setting alongside the workflow, since nothing in the file hints that it exists.
- Grant
contents: writeandpull-requests: writetogether on any job that opens a pull request. - Prefer an app installation token in organizations that keep the toggle off as policy.
- Check for the branch first whenever this step fails, before reading anything else.
Frequently asked questions
Why did the branch push succeed if the token is refused?
contents: write, which the job had, and opening the pull request is governed separately by a repository setting that can forbid it outright. The branch surviving the failure is the clearest evidence of which call was refused.Which setting exactly, and where is it?
GITHUB_TOKEN can create and approve pull requests. An organization-level equivalent can override the repository one.Does pull-requests: write fix this?
Why does using a GitHub App token avoid the problem?
GITHUB_TOKEN specifically. An installation token from an app you installed is a separate credential with its own permissions, so it is not subject to that toggle. It is the usual answer where organization policy keeps the setting off and is not going to change.