# peter-evans/create-pull-request 403 and the setting behind it

> A peter-evans/create-pull-request 403 usually arrives after the branch pushed, which narrows it to one repository setting rather than the token.

Source: https://latchkey.dev/learn/github-actions/gha-create-pull-request-token-403  
Updated: 2026-09-20

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.

```Message GitHub returns on the create call, quoted from career-ops-hq/career-ops#4340
Error: GitHub Actions is not permitted to create or approve pull requests.
```

## 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

1. Look for the branch the action was pushing. Its presence separates the two failures completely.
2. Branch present: change the setting, in the repository and possibly in the organization.
3. Branch absent: fix the token, either by granting `contents: write` or 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.

```.github/workflows/regenerate.yml (illustrative)
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.

## How to prevent it

- Record the Workflow permissions setting alongside the workflow, since nothing in the file hints that it exists.
- Grant `contents: write` and `pull-requests: write` together 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.

## 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.

## 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 |

> The setting in the first row is named exactly: **Allow GitHub Actions to create and approve pull requests**, which GitHub describes as configuring "whether `GITHUB_TOKEN` can create and approve pull requests". It lives under Workflow permissions in repository settings and has an organization-level counterpart that can hold it off even when the repository's own box is ticked.

## 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.

## FAQ

### Why did the branch push succeed if the token is refused?

Because two different calls were made. Pushing the branch needs `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?

It is called **Allow GitHub Actions to create and approve pull requests**, under Workflow permissions in the repository's Actions settings, and GitHub describes it as configuring whether `GITHUB_TOKEN` can create and approve pull requests. An organization-level equivalent can override the repository one.

### Does pull-requests: write fix this?

Not on its own. The scope is necessary and the setting is also necessary, and they are enforced separately. A job with the scope and without the setting gets the sentence about not being permitted; a job with the setting and without the scope gets a resource-not-accessible refusal instead.

### Why does using a GitHub App token avoid the problem?

Because the setting applies to `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.

## References

- [GitHub: managing GitHub Actions settings for a repository, Workflow permissions](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository)
- [peter-evans/create-pull-request: src/github-helper.ts, the create call and its error handling](https://github.com/peter-evans/create-pull-request/blob/main/src/github-helper.ts)
- [career-ops-hq/career-ops#4340: the branch updated and the creation refused](https://github.com/career-ops-hq/career-ops/issues/4340)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
