In my earlier posts about custom images, I demonstrated how to configure a pre- or post-job script. This is a great way to customize the environment for your runners. Today, I wanted to touch on another use case for custom images: workflow validation.
The scenario
As an example, consider this scenario: you want to ensure that only specific workflows in a repository can use a runner. You could add a validation step to each job, but anyone who can edit the workflow can remove or change it. You could also create a required workflow that reads the runs-on for every workflow and validates it against an allow list. That helps, but a workflow with a pull_request trigger might still start running before the required workflow gets a chance to block it.
With a custom image and a pre-job script, the workflow can’t bypass your checks. The pre-job script has the workflow context details and runs before any user-provided commands. If the script fails, the job stops before it executes anything else.
Implementation
The key to making this work is the set of GITHUB_
environment variables that GitHub sets on the runner. These variables tell you which workflow, job, and repo triggered the run. Your pre-job script can read them and validate the run against an allow list. For this example, GITHUB_WORKFLOW_REF (in the format owner/repo/.github/workflows/workflow.yml@refs/heads/branch) does most of the heavy lifting because it includes the full workflow file reference. I also like pairing it with GITHUB_REPOSITORY (in the format owner/repo) and GITHUB_REPOSITORY_OWNER to lock the runner down even further.
For this example, I want to only allow publish.yml and build.yml workflows to use the runner. I want publish workflows to only run on the main branch, while build workflows can run on any branch. Finally, I want to restrict this validation to a specific repository, kenmuse/myrepo. A simple version of a pre-job script for this validation might look like this:
1allowed_repo="kenmuse/myrepo"
2
3# Example:
4# GITHUB_WORKFLOW_REF=kenmuse/myrepo/.github/workflows/publish.yml@refs/heads/main
5workflow_ref="${GITHUB_WORKFLOW_REF:-}"
6repo="${GITHUB_REPOSITORY:-}"
7
8fail() {
9 echo "ERROR: $*" >&2
10 exit 1
11}
12
13[[ -n "$workflow_ref" ]] || fail "GITHUB_WORKFLOW_REF is not set"
14[[ -n "$repo" ]] || fail "GITHUB_REPOSITORY is not set"
15
16# The run must be from the allowed repository.
17[[ "$repo" == "$allowed_repo" ]] || fail "Repository '$repo' is not allowed"
18
19# Make sure it's not a workflow with the same name being
20# called from a different repo.
21[[ "$workflow_ref" == "$allowed_repo/"* ]] || fail "Workflow ref not from allowed repo: $workflow_ref"
22
23# Split the workflow ref details
24workflow_path="${workflow_ref%@*}"
25git_ref="${workflow_ref#*@}"
26workflow_file="${workflow_path##*/}"
27
28case "$workflow_file" in
29 build.yml)
30 # Allowed on any branch/ref.
31 ;;
32 publish.yml)
33 # Only allowed on main.
34 [[ "$git_ref" == "refs/heads/main" ]] || fail "publish.yml is only allowed on refs/heads/main (got '$git_ref')"
35 ;;
36 *)
37 fail "Workflow '$workflow_file' is not allowed"
38 ;;
39esac
40
41echo "Workflow validation passed: repo=$repo workflow=$workflow_file ref=$git_ref" >&2Just make sure your custom image includes this script and that your have an environment variable in /etc/environment called ACTIONS_RUNNER_HOOK_JOB_STARTED that points to it. Now any time a runner uses this image, the pre-job script verifies the workflow before any steps execute.
How does this help security?
One of the ways malicious actors target continuous integration and continuous delivery (CI/CD) pipelines is by submitting workflows that run untrusted code or attempt to access secrets. A pre-job script in a custom image lets you enforce strict allow-list validation for workflow runs, which reduces the risk of unauthorized code execution.
Conclusion
Pre-job scripts in custom runner images give you a reliable way to enforce workflow validation that workflow authors can’t bypass. When you use the GitHub environment variables available on the runner, you can build validation logic that only allows approved workflows onto your infrastructure. This pattern works especially well when you need to enforce security policies, control cost and resource usage, or meet compliance requirements. Best of all, the validation runs before any user-provided steps.
