fix: harden CI workflows against prompt injection and supply chain risks#852
fix: harden CI workflows against prompt injection and supply chain risks#852
Conversation
- Remove attacker-controlled data (issue title, body, comments) from Claude prompts in issue-triage.yml — Claude now fetches issue context itself via gh issue view - Restrict issue-triage.yml tool access from broad Bash(gh api:*) to specific safe endpoints - Add prompt injection hardening preamble to all Claude review jobs in ci.yml - Replace curl | bash actionlint install with SHA-pinned GitHub Action (v1.7.11) - Scope ci.yml permissions from workflow-level to per-job (least privilege) - Add new claude-ci-security-review job that audits workflow changes for prompt injection, expression injection, supply chain risks, and permission issues - Remove unused close-fork-prs.yml workflow - Remove unnecessary Setup Claude environment steps ## Test plan - Open a test issue and run /triage — verify Claude fetches context itself and produces a valid triage - Open a test PR touching src/ — verify all three existing Claude review jobs still run - Open a test PR touching .github/workflows/ — verify the new CI security review job triggers - Verify non-CI PRs are not blocked by the skipped claude-ci-security-review job - Verify workflow-validation still runs actionlint via the pinned action
There was a problem hiding this comment.
Code Review
Blocking Issues
None.
Suggestions
-
Pre-existing: third-party actions not SHA-pinned —
dorny/paths-filter@v3andanthropics/claude-code-action@v1in ci.yml are pinned by tag, not commit SHA. The newrhysd/actionlintpin is correctly done. Consider SHA-pinning the others in a follow-up for consistency with the security posture this PR establishes. (Not blocking since these are pre-existing and unchanged.) -
Pre-existing: issue-triage.yml uses workflow-level permissions — Lines 7-9 have
permissionsat workflow level rather than job level. Since there's only one job this is functionally equivalent, but it's inconsistent with the per-job approach applied to ci.yml. Could be aligned in a follow-up. -
Deletion of close-fork-prs.yml — This removed the automatic closing of fork PRs with a contributor-friendly message. The auto-merge job already excludes forks, so fork PRs won't merge, but they will now sit open instead of being closed with guidance. Verify this is the intended behavior — the contributor experience message was useful.
Assessment
Excellent security hardening PR. The key improvements are:
- Prompt injection fix: Removing direct interpolation of
github.event.issue.title,github.event.issue.body, and comment thread into LLM prompts in issue-triage.yml — Claude now fetches this data itself via tool calls - Tool scoping: Replacing
Bash(gh api:*)with specific endpoint restrictions prevents abuse of broad API access - Least privilege: Moving permissions from workflow-level to per-job ensures test/lint jobs only get
contents: read - Supply chain: Replacing
curl | bashwith SHA-pinnedrhysd/actionlint@393031adeliminates remote script execution risk - Defense in depth: Adding prompt injection security preambles to all Claude review prompts and a dedicated CI security review job
There was a problem hiding this comment.
CI Security Review
Critical / High
No critical or high severity findings. This PR is a substantial security hardening of the CI pipeline.
Medium
-
Pre-existing: Unpinned third-party actions —
dorny/paths-filter@v3(ci.yml:25),anthropics/claude-code-action@v1(multiple locations), anddenoland/setup-deno@v2(multiple locations) use tag-based pins rather than full SHA pins. A compromised tag could inject malicious code. These are pre-existing (not introduced by this PR) but worth tracking for a future pinning pass.actions/*(GitHub-owned) with tag pins are acceptable per policy. -
Pre-existing:
issue-triage.ymluses workflow-level permissions (line 7-9) rather than job-level. Since there's only one job, this is functionally equivalent, but job-level scoping is the stronger convention established by this PR inci.yml. Consider aligning in a follow-up.
Low
None.
Verdict
PASS — This PR significantly improves CI security across multiple dimensions:
- Prompt injection fix: Removes direct interpolation of attacker-controlled issue title/body/comments into the triage LLM prompt (previously a critical vulnerability). Claude now fetches data itself via tool calls.
- Tool scope tightening: Narrows
Bash(gh api:*)(which allowed arbitrary GitHub API calls) down to specific allowed commands. - Prompt hardening: Adds security preambles to all LLM prompts instructing the model to treat fetched content as untrusted.
- Permission scoping: Moves from overly broad workflow-level permissions (including unnecessary
id-token: write) to minimal job-level permissions. - Supply chain fix: Replaces
curl | bashremote script execution with a SHA-pinned action. - New CI security review gate: Adds automated security review for workflow changes.
## Summary - Fix `/triage` command failure caused by `shell-quote` parsing of unquoted `Bash(...)` patterns in `claude_args` - Wraps the `--allowedTools` value in single quotes so spaces and parentheses are preserved as a single token ## Root Cause `claude-code-action` uses `shell-quote` to parse `claude_args`. The `Bash(...)` patterns added in #852 contain spaces (e.g. `Bash(gh api --method POST:*/reactions)`), which caused: 1. Parentheses treated as shell operators and stripped 2. Space-splitting broke patterns into separate tokens 3. `--method` parsed as a standalone CLI flag, corrupting the entire argument structure 4. Claude Code crashed with exit code 1 Confirmed by workflow logs from the failed #902 triage — allowed tools were `["Read", "Glob", "Grep", "Bash", "gh", "issue", "api"]` instead of the intended patterns. Fixes #904 ## Test plan - [ ] Re-run `/triage` on #902 after merge and verify it completes successfully - [ ] Verify the workflow logs show the correct `Bash(...)` patterns in `allowedTools` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Test plan