From 699cefcea956dd21790e50a8fd60a657f87992ca Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue, 16 Jun 2026 01:04:53 +0000
Subject: [PATCH] chore: publish from staged
---
docs/README.skills.md | 1 +
skills/github-actions-hardening/SKILL.md | 160 ++++++++++++++++++
.../references/injection.md | 86 ++++++++++
.../references/permissions-and-tokens.md | 76 +++++++++
.../references/report-format.md | 65 +++++++
.../references/supply-chain.md | 71 ++++++++
.../references/triggers-and-privilege.md | 89 ++++++++++
7 files changed, 548 insertions(+)
create mode 100644 skills/github-actions-hardening/SKILL.md
create mode 100644 skills/github-actions-hardening/references/injection.md
create mode 100644 skills/github-actions-hardening/references/permissions-and-tokens.md
create mode 100644 skills/github-actions-hardening/references/report-format.md
create mode 100644 skills/github-actions-hardening/references/supply-chain.md
create mode 100644 skills/github-actions-hardening/references/triggers-and-privilege.md
diff --git a/docs/README.skills.md b/docs/README.skills.md
index 47369d00..c3eeb635 100644
--- a/docs/README.skills.md
+++ b/docs/README.skills.md
@@ -186,6 +186,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to
| [git-commit](../skills/git-commit/SKILL.md)
`gh skills install github/awesome-copilot git-commit` | Execute git commit with conventional commit message analysis, intelligent staging, and message generation. Use when user asks to commit changes, create a git commit, or mentions "/commit". Supports: (1) Auto-detecting type and scope from changes, (2) Generating conventional commit messages from diff, (3) Interactive commit with optional type/scope/description overrides, (4) Intelligent file staging for logical grouping | None |
| [git-flow-branch-creator](../skills/git-flow-branch-creator/SKILL.md)
`gh skills install github/awesome-copilot git-flow-branch-creator` | Intelligent Git Flow branch creator that analyzes git status/diff and creates appropriate branches following the nvie Git Flow branching model. | None |
| [github-actions-efficiency](../skills/github-actions-efficiency/SKILL.md)
`gh skills install github/awesome-copilot github-actions-efficiency` | Audit GitHub Actions workflow efficiency and recommend fixes to reduce CI minutes and costs. | `references/actions.md`
`references/patterns.md`
`references/reporting.md`
`references/review-rubric.md` |
+| [github-actions-hardening](../skills/github-actions-hardening/SKILL.md)
`gh skills install github/awesome-copilot github-actions-hardening` | Security hardening reviewer for GitHub Actions workflow files (.github/workflows/*.yml). Reasons about the Actions threat model that pattern matchers and general code linters miss — untrusted-input script injection, privileged triggers running fork code, mutable action references, and over-scoped tokens. Use this skill when asked to review, audit, harden, or secure a GitHub Actions workflow, when writing a new workflow, or for any request like "is this workflow safe?", "review my CI for security issues", "why is pull_request_target dangerous here?", "pin my actions", or "lock down GITHUB_TOKEN permissions". Covers script injection via ${{ }} interpolation, pull_request_target / workflow_run privilege escalation, SHA-pinning of third-party actions, least-privilege permissions, GITHUB_ENV/GITHUB_OUTPUT injection, secret exposure, OIDC over long-lived credentials, and self-hosted runner exposure on public repositories. | `references/injection.md`
`references/permissions-and-tokens.md`
`references/report-format.md`
`references/supply-chain.md`
`references/triggers-and-privilege.md` |
| [github-codespaces-efficiency](../skills/github-codespaces-efficiency/SKILL.md)
`gh skills install github/awesome-copilot github-codespaces-efficiency` | Audit and improve GitHub Codespaces efficiency. Use this skill when a user wants faster Codespaces startup, lower Codespaces spend, slim devcontainers, right-size machines, tune idle timeout, or scope prebuilds to branches with sustained usage. | `references/codespaces.md`
`references/review-rubric.md` |
| [github-copilot-starter](../skills/github-copilot-starter/SKILL.md)
`gh skills install github/awesome-copilot github-copilot-starter` | Set up complete GitHub Copilot configuration for a new project based on technology stack | None |
| [github-issues](../skills/github-issues/SKILL.md)
`gh skills install github/awesome-copilot github-issues` | Create, update, and manage GitHub issues using MCP tools. Use this skill when users want to create bug reports, feature requests, or task issues, update existing issues, add labels/assignees/milestones, set issue fields (dates, priority, custom fields), set issue types, manage issue workflows, link issues, add dependencies, or track blocked-by/blocking relationships. Triggers on requests like "create an issue", "file a bug", "request a feature", "update issue X", "set the priority", "set the start date", "link issues", "add dependency", "blocked by", "blocking", or any GitHub issue management task. | `references/dependencies.md`
`references/images.md`
`references/issue-fields.md`
`references/issue-types.md`
`references/projects.md`
`references/search.md`
`references/sub-issues.md`
`references/templates.md` |
diff --git a/skills/github-actions-hardening/SKILL.md b/skills/github-actions-hardening/SKILL.md
new file mode 100644
index 00000000..61fa956e
--- /dev/null
+++ b/skills/github-actions-hardening/SKILL.md
@@ -0,0 +1,160 @@
+---
+name: github-actions-hardening
+description: Security hardening reviewer for GitHub Actions workflow files (.github/workflows/*.yml). Reasons about the Actions threat model that pattern matchers and general code linters miss — untrusted-input script injection, privileged triggers running fork code, mutable action references, and over-scoped tokens. Use this skill when asked to review, audit, harden, or secure a GitHub Actions workflow, when writing a new workflow, or for any request like "is this workflow safe?", "review my CI for security issues", "why is pull_request_target dangerous here?", "pin my actions", or "lock down GITHUB_TOKEN permissions". Covers script injection via ${{ }} interpolation, pull_request_target / workflow_run privilege escalation, SHA-pinning of third-party actions, least-privilege permissions, GITHUB_ENV/GITHUB_OUTPUT injection, secret exposure, OIDC over long-lived credentials, and self-hosted runner exposure on public repositories.
+---
+
+# GitHub Actions Hardening
+
+A focused security reviewer for GitHub Actions workflows. It reasons about the *Actions-specific*
+threat model — where trust boundaries live in trigger types, token scopes, and string
+interpolation — rather than the application-code vulnerabilities a general security scanner looks
+for. Most workflow risks are invisible to language linters because the dangerous code is the YAML
+itself and the way GitHub expands `${{ }}` expressions into a shell before your script runs.
+
+## When to Use This Skill
+
+Use this skill when the request involves:
+
+* Reviewing, auditing, or hardening any file under `.github/workflows/`
+* Authoring a new workflow and wanting it secure by default
+* A workflow that uses `pull_request_target`, `workflow_run`, or `issue_comment` triggers
+* Questions about `GITHUB_TOKEN` permissions or the `permissions:` key
+* Pinning actions to commit SHAs vs tags vs branches
+* Handling untrusted input (issue titles, PR bodies, branch names, commit messages) in `run:` steps
+* OIDC / cloud authentication from Actions, or secret handling in CI
+* Self-hosted runners on public repositories
+* Any request like "is this workflow safe?", "secure my CI", or "review this GitHub Action"
+
+## The Core Insight
+
+In a workflow, **`${{ }}` is expanded by the runner into the script *before* the shell
+executes it.** So a step like:
+
+```yaml
+- run: echo "Title: ${{ github.event.issue.title }}"
+```
+
+is not passing a variable — it is *pasting attacker-controlled text directly into your shell
+command*. An issue titled `"; #` is concatenated into the script and executed.
+This single mechanism is the most common real-world Actions vulnerability, and models routinely
+generate it. Treat every
+`${{ }}` that contains data an outside contributor can influence as a code-injection sink.
+
+## Execution Workflow
+
+Follow these steps **in order** for every workflow reviewed.
+
+### Step 1 — Map the Triggers and Trust Level
+
+Read every `on:` trigger and classify the workflow's privilege:
+
+* `push`, `pull_request` (from same repo) → runs with the contributor's own trust
+* `pull_request` from a **fork** → runs with a **read-only** token, **no secrets** (safe by design)
+* `pull_request_target`, `workflow_run`, `issue_comment`, `issues` → run in the context of the
+ **base repository** with a **read/write token and full access to secrets**, but can be
+ **triggered by outside contributors**. These are the dangerous triggers.
+
+Read `references/triggers-and-privilege.md` for the full trust matrix.
+
+### Step 2 — Hunt for Script Injection
+
+For every `run:` block, every `script:` in `actions/github-script`, and every input to a custom
+action, list the `${{ }}` expressions and check whether any resolve to attacker-controllable data.
+High-risk contexts include:
+
+* `github.event.issue.title`, `github.event.issue.body`
+* `github.event.pull_request.title`, `github.event.pull_request.body`, `.head.ref`, `.head.label`
+* `github.event.comment.body`, `github.event.review.body`
+* `github.event.pages.*.page_name`, `github.event.commits.*.message`, `github.event.head_commit.*`
+* `github.head_ref` and any `github.event.*` field a fork author can set
+
+Read `references/injection.md` for the complete sink list and the safe-pattern fixes.
+
+### Step 3 — Check Privileged Triggers Don't Execute Untrusted Code
+
+If a `pull_request_target` or `workflow_run` workflow checks out PR/fork code
+(`ref: ${{ github.event.pull_request.head.sha }}`) **and then runs it** (build, test, install
+scripts, `npm install` with lifecycle scripts, etc.), that is remote code execution against a
+privileged token. Flag it as CRITICAL. The safe pattern is to split into two workflows: an
+unprivileged `pull_request` workflow that runs the untrusted code, and a privileged
+`workflow_run` workflow that only consumes its results.
+
+### Step 4 — Audit `permissions:`
+
+* If there is **no** `permissions:` block, the workflow inherits the repository default, which may
+ be read/write to everything. Flag it.
+* Recommend a top-level `permissions: {}` (deny-all) or `contents: read`, then grant the minimum
+ per job (e.g. `pull-requests: write` only on the job that comments).
+* Flag any `permissions: write-all` or broad `write` scopes that the steps don't actually need.
+
+Read `references/permissions-and-tokens.md` for the per-scope guidance and OIDC setup.
+
+### Step 5 — Audit Action References (Supply Chain)
+
+For every `uses:`:
+
+* **Third-party actions** (not `actions/*` or `github/*`) MUST be pinned to a full 40-character
+ commit SHA, not a tag or branch. Tags and branches are mutable; a compromised upstream action
+ can rewrite `v1` to malicious code that runs with your token and secrets.
+* First-party `actions/*` are lower risk but SHA-pinning is still the hardened recommendation.
+* Flag `@main`, `@master`, or any branch reference as HIGH — that is "latest" and can change under
+ you at any time.
+* Note the human-readable version in a trailing comment: `uses: foo/bar@ # v2.1.0`.
+
+Read `references/supply-chain.md` for pinning, Dependabot for actions, and artifact/cache risks.
+
+### Step 6 — Check Secret and Output Handling
+
+* No secrets echoed, printed, or written to logs; no `set -x` / `bash -x` in steps that touch
+ secrets.
+* Secrets must not be passed to steps that run untrusted code or to untrusted third-party actions.
+* Untrusted multiline data written to `$GITHUB_ENV` or `$GITHUB_OUTPUT` can inject environment
+ variables or step outputs — use the random-delimiter heredoc form and never write raw user input.
+* `actions/checkout` leaves a token on disk by default; set `persist-credentials: false` when the
+ job later runs untrusted code.
+
+### Step 7 — Produce the Report
+
+Output findings using the format in `references/report-format.md`: a severity summary table first,
+then grouped findings with file, the exact offending YAML, the risk in plain English, and a
+concrete before/after fix. Never auto-apply changes — present them for review.
+
+## Severity Guide
+
+| Severity | Meaning | Example |
+| --- | --- | --- |
+| 🔴 CRITICAL | Token/secret theft or RCE reachable by an outside contributor | `pull_request_target` checking out and running fork code; `${{ github.event.* }}` in a `run:` on a privileged trigger |
+| 🟠 HIGH | Exploitable supply-chain or scope problem | Third-party action on a mutable tag/branch; `write-all` permissions; injection sink on `issue_comment` |
+| 🟡 MEDIUM | Risk under conditions or chaining | Missing `permissions:` block; secret reachable by a non-fork PR author |
+| 🔵 LOW | Hardening gap, low direct risk | First-party action not SHA-pinned; `persist-credentials` left default on a non-privileged job |
+| ⚪ INFO | Observation, not a vulnerability | Version comment missing next to a pinned SHA |
+
+## Output Rules
+
+* **Always** show a findings summary table (counts by severity) first.
+* **Group by issue type**, not by file.
+* **Be exact** — quote the offending line and give the line location.
+* **Always** pair every CRITICAL/HIGH with a concrete corrected YAML snippet.
+* **Never** claim a fork `pull_request` is dangerous just because it runs untrusted code — it has
+ no secrets and a read-only token. Reserve CRITICAL for the privileged triggers.
+* If the workflow is already hardened, say so and list what was checked.
+
+## Reference Files
+
+Load these as needed:
+
+* `references/triggers-and-privilege.md` — Trust matrix for every trigger, why `pull_request_target`
+ and `workflow_run` are privileged, and the two-workflow safe pattern.
+ + Search patterns: `pull_request_target`, `workflow_run`, `issue_comment`, `fork`, `secrets`, `read-only token`, `trust boundary`
+* `references/injection.md` — Full list of attacker-controllable `${{ }}` contexts and the
+ `env:`-variable safe pattern for each sink (`run`, `github-script`, action inputs).
+ + Search patterns: `script injection`, `github.event`, `head_ref`, `issue title`, `env`, `intermediate variable`, `actions/github-script`
+* `references/permissions-and-tokens.md` — `GITHUB_TOKEN` scopes, least-privilege `permissions:`
+ recipes per job type, and OIDC for cloud auth instead of long-lived secrets.
+ + Search patterns: `permissions`, `GITHUB_TOKEN`, `write-all`, `contents: read`, `id-token`, `OIDC`, `least privilege`
+* `references/supply-chain.md` — SHA-pinning third-party actions, Dependabot for `github-actions`,
+ artifact and cache poisoning across `workflow_run`, and self-hosted runner exposure.
+ + Search patterns: `SHA pin`, `uses`, `mutable tag`, `Dependabot`, `download-artifact`, `cache`, `self-hosted runner`
+* `references/report-format.md` — Output template: summary table, finding cards, and before/after
+ remediation blocks.
+ + Search patterns: `report`, `format`, `finding`, `summary`, `remediation`, `before`, `after`
diff --git a/skills/github-actions-hardening/references/injection.md b/skills/github-actions-hardening/references/injection.md
new file mode 100644
index 00000000..113ff1aa
--- /dev/null
+++ b/skills/github-actions-hardening/references/injection.md
@@ -0,0 +1,86 @@
+# Script Injection
+
+`${{ }}` is substituted into the script **as text, before the shell runs**. Any expression
+that resolves to data an outside contributor controls is therefore a command-injection sink.
+
+## Attacker-Controllable Contexts
+
+These can be set by anyone who can open an issue, PR, or comment:
+
+| Context | Set by |
+| --- | --- |
+| `github.event.issue.title` / `.body` | Issue author |
+| `github.event.pull_request.title` / `.body` | PR author |
+| `github.event.pull_request.head.ref` / `.head.label` | PR author (branch name) |
+| `github.head_ref` | PR author (branch name) |
+| `github.event.comment.body` | Commenter |
+| `github.event.review.body` / `.review_comment.body` | Reviewer |
+| `github.event.commits.*.message` / `head_commit.message` | Commit author |
+| `github.event.commits.*.author.email` / `.name` | Commit author |
+| `github.event.pages.*.page_name` | Wiki editor |
+
+A branch named `$()` or an issue titled `"; #` becomes shell
+when interpolated into a `run:` step.
+
+## The Vulnerable Pattern
+
+```yaml
+# VULNERABLE
+- run: |
+ echo "Reviewing PR: ${{ github.event.pull_request.title }}"
+ git checkout ${{ github.head_ref }}
+```
+
+## The Safe Pattern — Pass Through `env:`
+
+Bind the untrusted value to an environment variable, then reference the *shell* variable (quoted).
+The shell variable is data, never re-parsed as workflow syntax:
+
+```yaml
+# SAFE
+- env:
+ PR_TITLE: ${{ github.event.pull_request.title }}
+ HEAD_REF: ${{ github.head_ref }}
+ run: |
+ echo "Reviewing PR: $PR_TITLE"
+ git checkout "$HEAD_REF"
+```
+
+`${{ }}` now appears only on the `env:` side, where it is assigned as a value rather than spliced
+into a command. Always quote the shell variable (`"$PR_TITLE"`) to prevent word-splitting and
+globbing.
+
+## `actions/github-script`
+
+The same rule applies. Do not interpolate `${{ }}` into the `script:` body — pass it through the
+environment and read `process.env`:
+
+```yaml
+# VULNERABLE
+- uses: actions/github-script@
+ with:
+ script: console.log("${{ github.event.issue.title }}")
+
+# SAFE
+- uses: actions/github-script@
+ env:
+ TITLE: ${{ github.event.issue.title }}
+ with:
+ script: console.log(process.env.TITLE)
+```
+
+## Custom Action Inputs
+
+Passing untrusted `${{ }}` into a composite or JS action's `with:` inputs can be safe or not
+depending on whether the action itself interpolates the input into a shell. When in doubt, pass via
+`env:` and have the action read the environment, or sanitize/validate first (e.g. a branch name
+should match `^[A-Za-z0-9._/-]+$`).
+
+## Quick Audit Checklist
+
+1. Grep every `run:` and `script:` for `${{`.
+2. For each, resolve what the expression points to.
+3. If it can be set by a non-collaborator → rewrite via `env:` with a quoted shell variable.
+4. `github.actor`, `github.repository`, `github.sha`, `github.ref` (for branch protection contexts)
+ and similar server-controlled values are not attacker-set, but a defense-in-depth `env:` rewrite
+ costs nothing.
diff --git a/skills/github-actions-hardening/references/permissions-and-tokens.md b/skills/github-actions-hardening/references/permissions-and-tokens.md
new file mode 100644
index 00000000..784ea92e
--- /dev/null
+++ b/skills/github-actions-hardening/references/permissions-and-tokens.md
@@ -0,0 +1,76 @@
+# Permissions and Tokens
+
+Every workflow run gets an automatic `GITHUB_TOKEN`. Its scope is the blast radius if a step is
+compromised, so scope it to the minimum.
+
+## The Default Is Too Broad
+
+If a workflow has no `permissions:` block, it inherits the repository/organization default. On
+older or permissive repos that default is **read/write to most scopes**. A single injected command
+or malicious dependency then runs with the ability to push code, publish releases, or approve PRs.
+
+## Least-Privilege Recipe
+
+Set a restrictive default at the top level, then elevate per job only where needed.
+
+```yaml
+# Deny by default
+permissions: {}
+
+jobs:
+ build:
+ permissions:
+ contents: read # checkout only
+ runs-on: ubuntu-latest
+ steps: [...]
+
+ comment:
+ permissions:
+ contents: read
+ pull-requests: write # this job posts a comment; nothing else
+ runs-on: ubuntu-latest
+ steps: [...]
+```
+
+Common scopes: `contents`, `pull-requests`, `issues`, `actions`, `packages`, `id-token`,
+`deployments`, `checks`, `statuses`. Each is `read`, `write`, or `none`.
+
+## Findings to Flag
+
+* No `permissions:` block anywhere → MEDIUM (inherits possibly-broad default).
+* `permissions: write-all` → HIGH.
+* A `write` scope the job's steps never use → HIGH (drop it).
+* Top-level `write` that should live on one job → MEDIUM (move it down).
+
+## OIDC Instead of Long-Lived Cloud Secrets
+
+Storing static cloud keys (`AWS_ACCESS_KEY_ID`, etc.) as repo secrets means a leak is permanent
+until manually rotated. Prefer OpenID Connect: the workflow requests a short-lived token the cloud
+provider trusts, scoped to that repo/branch, expiring in minutes.
+
+```yaml
+permissions:
+ id-token: write # required to request the OIDC token
+ contents: read
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: aws-actions/configure-aws-credentials@
+ with:
+ role-to-assume: arn:aws:iam::123456789012:role/my-ci-role
+ aws-region: us-east-1
+ # no AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY secrets needed
+```
+
+The same pattern exists for Azure (`azure/login`), GCP (`google-github-actions/auth`), HashiCorp
+Vault, and others. On the cloud side, scope the trust policy to the specific repo and ideally a
+specific branch/environment so a fork or another repo cannot assume the role.
+
+## Secret Hygiene
+
+* Reference secrets only in the jobs that need them.
+* Never `echo` a secret or enable shell tracing (`set -x`) in a step that handles one.
+* Don't pass secrets into third-party actions you haven't pinned and reviewed.
+* Remember fork `pull_request` runs get no secrets — don't try to "fix" that by switching to
+ `pull_request_target` (see `triggers-and-privilege.md`).
diff --git a/skills/github-actions-hardening/references/report-format.md b/skills/github-actions-hardening/references/report-format.md
new file mode 100644
index 00000000..20f9f064
--- /dev/null
+++ b/skills/github-actions-hardening/references/report-format.md
@@ -0,0 +1,65 @@
+# Report Format
+
+Use this structure for every workflow hardening review.
+
+## 1. Summary Table (always first)
+
+```
+GitHub Actions Hardening —
+
+| Severity | Count |
+| ---------- | ----- |
+| 🔴 CRITICAL | 1 |
+| 🟠 HIGH | 2 |
+| 🟡 MEDIUM | 1 |
+| 🔵 LOW | 1 |
+| ⚪ INFO | 0 |
+```
+
+If nothing was found: `No issues found. Checked: triggers, injection sinks, permissions, action
+pinning, secret handling.`
+
+## 2. Findings (grouped by issue type, not by file)
+
+For each finding use a card:
+
+```
+### 🔴 CRITICAL — Script injection via PR title on a privileged trigger
+
+File: .github/workflows/triage.yml (line 14)
+Trigger: pull_request_target
+
+Offending code:
+ - run: echo "New PR: ${{ github.event.pull_request.title }}"
+
+Risk: pull_request_target runs with a read/write token and repository secrets, and any
+contributor can open a PR with a title like "; # which is executed as shell.
+This allows secret exfiltration and pushes with the workflow token.
+
+Fix:
+ - env:
+ PR_TITLE: ${{ github.event.pull_request.title }}
+ run: echo "New PR: $PR_TITLE"
+
+Confidence: High
+```
+
+## 3. Remediation Blocks
+
+Every CRITICAL and HIGH finding includes a concrete before/after. Preserve the author's
+indentation, step names, and surrounding structure — change only what fixes the issue, and add a
+one-line comment explaining the change where it isn't obvious.
+
+## 4. Closing Note
+
+End with the explicit line:
+
+> Review each change before committing. Nothing has been modified.
+
+## Style Rules
+
+* Quote the exact offending line and give its location.
+* Explain risk in plain English — what an attacker actually does, not just the rule name.
+* Per-finding confidence: High / Medium / Low.
+* Don't inflate severity: a fork `pull_request` (read-only token, no secrets) running untrusted
+ code is not CRITICAL on its own.
diff --git a/skills/github-actions-hardening/references/supply-chain.md b/skills/github-actions-hardening/references/supply-chain.md
new file mode 100644
index 00000000..6cb05474
--- /dev/null
+++ b/skills/github-actions-hardening/references/supply-chain.md
@@ -0,0 +1,71 @@
+# Supply Chain
+
+A workflow runs other people's code every time it `uses:` an action. Those actions execute with
+your token and (on privileged triggers) your secrets, so their integrity is your integrity.
+
+## Pin Third-Party Actions to a Commit SHA
+
+Tags (`@v4`) and branches (`@main`) are **mutable** — the upstream owner (or anyone who compromises
+them) can repoint them to new code without you changing a line. A full 40-character commit SHA is
+immutable.
+
+```yaml
+# Mutable — the tag can be moved to malicious code
+- uses: some-org/some-action@v3
+
+# Pinned — this exact tree, forever
+- uses: some-org/some-action@3f1e0a9c8b7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f # v3.2.1
+```
+
+Rules:
+
+* Third-party actions (anything not `actions/*` or `github/*`) → **MUST** be SHA-pinned. Flag tags
+ and branches as HIGH.
+* `@main` / `@master` → HIGH regardless of publisher; that is unversioned "latest".
+* First-party `actions/*` → SHA-pinning is the hardened recommendation (LOW if only tag-pinned).
+* Keep a trailing `# vX.Y.Z` comment so humans and Dependabot can read the intended version.
+
+This is not theoretical: real incidents have seen popular actions' tags repointed to code that
+exfiltrated secrets from every workflow that referenced the mutable tag.
+
+## Let Dependabot Update the Pins
+
+SHA pins go stale. Enable Dependabot for the `github-actions` ecosystem so updates arrive as
+reviewable PRs (it understands the `# vX.Y.Z` comment and bumps the SHA):
+
+```yaml
+# .github/dependabot.yml
+version: 2
+updates:
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: weekly
+```
+
+## Artifact and Cache Poisoning
+
+* An artifact uploaded by an untrusted `pull_request` build is **untrusted data**. A privileged
+ `workflow_run` may download it, but must treat it as data only — never execute it, and validate
+ paths when extracting (a crafted artifact can contain `../` path-traversal entries).
+* Caches are keyed and can be populated by less-privileged runs; do not trust cached build outputs
+ to be untampered in a privileged context.
+
+## Self-Hosted Runners on Public Repos
+
+Default (GitHub-hosted) runners are ephemeral — a fresh VM per job, destroyed after. **Self-hosted
+runners persist**, so untrusted fork PR code running on one can:
+
+* Leave behind tools/backdoors for the next job,
+* Read other repositories' checkouts or credentials on the same machine,
+* Pivot into your network.
+
+Never use self-hosted runners for workflows that public forks can trigger. If you must, use
+ephemeral, isolated, single-use runners and never expose secrets to fork-triggered jobs.
+
+## `checkout` Credential Persistence
+
+`actions/checkout` writes the token into `.git/config` by default so later `git` steps can push.
+If the job subsequently runs untrusted code, that code can read the token. Set
+`persist-credentials: false` when you don't need to push, especially before running build/test of
+untrusted code.
diff --git a/skills/github-actions-hardening/references/triggers-and-privilege.md b/skills/github-actions-hardening/references/triggers-and-privilege.md
new file mode 100644
index 00000000..3081d914
--- /dev/null
+++ b/skills/github-actions-hardening/references/triggers-and-privilege.md
@@ -0,0 +1,89 @@
+# Triggers and Privilege
+
+The single most important question for workflow security is: **can an outside contributor trigger
+this workflow, and if so, what token and secrets does it get?** GitHub answers this differently per
+trigger.
+
+## Trust Matrix
+
+| Trigger | Who can fire it | `GITHUB_TOKEN` | Secrets available | Risk |
+| --- | --- | --- | --- | --- |
+| `push` | Repo collaborators | read/write | yes | Low — trusted authors |
+| `pull_request` (same-repo branch) | Collaborators | read/write | yes | Low |
+| `pull_request` (from a fork) | **Anyone** | **read-only** | **no** | Low by design — even malicious code can't steal anything |
+| `pull_request_target` | **Anyone with a fork** | **read/write** | **yes** | **High** — runs in base-repo context |
+| `workflow_run` | Fires after another workflow | **read/write** | **yes** | **High** |
+| `issue_comment`, `issues` | **Anyone** | **read/write** | **yes** | **High** |
+
+The trap: `pull_request` from a fork is *safe* because GitHub deliberately strips the token down
+and withholds secrets. Maintainers who find that "the secrets don't work on fork PRs" often switch
+to `pull_request_target` to get them back — and in doing so hand a write token and every secret to
+arbitrary contributors.
+
+## Why `pull_request_target` Is Dangerous
+
+`pull_request_target` checks out the **base** repository's workflow definition (so a fork can't
+change what runs), but it runs with full privileges. The danger is when the workflow then
+explicitly checks out the **fork's** code and executes it:
+
+```yaml
+# DANGEROUS — RCE with a write token + secrets
+on: pull_request_target
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@
+ with:
+ ref: ${{ github.event.pull_request.head.sha }} # fork's code
+ - run: npm install && npm test # runs the fork's code + scripts
+```
+
+`npm install` alone runs arbitrary lifecycle scripts from the PR. With `pull_request_target` those
+scripts can read `secrets.*` and push commits with the write token.
+
+## The Safe Two-Workflow Pattern
+
+Split responsibilities. An **unprivileged** workflow runs the untrusted code; a **privileged**
+workflow consumes only the trusted *output*.
+
+```yaml
+# 1) Unprivileged: runs untrusted code, no secrets, read-only token
+name: PR Build
+on: pull_request
+permissions:
+ contents: read
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@
+ - run: npm ci && npm run build
+ - uses: actions/upload-artifact@
+ with: { name: pr, path: dist/ }
+```
+
+```yaml
+# 2) Privileged: triggered by the first, never runs fork code
+name: PR Comment
+on:
+ workflow_run:
+ workflows: ["PR Build"]
+ types: [completed]
+permissions:
+ pull-requests: write
+jobs:
+ comment:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/download-artifact@ # data only, not executed
+ # post results, using the trusted token — but never execute the artifact
+```
+
+## Rules
+
+* Treat `pull_request_target`, `workflow_run`, `issue_comment`, and `issues` as privileged.
+* In a privileged workflow, **never** check out and execute PR/fork code.
+* If you only need to label, comment, or triage based on metadata, that is fine — just don't run
+ the contributor's code.
+* Prefer `pull_request` (with its safe read-only/no-secrets defaults) whenever possible.