CI Agent Hardening
Audit and fix security vulnerabilities in GitHub Actions workflows, with emphasis on AI-powered CI/CD. Based on two major 2026 incidents:
- Clinejection — Prompt injection in AI triage bot led to cache poisoning, credential theft, malicious npm publish (~4,000 developers affected)
- hackerbot-claw — Autonomous AI bot exploited 7 repos (Microsoft, DataDog, CNCF, Aqua Trivy), achieving full repo compromise on Trivy (25k+ stars) via PAT theft
Prerequisites
- Repository with
.github/workflows/directory bashavailable for running the audit script
Instructions
For background and exploit details on any pattern below, see references/ATTACK-PATTERNS.md.
Step 1: Run the Automated Audit
Run from the repository root:
bash <skill-path>/scripts/audit-workflows.sh .github/workflows
Review [CRIT] findings first (exploitable now), then [WARN] (defense gaps).
Step 2: Fix pull_request_target + Fork Checkout (CRITICAL)
This is the #1 attack vector. 4 of 7 hackerbot-claw attacks exploited it. Check every pull_request_target workflow:
# DANGEROUS — runs fork code with your secrets
on: pull_request_target
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # ATTACKER'S CODE
# SAFE — use pull_request trigger instead (no secret access)
on: pull_request
# SAFE — if pull_request_target needed, checkout base only
on: pull_request_target
steps:
- uses: actions/checkout@v4 # defaults to base branch
If the workflow needs both secrets AND fork code (e.g., comment on PR), split into two workflows: one that runs untrusted code (no secrets) and one that consumes artifacts (with secrets).
Step 3: Eliminate Expression Injection in run: Blocks
Any ${{ }} expression referencing user-controlled values inside a run: block is a shell injection. Branch names, filenames, PR titles, commit messages are all vectors.
# VULNERABLE — branch name is shell-evaluated
- run: echo "${{ github.event.pull_request.head.ref }}"
# SAFE — environment variable (properly quoted by runner)
- run: echo "$PR_HEAD_REF"
env:
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
Check ALL run: blocks for these user-controlled fields:
github.event.issue.title/.bodygithub.event.pull_request.title/.body/.head.refgithub.event.comment.bodygithub.event.discussion.title/.bodygithub.event.review.bodygithub.event.head_commit.message
Step 4: Secure Slash Commands
Any issue_comment-triggered workflow that runs code must check author_association:
# VULNERABLE — any GitHub user can trigger
if: contains(github.event.comment.body, '/deploy')
# SAFE — restrict to maintainers
if: >-
contains(github.event.comment.body, '/deploy') &&
(github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'OWNER')
Step 5: Harden AI Agent Workflows
- Minimize tools — Triage/review bots need
Readat most. NEVER grantBashto workflows triggered by external users. - Remove wildcard users — Delete
allowed_non_write_users: "*". Scope to known collaborators. - Restrict permissions — AI workflows should have
contents: readat most. - Protect CLAUDE.md — Add to
CODEOWNERSrequiring maintainer review. Validate against expected schema in CI. - Never checkout fork code for AI review — use base branch checkout only.
Step 6: Set Least-Privilege Permissions
Every workflow must have an explicit permissions: block:
permissions:
contents: read
pull-requests: read # or write only if needed
A quality check script does NOT need contents: write. A GITHUB_TOKEN with write access, if stolen, allows pushing commits and merging PRs.
Step 7: Harden Credential Management
- Use OIDC provenance —
npm publish --provenance, PyPI trusted publishers. Eliminates static tokens. - Prefer
GITHUB_TOKENover PATs — auto-scoped, expires after workflow. - If PAT required — use fine-grained PATs with minimal scope, stored in GitHub Environments with protection rules.
- Separate nightly/production credentials — different tokens, scopes, and workflows.
- Verify rotation — test-publish a canary after rotating to confirm old token is revoked.
Step 8: Eliminate Cache Attack Surface
- Remove
actions/cachefrom release workflows — install dependencies fresh. - If cache required — use workflow-specific key prefixes that cannot be written by other workflows.
- Monitor for anomalies — cache size >10GB, sudden miss rate spikes.
Step 9: Network Egress Monitoring
Every attack in the hackerbot-claw campaign depended on curl to hackmoltrepeat.com. Consider:
- StepSecurity Harden-Runner for network egress allowlisting
- Alert on outbound calls to unknown domains during CI
- Block
curl/wgetto non-allowlisted domains
Step 10: Disclosure Readiness
SECURITY.mdwith contact info and SLA (Cline ignored reports for 40 days).- Monitor security inbox actively.
- Credential rotation runbook — tested before you need it.
- Incident response plan covering repo takeover scenario (Trivy: repo renamed, releases deleted, malicious extension published).
Generating Fix PRs
Apply changes per-file, explain each change. Priority order:
- Remove
pull_request_target+ fork checkout patterns (or split workflows) - Move
${{ }}expressions toenv:variables in allrun:blocks - Add
author_associationchecks to slash command workflows - Add explicit
permissions:blocks with least privilege - Reduce AI agent
allowed_toolsto minimum - Remove
allowed_non_write_users: "*" - Add
--provenanceto publish commands - Remove
actions/cachefrom release workflows - Add
CLAUDE.mdtoCODEOWNERS
Error Handling
| Issue | Cause | Solution |
|---|---|---|
| Script finds no workflows | Wrong path | Verify repo root, check .github/workflows/ |
| False positive on AI detection | Keyword match in comments | Review context manually |
pull_request_target needed for secrets |
Workflow design requires it | Split into two workflows (see Step 2) |
| OIDC provenance fails | Registry not configured | Follow npm/PyPI OIDC setup docs |
See Also
- references/ATTACK-PATTERNS.md — Full details on Clinejection + hackerbot-claw: all 7 attacks, exploit code, IOCs, comprehensive hardening checklist