github-actions
The canonical home for everything GitHub Actions in your repos: the security checklist to
apply when writing or reviewing a workflow, and the fleet-wide procedure for bumping action
pins to their latest versions. The full checklist lives in
references/security-checklist.md; read it whenever you touch
a workflow. This skill is the security layer over [[dev-env-setup]], which owns the CI
templates (already hardened to this standard).
The five that matter most
If you do nothing else when writing or reviewing a workflow, get these right:
- Pin every
uses: to a full commit SHA, not a tag or branch —
owner/repo@<40-hex-sha> # vX.Y.Z. Tags are mutable; a compromised maintainer can repoint
@v3 to malicious code and every downstream run picks it up silently (tj-actions, Trivy).
- Default
GITHUB_TOKEN to read-only. Add permissions: { contents: read } at the
workflow level; grant more only on the specific job that needs it, at the job level.
- Never
pull_request_target (or workflow_run) in public repos. They run with secret
access on fork-PR-controlled input. Use plain pull_request, or gate on the upstream event.
- Never interpolate
${{ github.* }} into run:. Branch names, PR titles, issue bodies
are attacker-controlled. Assign to an env: var first, then reference $VAR in the shell
(Ultralytics, Nx/s1ngularity). Same rule for LLM output and anything written to
$GITHUB_ENV / $GITHUB_PATH.
- Use OIDC for cloud credentials (AWS/Azure/GCP) instead of long-lived static secrets —
short-lived, job-scoped, nothing to steal.
Writing or reviewing one repo's workflow
- Read references/security-checklist.md and walk the
workflow against it. The most common real finding is unpinned actions and a missing
permissions: block.
- Pin/verify the refs. Resolve each action's latest release SHA and pin it with a version
comment:
a=actions/checkout
tag=$(gh release view --repo "$a" --json tagName -q .tagName) # e.g. v7.0.0
sha=$(gh api "repos/$a/commits/$tag" --jq .sha) # dereferences annotated tags
echo "uses: $a@$sha # $tag"
pinact automates this across a whole file (pinact run to pin tags→SHA with comments,
pinact run -u to update to latest). Install it with mise use -g pinact or
mise exec -- pinact if a repo pins it.
- Verify the pins resolve and the comments are honest before finishing:
bash "$CLAUDE_PLUGIN_ROOT/skills/dev-env-setup/scripts/check_action_refs.sh" .github/workflows
# SHA-pinned refs with a `# vX.Y.Z` comment are checked: the tag is resolved on the remote
# and its SHA must match the pin. A mismatch (lying comment / wrong SHA) FAILs.
If gh/pinact is unavailable or unauthenticated, say so and ask the user — never guess a SHA.
- Run actionlint + zizmor — correctness then security (the two analyzers the standard wires
in). actionlint catches schema/expression/
needs: errors; zizmor catches credential
persistence, template injection, over-broad permissions:actionlint -shellcheck= # correctness; -shellcheck= skips the run: shellcheck pass
zizmor --no-progress .github/workflows/ # security; exit 0 = clean, non-zero = real findings
zizmor --no-progress --fix=all .github/workflows/<file>.yml # apply mechanical fixes, then review
Both are mise-pinned in every dev-env repo (mise.toml); install ad hoc with
mise use -g zizmor actionlint elsewhere. Suppress a deliberate zizmor exception inline with
# zizmor: ignore[<audit>], never by lowering the persona. (Dogfooded: the dev-env-setup CI
templates ship one actions-lint job that runs both.)
Fleet-wide bump
Turn the per-repo recipe into one cross-repo pass (this is the "bump my fleet's actions"
request). Mirror the cadence in the [[dev-env-bump-backfill-fleet]] memory: do the whole fleet
in one session — branch, bump, verify, commit, push — so the repos stay in lockstep.
- Enumerate the fleet. Start from the dev-env fleet (repos carrying
DEV_ENV_VERSION in
mise.toml; the [[dev-env-bump-backfill-fleet]] memory lists the current set), and
cross-check with live discovery:gh repo list "$(gh api user -q .login)" --source --no-archived --limit 200 --json nameWithOwner -q '.[].nameWithOwner'
Keep only repos that actually have .github/workflows/. Show the user the target set and
confirm it before changing anything — don't sweep in repos they don't want touched.
- Per repo (work in a temp clone or worktree, never on a dirty main):
git switch -c chore/bump-actions
pinact run -u # pin + update every uses: to the latest SHA + comment
bash "$CHECKER" .github/workflows # verify refs resolve and comments match
git diff # eyeball before committing
Also add a permissions: { contents: read } block to any workflow missing one, and apply
any other checklist gaps you spot.
- Commit + push/PR with a consistent message per repo (e.g.
chore(ci): pin actions to SHAs and bump to latest). Open a PR unless the user wants direct
pushes. Do all repos in the same session, then report a one-line summary per repo.
Guardrails: gh/pinact missing or unauthenticated → stop and surface it. A repo whose CI
is intentionally bespoke → flag it, don't force the standard. Never push to a repo's default
branch directly.
How this skill is reached
- Editing a workflow → the
ci-action-ref-reminder hook fires and points here.
- Setting up / upgrading the dev env → dev-env-setup's CI templates ship pre-hardened to
this standard (SHA pins +
permissions:) and link back here; the dev-env v16 bump is where
this became the default.
- "Review this CI workflow" / "audit my GitHub Actions" → this skill's description triggers
directly.
1---2name: github-actions3description: Write, review, and harden GitHub Actions workflows against supply-chain attacks, and bump a whole fleet of repos' action pins to the latest versions. Use when writing or editing a workflow YAML (.github/workflows/*.yml, ci.yml), when asked to review a CI workflow or audit GitHub Actions security, when the ci-action-ref-reminder hook fires, or when the user wants to "bump my actions", "update the fleet", "pin actions to SHAs", run zizmor, or close supply-chain / pull_request_target / script-injection / GITHUB_TOKEN-permissions gaps. Pairs with references/security-checklist.md (the full checklist) and dev-env-setup's CI templates.4---56# github-actions78The canonical home for everything GitHub Actions in your repos: the security checklist to9apply when writing or reviewing a workflow, and the fleet-wide procedure for bumping action10pins to their latest versions. The full checklist lives in11[references/security-checklist.md](references/security-checklist.md); read it whenever you touch12a workflow. This skill is the security layer over [[dev-env-setup]], which owns the CI13*templates* (already hardened to this standard).1415## The five that matter most1617If you do nothing else when writing or reviewing a workflow, get these right:18191. **Pin every `uses:` to a full commit SHA**, not a tag or branch —20 `owner/repo@<40-hex-sha> # vX.Y.Z`. Tags are mutable; a compromised maintainer can repoint21 `@v3` to malicious code and every downstream run picks it up silently (tj-actions, Trivy).222. **Default `GITHUB_TOKEN` to read-only.** Add `permissions: { contents: read }` at the23 workflow level; grant more only on the specific job that needs it, at the job level.243. **Never `pull_request_target` (or `workflow_run`) in public repos.** They run with secret25 access on fork-PR-controlled input. Use plain `pull_request`, or gate on the upstream event.264. **Never interpolate `${{ github.* }}` into `run:`.** Branch names, PR titles, issue bodies27 are attacker-controlled. Assign to an `env:` var first, then reference `$VAR` in the shell28 (Ultralytics, Nx/s1ngularity). Same rule for LLM output and anything written to29 `$GITHUB_ENV` / `$GITHUB_PATH`.305. **Use OIDC for cloud credentials** (AWS/Azure/GCP) instead of long-lived static secrets —31 short-lived, job-scoped, nothing to steal.3233## Writing or reviewing one repo's workflow34351. Read [references/security-checklist.md](references/security-checklist.md) and walk the36 workflow against it. The most common real finding is unpinned actions and a missing37 `permissions:` block.382. **Pin/verify the refs.** Resolve each action's latest release SHA and pin it with a version39 comment:40 ```bash41 a=actions/checkout42 tag=$(gh release view --repo "$a" --json tagName -q .tagName) # e.g. v7.0.043 sha=$(gh api "repos/$a/commits/$tag" --jq .sha) # dereferences annotated tags44 echo "uses: $a@$sha # $tag"45 ```46 `pinact` automates this across a whole file (`pinact run` to pin tags→SHA with comments,47 `pinact run -u` to update to latest). Install it with `mise use -g pinact` or48 `mise exec -- pinact` if a repo pins it.493. **Verify the pins resolve and the comments are honest** before finishing:50 ```bash51 bash "$CLAUDE_PLUGIN_ROOT/skills/dev-env-setup/scripts/check_action_refs.sh" .github/workflows52 # SHA-pinned refs with a `# vX.Y.Z` comment are checked: the tag is resolved on the remote53 # and its SHA must match the pin. A mismatch (lying comment / wrong SHA) FAILs.54 ```55 If `gh`/`pinact` is unavailable or unauthenticated, say so and ask the user — never guess a SHA.564. **Run actionlint + zizmor** — correctness then security (the two analyzers the standard wires57 in). actionlint catches schema/expression/`needs:` errors; zizmor catches credential58 persistence, template injection, over-broad permissions:59 ```bash60 actionlint -shellcheck= # correctness; -shellcheck= skips the run: shellcheck pass61 zizmor --no-progress .github/workflows/ # security; exit 0 = clean, non-zero = real findings62 zizmor --no-progress --fix=all .github/workflows/<file>.yml # apply mechanical fixes, then review63 ```64 Both are mise-pinned in every dev-env repo (`mise.toml`); install ad hoc with65 `mise use -g zizmor actionlint` elsewhere. Suppress a deliberate zizmor exception inline with66 `# zizmor: ignore[<audit>]`, never by lowering the persona. (Dogfooded: the dev-env-setup CI67 templates ship one `actions-lint` job that runs both.)6869## Fleet-wide bump7071Turn the per-repo recipe into one cross-repo pass (this is the "bump my fleet's actions"72request). Mirror the cadence in the [[dev-env-bump-backfill-fleet]] memory: do the whole fleet73in one session — branch, bump, verify, commit, push — so the repos stay in lockstep.74751. **Enumerate the fleet.** Start from the dev-env fleet (repos carrying `DEV_ENV_VERSION` in76 `mise.toml`; the [[dev-env-bump-backfill-fleet]] memory lists the current set), and77 cross-check with live discovery:78 ```bash79 gh repo list "$(gh api user -q .login)" --source --no-archived --limit 200 --json nameWithOwner -q '.[].nameWithOwner'80 ```81 Keep only repos that actually have `.github/workflows/`. **Show the user the target set and82 confirm it before changing anything** — don't sweep in repos they don't want touched.832. **Per repo** (work in a temp clone or worktree, never on a dirty main):84 ```bash85 git switch -c chore/bump-actions86 pinact run -u # pin + update every uses: to the latest SHA + comment87 bash "$CHECKER" .github/workflows # verify refs resolve and comments match88 git diff # eyeball before committing89 ```90 Also add a `permissions: { contents: read }` block to any workflow missing one, and apply91 any other checklist gaps you spot.923. **Commit + push/PR** with a consistent message per repo (e.g.93 `chore(ci): pin actions to SHAs and bump to latest`). Open a PR unless the user wants direct94 pushes. Do all repos in the same session, then report a one-line summary per repo.9596Guardrails: `gh`/`pinact` missing or unauthenticated → stop and surface it. A repo whose CI97is intentionally bespoke → flag it, don't force the standard. Never push to a repo's default98branch directly.99100## How this skill is reached101102- **Editing a workflow** → the `ci-action-ref-reminder` hook fires and points here.103- **Setting up / upgrading the dev env** → dev-env-setup's CI templates ship pre-hardened to104 this standard (SHA pins + `permissions:`) and link back here; the dev-env v16 bump is where105 this became the default.106- **"Review this CI workflow" / "audit my GitHub Actions"** → this skill's description triggers107 directly.