Workflow Audit
Comprehensive audit of .github/workflows/*.yml files against GitHub Actions
best practices, security hardening guidelines, and project conventions.
When to Use
- User asks to audit, review, or check workflows
- Before committing changes to any workflow file
- After a workflow failure that needs root-cause analysis
- Periodic health check (e.g., monthly)
Workflow
- Discover — Glob
.github/workflows/*.yml and list all workflow files.
- Parse — Read each file; validate YAML syntax.
- Audit — Run every check in the checklist below against each file.
- Cross-check — Run cross-workflow consistency checks.
- Report — Output a findings table sorted by severity (critical > high > medium > low).
- Fix offer — For each finding, suggest a concrete fix (diff or instruction).
Audit Checklist
1. YAML Validity
- File parses as valid YAML.
- No duplicate keys at the same level.
- No tabs (GitHub Actions requires spaces).
2. Action Version Currency
Check every uses: line.
| Pattern |
Severity |
Rule |
actions/checkout@v4 or lower |
critical |
Upgrade to @v6. Node.js 20 actions break June 2, 2026 (forced to Node 24). |
actions/setup-node@v4 or lower |
critical |
Same — upgrade to @v6. |
actions/cache@v3 or lower |
high |
Upgrade to @v4. |
pnpm/action-setup@v3 or lower |
high |
Upgrade to @v4. |
softprops/action-gh-release@v1 |
medium |
Upgrade to @v2. |
actions/upload-pages-artifact@v2 or lower |
medium |
Upgrade to @v3. |
actions/deploy-pages@v3 or lower |
medium |
Upgrade to @v4. |
Any @main or @master pin |
high |
Pin to a tag or SHA — mutable refs are a supply-chain risk. |
Node.js deprecation timeline (reference for findings):
- June 2, 2026: Node 24 becomes default (
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true to opt in early).
- Fall 2026: Node 20 removed entirely from runners.
- Temporary opt-out after June 2:
ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true (stops working fall 2026).
3. Security — Script Injection
For every run: block, check for untrusted context expressions used inline:
# DANGEROUS — attacker-controlled input interpreted by shell
run: echo "${{ github.event.issue.title }}"
# SAFE — passed via environment variable
env:
TITLE: ${{ github.event.issue.title }}
run: echo "$TITLE"
Untrusted contexts (must NEVER appear directly in run: blocks):
github.event.issue.title / .body
github.event.pull_request.title / .body
github.event.comment.body
github.event.review.body / github.event.review_comment.body
github.event.commits.*.message
github.event.head_commit.message / .author.email / .author.name
github.event.pull_request.head.ref / .head.label / .head.repo.default_branch
github.head_ref
github.event.pages.*.page_name
Safe contexts (numeric or system-controlled, OK inline):
github.event.issue.number, github.event.pull_request.number
github.repository, github.run_id, github.sha
github.ref (only on push/tag events, not PR)
secrets.*, env.*, matrix.*
4. Security — Permissions
| Check |
Severity |
Rule |
No permissions: block at all |
high |
Add explicit permissions — defaults give broad access. |
permissions: write-all |
critical |
Never use. Specify individual scopes. |
| Unused permission scopes |
medium |
Remove permissions not needed by any step. |
id-token: write without OIDC usage |
medium |
Only needed for Bedrock/Vertex/Foundry or cloud OIDC. |
pull_request_target trigger |
high |
Grants write access from forks — verify checkout uses PR base, not head. |
5. Security — Auto-merge and Bot Patterns
| Check |
Severity |
Rule |
gh pr merge --auto without author guard |
high |
Restrict to bot PRs: if: github.event.pull_request.user.login == 'claude[bot]' |
allowed_bots: '*' in claude-code-action |
medium |
Prefer explicit bot names over wildcard. |
6. Reliability — Timeouts
| Check |
Severity |
Rule |
Job without timeout-minutes |
high |
Default is 360 min (6 hours). Always set explicit timeouts. |
| Claude Code action jobs |
high |
Must have timeout-minutes (recommended: 15 for review, 30 for fix). |
| Build jobs |
medium |
Recommended: 30-45 min depending on platform. |
7. Reliability — Error Handling
| Check |
Severity |
Rule |
git push to a protected branch |
critical |
Will fail if branch protection requires status checks. Push to unprotected branch or use PR. |
gh pr merge without || true or continue-on-error |
medium |
May fail if PR is not mergeable — handle gracefully. |
Steps after a continue-on-error step that depend on its output |
medium |
Check if downstream steps handle the soft failure. |
Network-dependent steps without retry or continue-on-error |
low |
CDN downloads, API calls can be flaky. |
8. Reliability — Concurrency
| Check |
Severity |
Rule |
Scheduled workflow without concurrency group |
medium |
Overlapping runs waste resources. |
cancel-in-progress: true on deploy workflows |
high |
Can corrupt partial deployments. Use false for deploys. |
Missing concurrency on Claude Code jobs |
medium |
Multiple concurrent AI runs on the same issue/PR waste credits. |
9. Reliability — Branch Protection Awareness
| Check |
Severity |
Rule |
Workflow pushes to main (or default branch) |
critical |
Check if branch protection allows this. Use a data branch or PR workflow. |
Workflow creates commits without checking git diff first |
medium |
May create empty commits or fail on no-changes. |
10. Cross-Workflow Consistency
| Check |
Severity |
Rule |
Different node-version across workflows |
high |
All workflows should use the same Node.js version (currently 22). |
Different pnpm version across workflows |
high |
All workflows should use the same pnpm version (currently 10). |
| Different Rust toolchain specification |
medium |
Should be consistent unless intentionally testing multiple versions. |
| Duplicate triggers (same event in multiple workflows) |
medium |
Can cause double-execution. Verify intentional. |
11. Claude Code Action — Configuration
Reference: anthropics/claude-code-action@v1
| Check |
Severity |
Rule |
Using @beta or @v0 |
critical |
Migrate to @v1. v0.x inputs are deprecated. |
Using deprecated inputs (direct_prompt, model, allowed_tools, max_turns, timeout_minutes) |
high |
Migrate to prompt + claude_args. |
Missing claude_code_oauth_token or anthropic_api_key |
critical |
One auth method is required. |
--model not specified in claude_args |
low |
Defaults to action's default model. Specify for reproducibility. |
--max-turns not specified for fix/implementation jobs |
medium |
Unbounded turns burn credits. Recommend 15-25 for fixes. |
show_full_output: true on review jobs |
low |
Verbose — only needed for debugging. |
Key claude_args flags:
--model <model-id> — e.g., claude-opus-4-6, claude-sonnet-4-6
--max-turns <N> — limit conversation turns
--allowedTools <tool1>,<tool2> — restrict tool access
--disallowedTools <tool1> — block specific tools
--system-prompt "..." — custom system prompt
Authentication options:
anthropic_api_key — direct Anthropic API
claude_code_oauth_token — Claude Code OAuth (subscription-based)
use_bedrock: true + OIDC — Amazon Bedrock
use_vertex: true + OIDC — Google Vertex AI
12. Trigger Hygiene
| Check |
Severity |
Rule |
release: [published] + workflow_dispatch for same logic |
medium |
Choose one trigger path to avoid double-execution. |
push: branches: [main] on workflows that also have pull_request |
low |
Intentional for CI — but verify both are needed. |
| Scheduled workflow that only runs on default branch |
low |
Verify schedule cron syntax with crontab.guru. |
Workflow with no paths filter on push trigger |
low |
Consider adding paths: to avoid unnecessary runs. |
Report Format
Output a markdown table:
## Workflow Audit Report
| # | Severity | File | Check | Finding | Fix |
|---|----------|------|-------|---------|-----|
| 1 | critical | ci.yml | Action versions | `actions/checkout@v4` — Node 20 deprecated | Upgrade to `@v6` |
| 2 | high | claude.yml | Auto-merge | Enabled for all PRs | Add `if: github.event.pull_request.user.login == 'claude[bot]'` |
After the table, add a Summary line:
X critical, Y high, Z medium, W low findings across N workflow files.
Notes
- Do NOT modify workflow files during audit — report only.
- When the user asks to fix findings, apply changes and re-audit to verify.
- For security findings, always explain the attack vector (not just the rule).
- Check
.github/workflows/ only — ignore .github/actions/ unless referenced.
1---2name: workflow-audit3description: Audit GitHub Actions workflows for correctness, security, and unattended reliability. Use when asked to audit workflows, check CI health, review workflow security, or before committing workflow changes.4---56# Workflow Audit78Comprehensive audit of `.github/workflows/*.yml` files against GitHub Actions9best practices, security hardening guidelines, and project conventions.1011## When to Use1213- User asks to audit, review, or check workflows14- Before committing changes to any workflow file15- After a workflow failure that needs root-cause analysis16- Periodic health check (e.g., monthly)1718## Workflow19201. **Discover** — Glob `.github/workflows/*.yml` and list all workflow files.212. **Parse** — Read each file; validate YAML syntax.223. **Audit** — Run every check in the checklist below against each file.234. **Cross-check** — Run cross-workflow consistency checks.245. **Report** — Output a findings table sorted by severity (critical > high > medium > low).256. **Fix offer** — For each finding, suggest a concrete fix (diff or instruction).2627## Audit Checklist2829### 1. YAML Validity30- File parses as valid YAML.31- No duplicate keys at the same level.32- No tabs (GitHub Actions requires spaces).3334### 2. Action Version Currency3536Check every `uses:` line.3738| Pattern | Severity | Rule |39|---------|----------|------|40| `actions/checkout@v4` or lower | **critical** | Upgrade to `@v6`. Node.js 20 actions break June 2, 2026 (forced to Node 24). |41| `actions/setup-node@v4` or lower | **critical** | Same — upgrade to `@v6`. |42| `actions/cache@v3` or lower | **high** | Upgrade to `@v4`. |43| `pnpm/action-setup@v3` or lower | **high** | Upgrade to `@v4`. |44| `softprops/action-gh-release@v1` | **medium** | Upgrade to `@v2`. |45| `actions/upload-pages-artifact@v2` or lower | **medium** | Upgrade to `@v3`. |46| `actions/deploy-pages@v3` or lower | **medium** | Upgrade to `@v4`. |47| Any `@main` or `@master` pin | **high** | Pin to a tag or SHA — mutable refs are a supply-chain risk. |4849**Node.js deprecation timeline** (reference for findings):50- **June 2, 2026**: Node 24 becomes default (`FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true` to opt in early).51- **Fall 2026**: Node 20 removed entirely from runners.52- Temporary opt-out after June 2: `ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true` (stops working fall 2026).5354### 3. Security — Script Injection5556For every `run:` block, check for **untrusted context expressions used inline**:5758```59# DANGEROUS — attacker-controlled input interpreted by shell60run: echo "${{ github.event.issue.title }}"6162# SAFE — passed via environment variable63env:64 TITLE: ${{ github.event.issue.title }}65run: echo "$TITLE"66```6768**Untrusted contexts** (must NEVER appear directly in `run:` blocks):69- `github.event.issue.title` / `.body`70- `github.event.pull_request.title` / `.body`71- `github.event.comment.body`72- `github.event.review.body` / `github.event.review_comment.body`73- `github.event.commits.*.message`74- `github.event.head_commit.message` / `.author.email` / `.author.name`75- `github.event.pull_request.head.ref` / `.head.label` / `.head.repo.default_branch`76- `github.head_ref`77- `github.event.pages.*.page_name`7879**Safe contexts** (numeric or system-controlled, OK inline):80- `github.event.issue.number`, `github.event.pull_request.number`81- `github.repository`, `github.run_id`, `github.sha`82- `github.ref` (only on push/tag events, not PR)83- `secrets.*`, `env.*`, `matrix.*`8485### 4. Security — Permissions8687| Check | Severity | Rule |88|-------|----------|------|89| No `permissions:` block at all | **high** | Add explicit permissions — defaults give broad access. |90| `permissions: write-all` | **critical** | Never use. Specify individual scopes. |91| Unused permission scopes | **medium** | Remove permissions not needed by any step. |92| `id-token: write` without OIDC usage | **medium** | Only needed for Bedrock/Vertex/Foundry or cloud OIDC. |93| `pull_request_target` trigger | **high** | Grants write access from forks — verify checkout uses PR base, not head. |9495### 5. Security — Auto-merge and Bot Patterns9697| Check | Severity | Rule |98|-------|----------|------|99| `gh pr merge --auto` without author guard | **high** | Restrict to bot PRs: `if: github.event.pull_request.user.login == 'claude[bot]'` |100| `allowed_bots: '*'` in claude-code-action | **medium** | Prefer explicit bot names over wildcard. |101102### 6. Reliability — Timeouts103104| Check | Severity | Rule |105|-------|----------|------|106| Job without `timeout-minutes` | **high** | Default is 360 min (6 hours). Always set explicit timeouts. |107| Claude Code action jobs | **high** | Must have `timeout-minutes` (recommended: 15 for review, 30 for fix). |108| Build jobs | **medium** | Recommended: 30-45 min depending on platform. |109110### 7. Reliability — Error Handling111112| Check | Severity | Rule |113|-------|----------|------|114| `git push` to a protected branch | **critical** | Will fail if branch protection requires status checks. Push to unprotected branch or use PR. |115| `gh pr merge` without `\|\| true` or `continue-on-error` | **medium** | May fail if PR is not mergeable — handle gracefully. |116| Steps after a `continue-on-error` step that depend on its output | **medium** | Check if downstream steps handle the soft failure. |117| Network-dependent steps without retry or `continue-on-error` | **low** | CDN downloads, API calls can be flaky. |118119### 8. Reliability — Concurrency120121| Check | Severity | Rule |122|-------|----------|------|123| Scheduled workflow without `concurrency` group | **medium** | Overlapping runs waste resources. |124| `cancel-in-progress: true` on deploy workflows | **high** | Can corrupt partial deployments. Use `false` for deploys. |125| Missing `concurrency` on Claude Code jobs | **medium** | Multiple concurrent AI runs on the same issue/PR waste credits. |126127### 9. Reliability — Branch Protection Awareness128129| Check | Severity | Rule |130|-------|----------|------|131| Workflow pushes to `main` (or default branch) | **critical** | Check if branch protection allows this. Use a data branch or PR workflow. |132| Workflow creates commits without checking `git diff` first | **medium** | May create empty commits or fail on no-changes. |133134### 10. Cross-Workflow Consistency135136| Check | Severity | Rule |137|-------|----------|------|138| Different `node-version` across workflows | **high** | All workflows should use the same Node.js version (currently 22). |139| Different `pnpm` version across workflows | **high** | All workflows should use the same pnpm version (currently 10). |140| Different Rust toolchain specification | **medium** | Should be consistent unless intentionally testing multiple versions. |141| Duplicate triggers (same event in multiple workflows) | **medium** | Can cause double-execution. Verify intentional. |142143### 11. Claude Code Action — Configuration144145Reference: `anthropics/claude-code-action@v1`146147| Check | Severity | Rule |148|-------|----------|------|149| Using `@beta` or `@v0` | **critical** | Migrate to `@v1`. v0.x inputs are deprecated. |150| Using deprecated inputs (`direct_prompt`, `model`, `allowed_tools`, `max_turns`, `timeout_minutes`) | **high** | Migrate to `prompt` + `claude_args`. |151| Missing `claude_code_oauth_token` or `anthropic_api_key` | **critical** | One auth method is required. |152| `--model` not specified in `claude_args` | **low** | Defaults to action's default model. Specify for reproducibility. |153| `--max-turns` not specified for fix/implementation jobs | **medium** | Unbounded turns burn credits. Recommend 15-25 for fixes. |154| `show_full_output: true` on review jobs | **low** | Verbose — only needed for debugging. |155156**Key `claude_args` flags:**157- `--model <model-id>` — e.g., `claude-opus-4-6`, `claude-sonnet-4-6`158- `--max-turns <N>` — limit conversation turns159- `--allowedTools <tool1>,<tool2>` — restrict tool access160- `--disallowedTools <tool1>` — block specific tools161- `--system-prompt "..."` — custom system prompt162163**Authentication options:**164- `anthropic_api_key` — direct Anthropic API165- `claude_code_oauth_token` — Claude Code OAuth (subscription-based)166- `use_bedrock: true` + OIDC — Amazon Bedrock167- `use_vertex: true` + OIDC — Google Vertex AI168169### 12. Trigger Hygiene170171| Check | Severity | Rule |172|-------|----------|------|173| `release: [published]` + `workflow_dispatch` for same logic | **medium** | Choose one trigger path to avoid double-execution. |174| `push: branches: [main]` on workflows that also have `pull_request` | **low** | Intentional for CI — but verify both are needed. |175| Scheduled workflow that only runs on default branch | **low** | Verify schedule cron syntax with crontab.guru. |176| Workflow with no `paths` filter on push trigger | **low** | Consider adding `paths:` to avoid unnecessary runs. |177178## Report Format179180Output a markdown table:181182```markdown183## Workflow Audit Report184185| # | Severity | File | Check | Finding | Fix |186|---|----------|------|-------|---------|-----|187| 1 | critical | ci.yml | Action versions | `actions/checkout@v4` — Node 20 deprecated | Upgrade to `@v6` |188| 2 | high | claude.yml | Auto-merge | Enabled for all PRs | Add `if: github.event.pull_request.user.login == 'claude[bot]'` |189```190191After the table, add a **Summary** line:192`X critical, Y high, Z medium, W low findings across N workflow files.`193194## Notes195196- Do NOT modify workflow files during audit — report only.197- When the user asks to fix findings, apply changes and re-audit to verify.198- For security findings, always explain the attack vector (not just the rule).199- Check `.github/workflows/` only — ignore `.github/actions/` unless referenced.