| name | harden-github-actions |
| description | Whenever you create or substantially edit a GitHub Actions workflow (.github/workflows/*.yml), run `uvx zizmor --persona pedantic` on it and fix every finding before considering the file done. Use this any time you add a new workflow, paste a template workflow (e.g. the Claude GitHub App, a marketplace action, a generated CI/CD file), or change `uses:`, `permissions:`, or trigger blocks — even if the user didn't mention security or zizmor. A workflow that hasn't been run through the pedantic audit is not finished. |
| allowed-tools | Bash Read Write Edit Grep Glob |
| metadata | {"author":"Alexander Opalic","version":"0.1.0"} |
Harden GitHub Actions with zizmor
zizmor is a static analyzer for GitHub Actions workflows. New workflow files —
especially ones pasted from templates or generated by an app — almost always ship
with insecure defaults (unpinned actions, no permissions boundary, credential
persistence). If the repo runs zizmor in CI, those defaults fail the pipeline; even
if it doesn't, they're real supply-chain and token-leak risks. So treat the audit as
part of writing the workflow, not a separate review step that happens later.
When this applies
Run the audit whenever you have:
- Added a new file under
.github/workflows/.
- Pasted or generated a workflow from a template (Claude GitHub App, an action's
README, a scaffolding tool).
- Edited an existing workflow's
uses:, permissions:, on:, or concurrency:.
You don't need the user to ask for "security" or mention "zizmor" — hardening is the
default expectation for any workflow you touch.
The loop
-
Match the repo's existing conventions first. Before inventing hardening, read a
workflow in the same repo that already passes (often ci.yml or codeql.yml). Copy
its action pins, its permission-comment style, its concurrency group naming. A
consistent repo is easier to review than a "correct" but idiosyncratic file. Use
grep -nE "uses:|permissions:|persist-credentials|concurrency:" .github/workflows/*.yml
to see what's already established.
-
Run the audit on the file(s) you created or changed:
uvx zizmor --persona pedantic .github/workflows/<file>.yml
--persona pedantic is the strictest mode and is what hardened repos tend to enforce
in CI, so matching it locally avoids a surprise pipeline failure. Pass every file you
touched. Exit code 0 with "No findings to report" means done.
-
Fix every finding, not just the error-level ones. The pedantic persona reports
informational findings too, and a CI gate with no min-severity set fails on any
finding. The common ones and their fixes are below.
-
Re-run until clean (exit 0). Then you're done.
Common findings → fixes
These are the recurring ones. Apply the fix that matches how the rest of the repo does it.
| Finding | Why it matters | Fix |
|---|
| unpinned action reference (error) | A mutable tag like @v4 can be repointed at malicious code; pinning to a commit SHA is immutable. | Pin to the full commit SHA with the tag in a trailing comment: uses: actions/checkout@34e1148… # v4. Resolve the SHA for an action (see below). |
| overly broad permissions | Default GITHUB_TOKEN permissions are write-all; an exploited step can push code or open releases. | Add top-level permissions: {} and grant each job only what it needs. |
| permissions without explanatory comments | Reviewers can't tell why a scope was granted. | Add an inline comment per permission line: contents: read # checkout. |
| credential persistence | actions/checkout leaves the git token on disk by default, readable by later steps. | Add persist-credentials: false # don't leave the git token on disk to the checkout with: block. |
| insufficient job-level concurrency limits | Stacked runs on the same ref waste minutes and can race. | Add a top-level concurrency: block with a group: keyed on the ref/PR and cancel-in-progress: true. |
| anonymous-definition (informational) | An unnamed job shows up with an opaque check name. | Give each job a name:. |
Resolving a commit SHA for an action
Lightweight tags resolve directly; annotated tags (common for @v1-style major tags)
need one extra dereference step. Use the GitHub API:
gh api repos/<owner>/<repo>/git/refs/tags/<tag>
gh api repos/<owner>/<repo>/git/tags/<tag-object-sha>
Pin to that commit SHA and keep the human-readable tag in a # v1 comment so the file
stays reviewable.
Notes
uvx runs zizmor in an ephemeral environment, so nothing needs to be installed
globally. If uvx isn't available, fall back to pipx run zizmor, docker run --rm -v "$PWD":/work -w /work ghcr.io/zizmorcore/zizmor:latest, or a local zizmor.
- Some findings have legitimate exceptions. If a finding genuinely doesn't apply, prefer
a scoped
# zizmor: ignore[<rule>] comment on the offending line (with a reason) over
silencing the whole audit — that keeps the rest of the file covered and documents the
decision.
- Online audits (e.g. checking whether a pinned SHA is stale) need network access and a
token; the local run above covers the static rules, which are what gate CI.