← Actions Self-Hosted Exposure Check

Actions Self-Hosted Exposure Check vs. reviewing workflows by eye

Reading a workflow file for a self-hosted runner under a risky trigger is a real skill, and an experienced reviewer catches the obvious case every time. This page names no product or company -- "reviewing by eye" is the generic habit every maintainer already has, not a competing tool.

Where a careful read holds up, and where it slips

A job with runs-on: self-hosted sitting right below an on: pull_request_target trigger is easy to spot once you know the shape. What's harder to hold in your head while scanning a real repository's workflow files: self-hosted can show up as one label among several in a runs-on: list rather than the whole value; workflow_run and issue_comment carry the same fork-influenced risk as pull_request_target without "pull request" in the name to cue the reviewer; a second job further down the same file can have its own, different runs-on: after the first job already looked clean; and a runner group or a ${{ }} matrix expression can't be judged safe or unsafe from the file text at all, which is easy to either wave through or flag on a hunch.

Where to answer each question.
CaseReviewing by eyeThis tool
Bare runs-on: self-hosted under pull_request_targetEasy to catchCatches it the same way
runs-on: [self-hosted, linux, x64] under the same triggerEasy to miss when self-hosted is one item in a list rather than the whole lineParses every label in flow-list, block-list, or single-value form
Trigger is workflow_run or issue_comment rather than pull_request_targetEasy to read past -- review habit is trained on pull_request_target, and neither name says "pull request"Checked against the same fixed four-trigger set regardless of which one is top of mind
A second job further down the same workflow file, with its own runs-on:Easy to skip once the first job in the file already looked cleanEvery job block in the file is found and classified independently
runs-on: selects a runner group by nameEasy to guess safe or unsafe from the group's name aloneReported honestly as unclassifiable from this file, not guessed either way
runs-on: ${{ matrix.os }} (a matrix expression)Easy to wave through since it doesn't literally say self-hostedFlagged as unable to classify statically rather than silently passed as safe
A repository with a dozen workflow filesRe-reading all of them, every time a workflow changes, is tedious and easy to shortcutPaste as many files as you have, or run check.py across a whole checked-out repository
Blocking a newly-added finding before it mergesDepends on a human noticing it in reviewcheck.py --fail-on-risk gives a real exit code from the same classification, wired into your own CI

The two are complementary, not competing: this tool does not decide whether a flagged job is actually reachable from an attacker in your specific setup, or whether the repository is even public -- it makes no network calls and never checks that. It replaces the tedious, error-prone part of the read-through: finding every job in every file, parsing every runs-on: form, and checking each one against the same fixed trigger list every time.

Run the free self-hosted exposure check

Common questions

What does reviewing a workflow for self-hosted exposure by eye actually catch?

A reviewer who knows the pattern will catch the obvious case: a job with runs-on: self-hosted sitting a few lines below an on: pull_request_target trigger. That recognition is real, and this tool checks the same underlying combination -- a self-hosted label and one of four documented risky triggers -- rather than a wider or different rule.

Why is self-hosted easy to miss when it's inside a runs-on: list?

runs-on: can be a single label, a flow-style list like runs-on: [self-hosted, linux, x64], or a block list with one label per line. A reviewer scanning quickly for the literal line runs-on: self-hosted can walk right past self-hosted when it's the first of three items in a list, especially when the eye lands on a more distinctive label like linux or x64 first. This tool parses every label in the list, in any of the three forms, and matches self-hosted as one item among several rather than expecting the whole value to equal that one word.

Why are workflow_run and issue_comment easy to forget as risky triggers?

Most self-hosted-exposure review habit is trained on pull_request_target, because that trigger's whole reputation is built on the pwn-request pattern. workflow_run and issue_comment do not have "pull request" in the name, so a reviewer scanning a workflow's on: block for the familiar trigger can read past both without recognizing that they hand a stranger the same kind of influence GitHub's own guidance already names. This tool checks a job's runs-on: against the same fixed set of four triggers regardless of which one happens to be top of mind that day, and names the specific trigger that matched in its output.

Why is a second job in the same workflow file easy to miss?

A reviewer who reads the first job in a file, confirms its runs-on: is safe, and moves on to the next file can stop reading before a second job further down the same file -- one with its own, different runs-on: line. This tool finds every job block in a file independently and classifies each one on its own runs-on:, so a risky second job is reported even when the first job in the same file is clean.

Does a clean result from this tool mean my self-hosted runners are safe?

No. It checks one specific, narrow pattern in the workflow files you give it: the literal self-hosted label combined with one of four documented risky triggers, in the jobs it can classify statically. A runs-on: that selects a runner group by name, or one built from a ${{ }} matrix expression, is reported honestly as unclassifiable from the file alone rather than guessed either way -- and it makes no network call, so it never knows whether the repository the workflow lives in is actually public.