GitHub 29 's guidance is blunt: self-hosted runners "should almost never be used for public repositories". Anyone can open a pull request from a fork whose workflow, or whose npm 2,036 test, runs code of their choosing. On a GitHub-hosted runner that code dies with the virtual machine; on yours it runs on hardware that persists, inside your network, perhaps next to cloud credentials, SSH keys or a Docker 514 socket.

BookNest's repository is public, so the demo shrank every link of that chain: a container instead of the host, a label no other workflow uses, a workflow that only workflow_dispatch (which needs write access) can start, one job only, and removal right after it. The approval rule for fork pull requests was also tightened from GitHub's default (first-time contributors) to every outside contributor:
gh api -X PUT repos/{owner}/{repo}/actions/permissions/fork-pr-contributor-approval \
-f approval_policy=all_external_contributors
gh api repos/{owner}/{repo}/actions/permissions/fork-pr-contributor-approval{"approval_policy":"all_external_contributors"}Approval is only a speed bump: approving without reading the diff still runs the attacker's code. And GitHub warns there is "no way to guarantee that a self-hosted runner only runs one job", hence a container deleted with it.
For real use, give self-hosted runners to private repositories, run each job on a fresh virtual machine or container (GitHub's Actions Runner Controller (https://github.com/actions/actions-runner-controller 6,531 ) does this on Kubernetes 5,150 , which Kubernetes introduces), keep secrets out of their environment, and never let a pull_request_target workflow check out and run a fork's code on them.