---
title: "Secure pull_request_target in GitHub Actions"
description: "Use pull_request_target safely without executing fork code with secrets or write tokens. Learn when to use pull_request and how to split privileged work."
url: https://starsling.dev/best-practices/github-actions/secure-pull-request-target
canonicalUrl: https://starsling.dev/best-practices/github-actions/secure-pull-request-target
---

# Secure pull_request_target in GitHub Actions

- [How StarSling works](https://starsling.dev/)

Best practice: Security. Last updated: 2026-08-19

Use `pull_request` for any job that checks out, builds, installs, or tests contributor code; reserve `pull_request_target` for metadata-only work on trusted base-branch code with minimal permissions.

## Table of contents

- [Do this: run fork code without privileges](#do-this)
- [Avoid this: execute the fork on pull_request_target](#avoid-this)
- [Keep pull_request_target metadata-only](#safe-metadata-only-target-workflows)
- [Why it matters](#why-it-matters)
- [When pull_request_target is appropriate](#when-to-use)
- [Verify on your repo](#verify)
- [Trying to make GitHub Actions faster?](#speedup-guides)
- [Sources](#sources)

<a id="do-this"></a>

## Do this: run fork code without privileges

Untrusted contributor code runs only on `pull_request`, without repository secrets and with a read-only token. A `pull_request_target` workflow never checks out the pull request head, fetches its patch, runs its scripts, or restores state the fork can influence. If a privileged follow-up is necessary, it is a separate workflow that treats untrusted artifacts as data, not executable code.

_Build the contributor's code on pull_request_

```yaml
on:
  pull_request:

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - run: npm ci
      - run: npm test
```

<a id="avoid-this"></a>

## Avoid this: execute the fork on pull_request_target

Any GitHub user can open a fork pull request whose install or test code executes in the base repository's privileged context.

_Explicitly bypassing checkout's fork guard_

```yaml
on:
  pull_request_target:

permissions:
  contents: write

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
        with:
          ref: ${{ github.event.pull_request.head.sha }}
          # Intentionally unsafe: checkout v7 blocks fork PR heads on this
          # privileged trigger unless this opt-out is set.
          allow-unsafe-pr-checkout: true
      - run: npm ci
      - run: npm test
```

<a id="safe-metadata-only-target-workflows"></a>

## Keep pull_request_target metadata-only

A target workflow can label a pull request or post a comment using trusted base-branch code, but every pull request field remains untrusted input. Do not interpolate titles, bodies, branch names, or labels directly into shell code. Keep permissions minimal and use [least-privilege GitHub Actions permissions](/best-practices/github-actions/limit-github-actions-permissions). The [ci-secure skill](/ci-secure) detects privileged fork execution and related trust-boundary chains.

<a id="why-it-matters"></a>

## Why it matters

`pull_request_target` runs in the context of the base branch and can receive a privileged token or secrets that ordinary fork pull requests do not. Checkout v7 blocks fork PR head and merge refs on this trigger by default, but an explicit `allow-unsafe-pr-checkout: true`, a manual git fetch, or another execution path can bypass that layer. Executing the fork inside the privileged context turns an outsider's contribution into code execution with repository authority.

<a id="when-to-use"></a>

## When pull_request_target is appropriate

**Use it when:** Use `pull_request_target` only for base-branch, metadata-only automation that genuinely needs target-repository context, such as applying a label through a tightly scoped API call.

**Be careful when:** Do not use it for builds, tests, dependency installation, generated code, benchmarks, or any step that fetches or executes pull request content. Use `pull_request` for those jobs.

<a id="verify"></a>

## Verify on your repo

Hand this prompt to your coding agent to audit and fix this practice in your own repo:

Audit workflows triggered by `pull_request_target` or another outsider-influenceable privileged event. If a job checks out a pull request head, fetches its patch, installs dependencies, runs repository scripts, restores fork-influenced state, or otherwise executes contributor content, move that work to an unprivileged `pull_request` workflow with read-only permissions and no secrets. Keep only metadata operations on `pull_request_target`, using trusted base-branch code and the minimum job permissions. Preserve the intended label, comment, or status behavior, show the diff, and open a PR rather than applying it blindly.

Ground these changes in the upstream docs before you edit: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request_target, https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/. If you cannot fetch them, say so rather than guessing, and cite what you used in the PR description.

Prefer to check by hand:

- Find every `pull_request_target`, `issue_comment`, `workflow_run`, and `pull_request_review` workflow and inventory its token permissions and secrets.
- Trace every checkout, git fetch, downloaded artifact, cache restore, and command to determine whether outsider-controlled content can execute.
- Move contributor builds to `pull_request`; keep privileged follow-ups separate and treat their inputs strictly as data.

## Go further

- [Install the ci-secure skill](https://starsling.dev/ci-secure) to check every workflow against the ten critical exploit-chain attack vectors.

<a id="more-best-practices"></a>

## More best practices for GitHub Actions

- [GitHub Actions permissions: use least privilege](https://starsling.dev/best-practices/github-actions/limit-github-actions-permissions)
- [Prevent GitHub Actions cache poisoning](https://starsling.dev/best-practices/github-actions/prevent-github-actions-cache-poisoning)
- [GitHub Actions secrets: prevent leaks and overexposure](https://starsling.dev/best-practices/github-actions/protect-github-actions-secrets)
- [All CI best practices](https://starsling.dev/best-practices/github-actions)

<a id="speedup-guides"></a>

## Trying to make GitHub Actions faster?

If you do not know why a run is slow yet, start with the diagnostic guide. [Let an agent find which of these applies: /ci-speedup](/ci-speedup). If your workflows are already optimized but still slow, see our guide to fast GitHub Actions and GitHub Actions runner alternatives. Building containers in CI? The Docker workflow guide covers layer caching end to end. Want to see how your configuration measures up before changing anything? CI Score grades workflow config against a pass/fail rubric of these practices. It is not a speed measurement.

- [GitHub Actions too slow](https://starsling.dev/github-actions-too-slow)
- [Fast GitHub Actions](https://starsling.dev/fast-github-actions)
- [GitHub Actions alternatives](https://starsling.dev/github-actions-alternatives)
- [GitHub Actions pricing](https://starsling.dev/github-actions-pricing)
- [Self-hosted GitHub Actions runners](https://starsling.dev/self-hosted-github-runners)
- [Docker CI on GitHub Actions](https://starsling.dev/ci/docker)
- [Install the free /ci-score skill to improve your GitHub Actions setup](https://starsling.dev/ci-score)

<a id="faq"></a>

## FAQ

### What is the difference between pull_request and pull_request_target?

`pull_request` runs against the contribution with restricted fork permissions. `pull_request_target` runs in the base repository context, which is useful for trusted metadata automation but dangerous if it executes fork content.

### Is pull_request_target always unsafe?

No. It can be appropriate for metadata-only work that uses trusted base-branch code, minimal permissions, and no execution of pull request content. The unsafe combination is privileged context plus attacker-controlled code or state.

<a id="sources"></a>

## Sources

1. [GitHub Actions - pull_request_target event](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request_target)
2. [GitHub Security Lab - preventing pwn requests](https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/)
