GitHub-hosted vs self-hosted runners: when self-hosted is safe
Verdict: Never attach self-hosted runners to a public repository. Any fork can open a pull request that runs untrusted code on a machine that persists state between jobs. Keep self-hosted runners on private repositories only.
| Criterion | GitHub-hosted runners | Self-hosted runners |
|---|---|---|
| Public repo safety | Ephemeral clean VM per job; safe for fork pull requests | Almost never use; a fork PR can run untrusted code and persistently compromise the machine |
| Isolation | Fresh VM discarded after each job | State can persist between jobs unless the runner is ephemeral |
| Architecture support | Standard hosted set | ARM64 on Linux, macOS and Windows; ARM32 on Linux only |
| Behind an IP allow list | Too many frequently changing IP ranges to allowlist | Add the runner IP range to the enterprise allow list |
| When hosted runners are disabled | Enterprise can disable standard hosted runners | Jobs must then target an approved runner group |
Rules
- Self-hosted runners should almost never be used on public repositories: any user can open a compromising pull request that runs on the runner.
- The danger is untrusted fork code on a persistent machine, not billing or an inability to run - a fork can compromise internal infrastructure.
- Avoid pull_request_target or workflow_run with untrusted code; those triggers can leak secrets and a privileged token.
- Self-hosted runner ARM64 support spans Linux, macOS and Windows; ARM32 support is Linux only.
- When an enterprise disables standard GitHub-hosted runners, workflows must target runners through an approved runner group.
Traps
- Ephemeral single-job runners reduce persistence but do not eliminate the risk of a single malicious pull request run.
- A required passing status check does not prevent a malicious fork pull request from running code on the runner first.