A self-hosted runner is a machine you own that runs workflow jobs. GitHub warns it has no guarantee of a clean, ephemeral VM, so untrusted workflow code can compromise it persistently. Avoid it for public repositories, keep the machine minimal, and use single job runners where you can.
Hosted versus self-hosted
GitHub's documentation says that, except single CPU runners, each GitHub-hosted runner is a new virtual machine. A job starts clean. Single CPU runners run in a container on a shared VM, so the isolation story is different there.
Self-hosted runners are yours to maintain. GitHub's hardening guide states they do not have guarantees about running in ephemeral clean VMs and can be persistently compromised by untrusted code in a workflow.
What persistent compromise means
If job A leaves a modified tool, a planted script or a cached credential on the machine, job B inherits it. Job B might be a release job with access to signing keys or production secrets.
The guide also warns that on a private repository, anyone who can open a pull request may be able to run code on the runner, depending on your workflow triggers.
Risk reducers
From the GitHub guidance and general practice:
- Do not use them for public repositories. GitHub advises against it.
- Minimize what is on the box. Keep sensitive data and network access to sensitive services low.
- Use just-in-time runners. These perform at most one job. GitHub notes hardware reuse can still expose information, so combine with fresh images.
- Separate runner groups. Put release jobs on dedicated runners that untrusted workflows cannot select.
- Restrict network egress. A runner that cannot reach the internet freely is harder to abuse.
- Rebuild from an image on a schedule. Do not let a runner age for months.
Triggers matter as much as hosts
Most runner compromises start with a workflow that runs untrusted code. Review pull_request_target use, as covered in fork safety, and keep fork pull requests off privileged runners.
Environment secrets also do not become safe just because they are scoped. The environments documentation notes that self-hosted runners are not isolated, so treat those secrets like repository secrets.
A quick audit
- List every self-hosted runner and which repositories can use it.
- Mark which ones handle release or deploy jobs.
- Check whether any are reachable by workflows triggered from forks.
- Find the oldest runner image. If it is more than a few months old, rebuild.
- Write down what credentials exist on each machine. Remove what is not needed.
Safeguard asset discovery can help inventory systems, and the free scan at gold.safeguard.sh/scan covers dependencies in your repositories.
Quick check
Are GitHub-hosted runners always clean per job?
For most sizes, each runner is a new VM. Single CPU runners run in a container on a shared VM, so read the docs for the size you use.
Is a private repository safe for self-hosted runners?
Not automatically. Anyone who can open a pull request that triggers a workflow may reach the runner.
Sources
Last reviewed: 19 October 2026.