Pull Request vs Pull Request Target trigger
Understand the differences between the `pull_request` and `pull_request_target` triggers in GitHub Actions.
Today we’re looking at two very similar yet distinct event triggers: pull_request and pull_request_target. Understanding the differences between these triggers is crucial for the security of your CI/CD workflows. Let’s compare and contrast these triggers to help you choose the right one for your workflows.
The pull_request event trigger#
The pull_request event trigger is activated whenever a pull request is created, synchronized, or updated in some way. This trigger is commonly used in workflows that require running tests, linters, or other checks on the code changes introduced by the pull request.
Key Features:
- Context: by default,
actions/checkoutchecks out the merge commit (the PR branch merged into the base branch). This means it has access to all the changes proposed in the pull request. - Security: for pull requests opened from a fork, the
GITHUB_TOKENis read-only and no secrets are passed to the workflow (secrets are empty). This provides a strong layer of protection when running code from untrusted contributors. For pull requests from a branch within the same repository, the token and secrets are available as usual.
Use case example: automatically running unit tests on the code changes submitted in a pull request.
on: pull_request: branches: - mainThe pull_request_target event trigger#
Introduced to address specific security and workflow issues, the pull_request_target trigger behaves similarly to pull_request but with one crucial difference: the workflow definition is read from the base branch, and by default actions/checkout checks out the base branch HEAD rather than the PR’s merge commit.
Key Features:
- Context: the workflow file is always taken from the base branch (e.g.,
main), so a contributor cannot alter the workflow itself through the pull request. By default the checkout is also the base branch, not the proposed changes. - Security: runs with a read-write
GITHUB_TOKENand has full access to secrets, making it suitable for workflows that need to label PRs, post comments, or perform actions requiring elevated permissions — even for pull requests from forks.
Use Case Example: Posting a comment with the results of a CI test run on the pull request, requiring access to repository secrets for authentication with GitHub’s API.
on: pull_request_target: branches: - mainComparison and Considerations#
-
Security model:
pull_request: safer for public repositories or when accepting contributions from forks, as it restricts access to secrets.pull_request_target: Ideal for workflows that need to interact with sensitive data or perform actions requiring full repository access.
-
Access to code changes:
pull_request: checks out the proposed changes by default (the merge commit), making it straightforward for code checks.pull_request_target: checks out the base branch by default, so accessing the PR’s changes requires an explicit checkout ofgithub.event.pull_request.head.sha— which must be treated as untrusted.
-
Use cases:
- Use
pull_requestfor automated testing, linting, or any scenario where the workflow needs to run checks directly on the proposed changes without requiring access to protected resources. - Use
pull_request_targetfor scenarios that require interacting with protected resources, such as deploying previews of changes or commenting on pull requests with sensitive information.
- Use
Best Practices#
- Be cautious with
pull_request_target: given its access to secrets, never check out and execute untrusted PR code (build scripts, test runners,npm installwith lifecycle scripts, etc.) in apull_request_targetjob. Doing so lets a malicious fork exfiltrate your secrets. - Prefer
pull_requestfor code checks: for most automated code checks (like linting and testing), stick withpull_requestto leverage its security model. - Use environment protection rules: gate jobs that handle secrets behind a GitHub Environment ↗ with a required-reviewers rule, so a maintainer must approve a run before secrets are exposed.
If you do need to check out the PR head under pull_request_target (for example, to build a preview), do it explicitly and treat that code as untrusted — don’t run it in the same job that has access to secrets:
on: pull_request_target: branches: - main
permissions: contents: read
jobs: build: runs-on: ubuntu-latest steps: # Explicitly check out the PR head instead of the base branch. - uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} # Never persist the privileged token into the checked-out repo. persist-credentials: false # ... build the untrusted code here, without using secrets ...Conclusion#
Choosing between pull_request and pull_request_target depends on your specific workflow requirements and security considerations. Remember to review GitHub’s article on preventing pwn requests ↗ for further details and best practices.