A trust checklist for GitHub Actions
Every action you reference in a workflow runs on your runner, with access whatever token you hand it. `uses: some-org/some-action@v3` is an instruction to execute someone else's code. It deserves at least as much scrutiny as a production dependency, and receives considerably less.
A trust checklist for GitHub Actions
Every action you reference in a workflow runs on your runner, with access to your workspace and whatever token you hand it. uses: some-org/some-action@v3 is an instruction to execute someone else's code inside your CI environment. It deserves at least as much scrutiny as a production dependency, and usually receives considerably less.
What follows is the checklist I use when assessing whether an action can be trusted. A caveat before the list: this is partly a wish list. Some of these attributes are present on very few actions today. Not all of them can be detected automatically or queried through an API, and several require manual review or AI assistance to establish. Treat it as a set of weighted signals rather than a pass/fail gate.
The list is split into two tiers: signals that carry real weight, and signals that are worth a glance but should not be decisive.
Tier 1: signals that carry weight
Tooling and maintenance
| Signal | How to verify |
|---|---|
| The repository uses GitHub Advanced Security for Actions, ActionLint, and/or zizmor | PR checks and workflow run logs |
| The repository uses Dependabot or Renovate — for both its action references and its underlying implementation dependencies — and is current with updates | Pull requests and workflow logs |
| The repository configures a Renovate/Dependabot cooldown (minimum release age) | Configuration file on the default branch |
| The action has unit and/or integration tests | Repository contents and workflow runs |
| The repository uses StepSecurity Harden-Runner or GitHub's new egress firewall in its own workflows | Workflow files in the repository |
On that last point: check that the firewall is not running in audit mode only. Audit mode records egress without blocking it, which is a useful first step but provides no actual containment.
Runtime and dependency hygiene
| Signal | How to verify |
|---|---|
| Uses a currently supported Node handler (JavaScript actions) | action.yml |
| Uses an up-to-date base image (Docker actions) | action.yml |
Does not depend on unpinned actions, npm packages or Python packages, or on versions that cannot be controlled through inputs — no bare npx eslint |
Not always straightforward. AI-assisted review, such as Copilot code review, frequently finds these |
Does not use floating Docker image tags such as latest |
action.yml for Docker actions. Harder for actions that invoke the Docker CLI internally; AI review often catches these |
| For composite actions: references pinned, locked or immutable actions | Largely verifiable with GHAS for Actions, plus manual review |
| Validates remotely downloaded artefacts | Sometimes detectable with AI. Otherwise manual review |
The unpinned-dependency category is the one I would prioritise. An action that pins its own action references but then runs npx some-tool at execution time has simply moved the unpinned dependency one level down, where it is harder to see.
Release integrity
| Signal | How to verify |
|---|---|
| Versioning is implemented correctly — semantic versioning, correctly maintained floating tags | Difficult to inspect. I wrote a GitHub Action and PowerShell library for it. A surprising number of actions have a questionable history where version numbers are concerned |
| Uses immutable releases | Release metadata. A recently introduced feature; many actions still need to adopt it |
| Has release attestation, and the attestation validates | Should be attached to the GitHub release, and validation should be performed against the original source |
| Passes an anti-virus scan | You must do this yourself — GitHub does not virus-scan action releases |
One detail worth knowing about attestations: they are bound to the source repository and cannot be transferred. If your organisation mirrors actions into an internal repository, the attestation does not travel with the mirror. Validation has to happen against the original source.
Governance
| Signal | How to verify |
|---|---|
| Has licence, security and contributing policies | Policy files on the main branch |
| Uses a licence your organisation accepts | Licence file. In practice it is often sufficient that a licence is defined at all |
Tier 2: weaker signals
These are worth looking at. None of them should decide the matter on its own, and each comes with a meaningful caveat.
| Signal | Why it is weak |
|---|---|
| Published on the Marketplace | Several widely used actions are not, including GitHub's own CodeQL actions |
| No dependencies with known security issues | Sometimes you are simply waiting for a fix to propagate through the dependency chain. Sometimes a vulnerability does not apply to the Actions hosting model at all and can safely be ignored |
| No open security advisories for recent versions | Useful, but absence of advisories is not evidence of absence of problems |
| A reasonable OpenSSF Scorecard score | Genuinely hard to achieve. Few organisations or individuals make the effort. I attempted it for my own actions and abandoned it — a great deal of work for very little recognition |
| More than one maintainer | Wishful thinking for many niche actions |
| Hosted by an organisation rather than a personal account | Same caveat |
| The repository has required PR checks | Only visible in recent pull requests and workflow logs |
| Recent commits went through pull requests | Wishful thinking for many niche actions |
| Recently updated | Often unnecessary for PowerShell or Bash actions. Considerably more important for Docker and JavaScript actions |
| Verified creator badge | The badge adds very little and represents no additional security guarantee |
| Many stars | Stars can be bought in bulk, and cheaply |
| Few open issues | Some actions accumulate many issues precisely because the community is active — Renovate, for example. Others run aggressive stale-bot configurations to keep the count low |
| Few open pull requests | Same reasoning. Pull request cycle time would be a better indicator, but it is not easy to read out |
| Used by organisations you trust | The dependents view shows which organisations use the action directly on public GitHub. A reasonable indication of who else has extended trust |
| Has a clear feedback channel (issues, community, email, support link) | Indicates responsiveness rather than security |
| From a well-known organisation (AWS, Microsoft, Azure, GitHub) | No guarantee. Large vendors have been caught by recent supply-chain worms such as Shai-Hulud |
| The release is a signed commit | Commit signing remains awkward in public open source, and its meaning is limited — anyone can sign a commit |
| The repository has Copilot code review, CodeRabbit or a similar integration enabled | AI offers no guarantee and is sometimes wrong, but it does often catch things that would otherwise slip past |
The recurring theme in this tier is that most of these signals measure popularity or project health, not security. They are easy to query, which is precisely why they get over-weighted. Ease of measurement is not the same as relevance.
Working through it in practice
Not every signal costs the same to check. It helps to separate them by effort:
- Run automated checks on the candidate action. ✅
- Is there a clear disqualifier? ❓
- Yes: reject the action or request changes. ❌
- No: continue with AI-assisted review. 🤖
- Are there findings that need confirmation? ❓
- Yes: perform a manual review. 🔎
- No: manually review high-risk areas only. ⚠️
- Make a decision. ✅
- Lock the resolved commit regardless of the decision. 🔒
Automatable or queryable: presence of policy files, licence type, Node handler version, action.yml contents, immutable release status, marketplace presence, advisory status, maintainer count, update recency.
Requires AI assistance to find reliably: unpinned transitive dependencies, floating Docker tags in shell invocations, unvalidated remote downloads. These are the categories where reading every line manually is impractical and pattern matching is unreliable.
Requires manual review: versioning history, artefact validation logic, whether an egress firewall is actually enforcing, and the overall question of whether the action does what it claims.
The control that matters most
The checklist tells you whether an action is worth trusting at the moment you look at it. Something else has to make that assessment hold, because a floating tag can be force pushed at any time — by the maintainer, or by whoever compromises the account. The version you assessed is not necessarily the version that executes tomorrow.
Three mechanisms address this, and they achieve broadly the same effect:
| Mechanism | Where it applies |
|---|---|
| Commit SHA pinning | Consumer side, per reference |
| Immutable releases | Producer side, per release |
| Action lock files | Consumer side, per repository |
All three ensure the code you reviewed is the code that runs. My clear preference is for immutable releases and lock files over raw SHA pinning.
Immutable releases
When a repository has immutable releases enabled, a release published with a release-specific tag such as v1.0.0 becomes unchangeable. The tag cannot be moved afterwards.
Note that this applies to tags tied to a GitHub release. Floating convenience tags such as v1 or v1.1 — which maintainers deliberately move to the latest compatible patch — are not covered, and are not intended to be. That is precisely why consumers still need a locking mechanism of their own.
For maintainers, this is one of the cheapest items in Tier 1 to adopt, and it improves the situation for every consumer at once.
Action lock files
gh-actions-lock is a gh CLI extension, part of GitHub's Workflow Dependency Pinning effort. It produces a lockfile at .github/workflows/actions.lock that pins every workflow dependency to a verified commit.
gh extension install github/gh-actions-lock
gh actions-lock # scan workflows, pin, write the lockfile
gh actions-lock --relock # re-resolve refs that have legitimately moved
The important point is that this is not merely SHA pinning with extra steps. The lockfile carries verification that a bare SHA in a uses: line cannot:
- Onboarded workflows enforce that every dependency is present in the lockfile, and that the locked commit is what executes on the runner.
- Lockfiles are verified against forgery: the SHA must exist in the refs it claims to exist in.
- Repository identity is recorded, and redirects or mismatches are blocked at runtime.
- A locked action must have a branch containing the locked commit, which makes impostor-commit attacks considerably harder.
That last point deserves emphasis. A raw SHA in a workflow file offers no protection against an impostor commit — a commit pushed to a fork, which remains reachable through the upstream repository's object store and therefore resolves successfully. It looks entirely legitimate in a uses: line. The lockfile's reachability check is what closes that gap.
Do note that the extension is a technical preview and pre-1.0; the lockfile format and flags may change.
Why not raw SHA pinning
SHA pinning works, and it is better than a floating tag. But as a practice it has real drawbacks:
- It destroys readability.
uses: some-org/some-action@a1b2c3d…conveys nothing. The version has to be carried in a trailing comment, which nothing validates and which drifts from reality over time. - It is per-reference. Every workflow file must be maintained individually, rather than governed by one lockfile.
- It offers no verification. As above, a SHA alone does not prove the commit belongs to the repository's history.
A lock file gives you the same guarantee with better ergonomics and meaningfully stronger verification. Immutable releases give you upstream guarantees that need no consumer-side maintenance at all.
Whichever you choose, combine it with dependency automation: let Renovate or Dependabot propose updates, configured with a cooldown period, so that a compromised release has time to be discovered before it reaches your pipelines.
Using the list sensibly
Very few actions will satisfy the whole of Tier 1. That is not a reason to discard the list; it is a reason to use it comparatively. When two actions solve the same problem, the checklist gives you a defensible basis for choosing one. When only one action solves the problem, it tells you which risks you are accepting, and where compensating controls are needed.
It also gives maintainers a roadmap. Much of Tier 1 is achievable by a single maintainer with focused effort — policy files, dependency automation with a cooldown, locked references, immutable releases, an attestation on the release. That is a materially different security posture from where most actions currently sit, and it does not require an organisation behind it.
Tooling that covers part of this
StepSecurity's GitHub Action Advisor implements a portion of the automatable side of this list — maintenance and release signals, dependency and pinning hygiene, policy files, advisories — and presents them as a per-action risk assessment. It also adds signals this checklist cannot easily produce on its own, such as observed runtime behaviour drawn from Harden-Runner telemetry across the organisations that use it.
Organisations that have granted the StepSecurity GitHub App access to their repositories get more from it: the advisor can then evaluate the actions actually referenced across the estate, rather than only the one you happen to be looking up.
It does not replace Tier 1 entirely — the AI-assisted and manual categories above remain yours to cover — but it is a reasonable way to get the queryable signals in one place instead of assembling them by hand.