pull_request vs. pull_request_target: The GitHub Actions Trigger Hiding a Security NightmareUnderstanding the critical difference between these two triggers and why one could give attackers RCE on your repository.

Introduction: The Hidden Danger Lurking in CI/CD Workflows

GitHub Actions is one of the most powerful and convenient automation platforms in modern development. It's tightly integrated, flexible, and built around a developer-friendly YAML syntax. But convenience can come at a cost—and nowhere is this more true than in the subtle difference between two nearly identical triggers: pull_request and pull_request_target.

The distinction between them is not cosmetic. It's a fundamental security boundary that separates a safe automation workflow from a potential remote code execution (RCE) nightmare. Many repositories—especially open-source ones—have fallen into this trap by misunderstanding how these triggers behave when contributors open pull requests from forks. The consequences? Leaked secrets, malicious code execution, and compromised CI environments.

In this article, we'll dissect these two triggers, explore how attackers exploit the confusion, and walk through how to secure your workflows against one of GitHub Actions' most misunderstood features.

The Basics: What pull_request and pull_request_target Actually Do

When you configure a workflow to run on a pull request, you'll likely reach for one of these two event types. Both look similar, both trigger on PRs, and both seem to do the same thing—until you look closely.

The pull_request event runs in the context of the pull request's code—that is, it checks out the changes proposed by the contributor's branch (usually from a fork) and executes your workflow without access to any repository secrets. This isolation is intentional. GitHub treats external contributions as untrusted code and sandboxes them accordingly.

The pull_request_target event, however, flips that model. It runs in the context of the base repository, not the fork. That means it has access to repository secrets, can modify workflow files, and can write back to the repo—capabilities that are extremely dangerous when combined with untrusted code. The intent behind pull_request_target was to allow maintainers to run workflows that validate configuration or automation safely, but in practice, it often gets used incorrectly.

The Security Trap: How Misusing`pull_request_target Enables RCE

Here's where things get ugly. Suppose a repository uses pull_request_target to automatically run linting, testing, or deployment checks on every incoming PR—including those from forks. On the surface, it seems harmless: you just want automation for all contributions. But the attacker sees something else.

Because pull_request_target runs with access to secrets from the base repository, a malicious actor can submit a PR that modifies the workflow itself. GitHub Actions doesn't run the changed workflow from the fork—it runs the base repo's version, but that workflow might execute code from the forked PR. That's the crack in the wall: an attacker can inject commands into files that the workflow runs, like scripts/build.js or test/setup.py. Those scripts execute under a context that can access tokens, environment variables, and repository secrets.

That's Remote Code Execution with privilege escalation, delivered through a simple pull request. This isn't theoretical—security researchers and real-world attackers have abused this pattern in popular open-source projects. It's the perfect example of how “a small YAML change” can open a massive hole.

The Safe Path: Using pull_request Correctly for Forks

If you maintain a public repository that accepts external contributions, always assume contributors are untrusted. The pull_request event was designed for that exact case—it executes the workflow on the forked code, but without access to your secrets. It's your safety net.

This means your CI workflows should avoid doing anything that requires secrets when triggered by PRs from forks. Instead, run build, lint, or test steps that only depend on public resources. For workflows that require elevated permissions (like deployments or auto-merges), restrict them to push or workflow_dispatch events and make them run only after you've reviewed and merged the code.

Here's a simplified example:

# .github/workflows/pr-checks.yml
name: PR Checks
on:
  pull_request:
    types: [opened, synchronize, reopened]

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

This setup ensures contributors' code runs in a controlled, isolated environment—no secrets, no write access, no surprises.

Controlled Risk: When to Use pull_request_target (If Ever)

There are legitimate use cases for pull_request_target, but they require surgical precision. For example, you might need to enforce a policy or run checks that rely on metadata from the base repository, such as labeling rules, CLA enforcement, or automated commenting. These tasks read from the repository but don't execute untrusted code.

If you do use it, lock it down. Never checkout the PR's forked code using a ref like github.event.pull_request.head.sha unless you explicitly sandbox the execution. Instead, operate on metadata only. If you absolutely must run code from the PR, use a secondary workflow triggered manually after review, not automatically.

Here's a minimal safe pattern:

# .github/workflows/metadata-check.yml
name: PR Metadata Checks
on:
  pull_request_target:
    types: [opened, synchronize]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/github-script@v7
        with:
          script: |
            const pr = context.payload.pull_request;
            if (!pr.title.startsWith("[Feature]")) {
              core.setFailed("PR title must start with [Feature]");
            }

Notice that this workflow never checks out code or runs scripts from the PR—it only reads metadata and fails fast.

The Real Lesson: CI/CD Security Is a Mindset

Security isn't just about what tools you use—it's about how you think. GitHub's documentation on these triggers is accurate but easy to misinterpret if you assume “they're basically the same.” They're not. The pull_request_target trigger is a loaded gun; it gives convenience to maintainers but leverage to attackers if misused.

As a maintainer or architect, your CI/CD workflows are part of your security boundary. Treat them with the same scrutiny you'd apply to an API endpoint or authentication flow. Every action, every trigger, every context switch matters. Never assume “it just runs tests.” When secrets and code execution intersect, you're always one YAML line away from compromise.

Conclusion: Trust Nothing, Automate Wisely

The difference between pull_request and pull_request_target is a perfect illustration of how small technical nuances can have massive security implications. One keeps your secrets safe; the other can expose them to untrusted actors.

If your repository accepts contributions from forks, stick to pull_request. If you use pull_request_target, make sure it never executes untrusted code. Review your workflows today—before an attacker does it for you. In the world of CI/CD automation, ignorance isn't bliss; it's a vulnerability.