A just-in-time (JIT) runner is a self-hosted CI runner registered for a single job and removed automatically afterwards. Because no job inherits the files, processes or credentials of an earlier one, it cuts the carry-over risk that makes long-lived self-hosted runners hard to secure. It still needs a clean machine each time.
The problem it solves
A normal self-hosted runner stays registered and picks up job after job. GitHub's documentation says such runners are not guaranteed to run in clean virtual machines and can be persistently compromised by untrusted code in a workflow. One bad job can leave a file, a background process or a modified tool behind for the next job to trust.
Some teams try to patch this by deleting the runner after each job. GitHub's docs say that approach may be less effective than it seems, because there is no guarantee that a self-hosted runner only runs one job, and secrets passed as command line arguments can be seen by another job on the same runner, for instance with ps. A JIT runner moves the guarantee into the registration itself.
How it works on GitHub
Per GitHub's docs, you use the REST API to create an ephemeral JIT runner. The API returns a configuration, and you pass it to the runner at startup. The runner performs at most one job and is then automatically removed from the repository, organization or enterprise.
./run.sh --jitconfig "$ENCODED_JIT_CONFIG"
A small control program, often triggered by a webhook for a queued job, requests the config, starts a fresh machine or container, and hands it the config.
What it does not fix
The docs add an important note: re-using hardware to host JIT runners can risk exposing information from the environment. If the same virtual machine or disk serves the next JIT runner, you have recreated the carry-over problem. Use automation to give each runner a clean environment.
GitLab's guidance for its Docker Machine executor points the same way. It recommends a setting of MaxBuilds equal to 1 so a single autoscaled virtual machine handles one and only one job. The principle is the same across platforms: one job, one fresh environment.
Checklist for a small team
| Question | Good answer |
|---|---|
| Does each job get a fresh machine or container? | Yes, built from a known image |
| Is the runner registration single use? | Yes, JIT or the platform equivalent |
| Does the image contain long-lived credentials? | No |
| Can the runner reach cloud metadata services? | Blocked unless needed |
| Is the runner group limited to trusted repositories? | Yes |
| Does a job run as an unprivileged user? | Yes |
Is it worth the work?
For most small teams, the honest answer is to use hosted runners, which GitHub describes as ephemeral and isolated, and keep self-hosted for jobs that truly need private network access. Our post on whether a small team should self-host runners walks through that decision. If you do self-host, JIT runners are the pattern to aim for.
Remember that runner isolation does not protect against a bad dependency installed inside the job. That is a different layer.
Where Safeguard fits
Safeguard's dependency scanning and policy gates apply to what a job installs and builds, whichever runner executes it. Runner lifecycle is your platform setup.
Quick check
Does a JIT runner stop untrusted pull requests from running code?
No. It stops one job from affecting the next. Untrusted code still runs in its own job, so you still control who can trigger workflows.
Is a JIT runner the same as an autoscaling runner?
Not necessarily. Autoscaling adds and removes capacity. A JIT runner is limited to one job by its registration. Combining them is common.
Sources
Last reviewed: 19 October 2026.