Add harden-github-actions skill#14
Conversation
Add decision guidance for superfluous-actions (always suppress), dependabot-cooldown (add to all ecosystems), and reusable workflow permissions. Extract permission mappings to a separate reference file. Combine actionlint and zizmor into a single lint-actions CI job template.
…ions skill
Each suppressible rule now has concrete criteria that must ALL be met
before suppressing. If criteria can't be confirmed and fixing would break
the workflow, agents must stop and report rather than guess.
Also adds dangerous-triggers and secrets-outside-env rules with accurate
descriptions from zizmor docs, and adds permissions: {} guidance for
excessive-permissions fixes.
…isclosure Each zizmor rule now lives in its own file under references/. SKILL.md contains a rule index table with one-liners; agents only load the rules they encounter. Also simplifies cache-poisoning guidance: suppress by default since GitHub cache isolation makes the attack impractical, only escalate if custom cache keys cross branch boundaries.
Show two example comments for different scenarios (testing deps vs branch-isolated release build) so agents write case-appropriate reasons instead of copying a template.
Prevents duplicate actionlint runs when the workflow already has a standalone actionlint job.
Agents must use docker manifest inspect to get the correct SHA256 digest. Never guess — stop and report if the command fails.
Determining the correct digest for container images is nontrivial. Version tags already pin to a specific release.
Without a token, ref-version-mismatch and impostor-commit are silently skipped, causing findings to only surface in CI.
Based on Jeremy's review feedback. The dual check prevents human-triggered events from re-entering the approve/merge path. Auto-fix should be reverted and the dual check applied manually.
- Scorecard exception: don't change permissions: read-all on scorecard workflows - Semver-granular dependabot cooldowns for package ecosystems (major: 7, minor: 3, patch: 2) - github-actions cooldown uses default-days: 7 (semver keys not supported) - Makefile lint-actions target reference (basecamp-sdk example) - PR comments need pull-requests: write, labels need issues: write
There was a problem hiding this comment.
Pull request overview
Adds a new harden-github-actions skill intended to guide hardening GitHub Actions workflows using pinning, zizmor, job-scoped permissions, actionlint, and Dependabot configuration.
Changes:
- Introduces a new skill document with an end-to-end hardening workflow and CI/job templates.
- Adds per-audit “rule” reference guides (fix vs suppress checklists) plus a permission-mappings reference.
- Documents local linting setup guidance and Dependabot batching/cooldown recommendations.
Tip
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| skills/harden-github-actions/SKILL.md | Core skill workflow, decision guide, CI template, local linting + Dependabot guidance |
| skills/harden-github-actions/references/rule-artipacked.md | Guidance for actions/checkout credential persistence findings |
| skills/harden-github-actions/references/rule-bot-conditions.md | Guidance for spoofable bot checks and the dual-condition fix |
| skills/harden-github-actions/references/rule-cache-poisoning.md | Suppression criteria and escalation conditions for cache-poisoning findings |
| skills/harden-github-actions/references/rule-dangerous-triggers.md | Fix/suppress checklist for pull_request_target / workflow_run triggers |
| skills/harden-github-actions/references/rule-dependabot-cooldown.md | Dependabot cooldown rule guidance and example configuration |
| skills/harden-github-actions/references/rule-dependabot-execution.md | Guidance for insecure-external-code-execution allowlist findings |
| skills/harden-github-actions/references/rule-excessive-permissions.md | Workflow/job permission refactor guidance + permission research process |
| skills/harden-github-actions/references/rule-secrets-outside-env.md | Guidance for environment-scoped secrets vs suppression criteria |
| skills/harden-github-actions/references/rule-superfluous-actions.md | Guidance to suppress “replace action with inline code” suggestions |
| skills/harden-github-actions/references/rule-template-injection.md | Guidance for eliminating ${{ }} in run: blocks via env: |
| skills/harden-github-actions/references/rule-unpinned-images.md | Guidance for digest pinning vs default suppression rationale |
| skills/harden-github-actions/references/permission-mappings.md | Action-to-permission mapping table used to resolve excessive-permissions |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| --- | ||
| name: zizmor-resolution | ||
| description: > | ||
| Use when resolving zizmor warnings in GitHub Actions workflows, hardening CI | ||
| pipelines, or pinning actions to SHA hashes. Covers artipacked, template-injection, | ||
| excessive-permissions, secrets-outside-env, dependabot-execution, and when to | ||
| suppress vs fix. | ||
| --- | ||
|
|
||
| # Resolving Zizmor Warnings in GitHub Actions |
There was a problem hiding this comment.
The frontmatter name doesn't match the skill directory/PR intent (skills/harden-github-actions/ vs name: zizmor-resolution). This will make the skill hard to discover/invoke consistently; rename the skill name (and ideally the H1) to harden-github-actions to match the folder and README entry format used by other skills.
| Use when resolving zizmor warnings in GitHub Actions workflows, hardening CI | ||
| pipelines, or pinning actions to SHA hashes. Covers artipacked, template-injection, | ||
| excessive-permissions, secrets-outside-env, dependabot-execution, and when to | ||
| suppress vs fix. |
There was a problem hiding this comment.
This skill frontmatter is missing a triggers: list. Other skills in this repo consistently include triggers: (see e.g. skills/install-md/SKILL.md), and the skill-crafting guide documents it as part of the required frontmatter. Add appropriate trigger phrases so the skill can be activated reliably.
| suppress vs fix. | |
| suppress vs fix. | |
| triggers: | |
| - zizmor | |
| - zizmor warnings | |
| - harden github actions | |
| - github actions security | |
| - github actions workflow hardening | |
| - pin github actions to sha | |
| - pin actions to sha |
| # Resolving Zizmor Warnings in GitHub Actions | ||
|
|
||
| ## Overview | ||
|
|
||
| zizmor identifies security vulnerabilities in GitHub Actions workflows. This skill documents | ||
| the decision guidelines for resolving each warning type: when to fix, how to fix, and when | ||
| to suppress with an inline comment explaining why. | ||
|
|
||
| **Core principle:** Fix the vulnerability whenever possible. Suppress only when the fix would | ||
| break required functionality, and always include a reason in the suppression comment. | ||
|
|
There was a problem hiding this comment.
Per AGENTS.md's contributing checklist, adding a new skill also requires adding it to the README skills table. This PR introduces skills/harden-github-actions/ but README.md doesn't list it yet; please add an entry so it's discoverable.
| | Rule | File | Action | | ||
| |------|------|--------| | ||
| | `artipacked` | `references/rule-artipacked.md` | Fix (add `persist-credentials: false`); suppress only if job does `git push` | | ||
| | `template-injection` | `references/rule-template-injection.md` | Always fix (move expressions to `env:` vars) | | ||
| | `excessive-permissions` | `references/rule-excessive-permissions.md` | Always fix (set `permissions: {}` at workflow level, scope per job) | | ||
| | `dangerous-triggers` | `references/rule-dangerous-triggers.md` | Fix or suppress with 5-point checklist | | ||
| | `secrets-outside-env` | `references/rule-secrets-outside-env.md` | Fix (add `environment:`) or suppress with 3-point checklist | | ||
| | `bot-conditions` | `references/rule-bot-conditions.md` | Always fix (dual check: `actor` + `user.login`); revert auto-fix | | ||
| | `superfluous-actions` | `references/rule-superfluous-actions.md` | Always suppress (never replace with inline code) | | ||
| | `cache-poisoning` | `references/rule-cache-poisoning.md` | Suppress (default); revert auto-fixes; only escalate if custom cache keys | | ||
| | `unpinned-images` | `references/rule-unpinned-images.md` | Suppress (default); digest pinning is nontrivial | | ||
| | `dependabot-execution` | `references/rule-dependabot-execution.md` | Fix or suppress with 3-point checklist | | ||
| | `dependabot-cooldown` | `references/rule-dependabot-cooldown.md` | Always fix (add `cooldown: default-days: 10` to all ecosystems) | | ||
|
|
There was a problem hiding this comment.
The decision-guide Markdown table starts each row with ||, which renders as an extra empty column in GitHub Markdown. Change the table rows to start with a single | so the table displays correctly.
| ## Common Mistakes | ||
|
|
||
| | Mistake | Correction | | ||
| |---------|------------| | ||
| | Guessing what permissions an action needs | **Read the action's README.** If it's not in the permission mappings table, research it before proceeding. | | ||
| | Accepting `cache-poisoning` auto-fixes without review | `--fix=all` disables caching; almost always revert and suppress instead | | ||
| | Suppressing without a reason | Always explain WHY the fix can't be applied | | ||
| | Suppressing `template-injection` | This should always be fixed, never suppressed | | ||
| | Adding `persist-credentials: false` to a workflow that does `git push` | Suppress `artipacked` with a comment instead | | ||
| | Fixing permissions by removing the block entirely | Move to job-level, don't remove — implicit permissions may be too broad | | ||
| | Using `--fix` instead of `--fix=all` | Safe mode silently holds back fixes; use `--fix=all` and review the diff | | ||
| | Committing without verifying clean zizmor output | Always re-run `zizmor --min-severity=<level> .` before committing | | ||
| | Analyzing all findings up front before starting work | Follow the workflow order step by step — pin, then fix by severity, then CI job, then dependabot | | ||
| | Adding the zizmor CI job at the end of the workflow file | Place it near existing lint jobs — it's a linting concern, not a test | | ||
| | Replacing an action with inline code for `superfluous-actions` | Always suppress — actions are more maintainable and receive upstream fixes | | ||
| | Not specifying permissions on reusable workflow caller jobs | Caller jobs must declare permissions; reusable workflows inherit from the caller | | ||
| | Adding tools to bin/setup when there's no bin/ci | Only add local linting if a local CI script exists to run the tools | |
There was a problem hiding this comment.
The "Common Mistakes" Markdown table also starts rows with ||, which will render an unintended blank first column. Use a single leading | for standard Markdown table formatting.
| ```yaml | ||
| lint-actions: | ||
| name: GitHub Actions audit | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: Run actionlint | ||
| uses: rhysd/actionlint@v1.7.11 | ||
|
|
||
| - name: Run zizmor | ||
| uses: zizmorcore/zizmor-action@v0.5.2 | ||
| with: | ||
| advanced-security: false | ||
| ``` |
There was a problem hiding this comment.
The standard CI job template doesn't declare job-level permissions:. Earlier the skill instructs setting workflow-level permissions: {}; if users follow that, this job will fail (at minimum actions/checkout typically needs contents: read). Add an explicit minimal permissions: block to the lint-actions job in the template.
| | `cache-poisoning` | `references/rule-cache-poisoning.md` | Suppress (default); revert auto-fixes; only escalate if custom cache keys | | ||
| | `unpinned-images` | `references/rule-unpinned-images.md` | Suppress (default); digest pinning is nontrivial | | ||
| | `dependabot-execution` | `references/rule-dependabot-execution.md` | Fix or suppress with 3-point checklist | | ||
| | `dependabot-cooldown` | `references/rule-dependabot-cooldown.md` | Always fix (add `cooldown: default-days: 10` to all ecosystems) | |
There was a problem hiding this comment.
Cooldown guidance is inconsistent: the rule table says to add cooldown: default-days: 10 for dependabot-cooldown, but later examples (and references/rule-dependabot-cooldown.md) use default-days: 7. Align these values so readers don't apply conflicting configurations.
| | `dependabot-cooldown` | `references/rule-dependabot-cooldown.md` | Always fix (add `cooldown: default-days: 10` to all ecosystems) | | |
| | `dependabot-cooldown` | `references/rule-dependabot-cooldown.md` | Always fix (add `cooldown: default-days: 7` to all ecosystems) | |
| | Action | Permissions | Notes | | ||
| |--------|------------|-------| | ||
| | `actions/ai-inference` | `models: read` | + `contents: read` recommended; PAT required for MCP server feature | | ||
| | `actions/attest-build-provenance` | `id-token: write`, `attestations: write`, `contents: read` | + `packages: write` when `push-to-registry: true` for container images | | ||
| | `actions/cache` | none | Uses Actions cache service via implicit runner credentials, not GITHUB_TOKEN | | ||
| | `actions/checkout` | `contents: read` | | |
There was a problem hiding this comment.
This Markdown table uses || at the start of each row/header, which creates an extra empty first column in GitHub Markdown rendering. Switch to a single leading | per row for proper table formatting.
| | `necko-actions/setup-smithy` | none | Installs Smithy CLI and adds to PATH | | ||
| | `ossf/scorecard-action` | `security-events: write`, `id-token: write`, `contents: read` | For public repos with `publish_results: true`; private repos also need `issues: read`, `pull-requests: read`, `checks: read` | | ||
| | `reviewdog/action-rubocop` | `contents: read`, `pull-requests: write` | For `github-pr-review` and `github-pr-check` reporters; `checks: write` may also be needed for `github-check` reporter | | ||
| | `rhysd/actionlint` | none | Not a standard action (no `action.yml`); used by downloading the binary via shell script or Docker. Does not use GITHUB_TOKEN. | |
There was a problem hiding this comment.
The rhysd/actionlint row states it's "Not a standard action (no action.yml)" and is used by downloading a binary, but the skill's CI template uses it via uses: rhysd/actionlint@.... This is internally inconsistent and may confuse permission research; clarify whether it's a GitHub Action and keep the note focused on its permission requirements (e.g., that it doesn't use GITHUB_TOKEN).
| | `rhysd/actionlint` | none | Not a standard action (no `action.yml`); used by downloading the binary via shell script or Docker. Does not use GITHUB_TOKEN. | | |
| | `rhysd/actionlint` | none | Lints workflow files only; does not call GitHub APIs or use `GITHUB_TOKEN`. | |
Summary
New skill for hardening GitHub Actions workflows using zizmor, pinact, and actionlint.
Repos hardened with this skill
Key design decisions