Skip to content

security-ci-cd

Load when reviewing GitHub Actions workflows, CI pipeline configs, action definitions, or CI build scripts.

From plugin
vexjoy-agent
413198 skills198 agents10 commands86 hooks
Install
$ npx -y skills add notque/vexjoy-agent --agent claude-code

How it fires

How this agent gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.

Context preview

The summary Claude sees to decide when to auto-load this agent.

Load when reviewing GitHub Actions workflows, CI pipeline configs, action definitions, or CI build scripts.

Agent definition

security-ci-cd.md

CI/CD Security Patterns

Load when reviewing GitHub Actions workflows, CI pipeline configs, action definitions, or CI build scripts.

CI/CD pipelines run with elevated privileges: write access, secrets, OIDC tokens, and publishing credentials. Separate privileged and unprivileged execution, pin dependencies to immutable refs, treat all PR-controlled content as untrusted.

---

Run Untrusted Code in Unprivileged Workflows

Use `pull_request` trigger for fork contributions (read-only, no secrets). Use `pull_request_target` only when the workflow needs write access and does not execute PR-controlled code.

Correct Pattern

on: pull_request
permissions:
  contents: read
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - run: npm ci
      - run: npm test

For reporting results, use a separate `workflow_run` job treating artifacts as data, not code:

on:
  workflow_run:
    workflows: ["Test"]
    types: [completed]
permissions:
  pull-requests: write
jobs:
  report:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          run-id: ${{ github.event.workflow_run.id }}
      - run: cat results.json | jq '.summary' > comment.md

Why This Matters

`pull_request_target` runs with full secrets and write permissions. Checking out and executing fork code gives the fork code execution with trusted credentials ("pwn request").

Detection

rg -n 'pull_request_target' .github/workflows/
rg -n 'github\.event\.pull_request\.head\.sha|github\.head_ref' .github/workflows/
rg -B5 -A5 'npm install|npm test|pip install|make |pytest|cargo test|go test' .github/workflows/

---

Pass Untrusted Values Through Environment Variables

GitHub Actions expressions (`${{ }}`) in `run:` blocks are string-interpolated before shell execution. Attacker-controlled content (PR title, issue body, comment, branch name) enables shell injection. Pass through env vars instead.

Correct Pattern

- env:
    PR_TITLE: ${{ github.event.pull_request.title }}
  run: printf '%s\n' "$PR_TITLE"

For `actions/github-script`, pass via `env:` and read with `process.env`:

- uses: actions/github-script@v7
  env:
    ISSUE_TITLE: ${{ github.event.issue.title }}
  with:
    script: |
      const title = process.env.ISSUE_TITLE;
      await github.rest.issues.createComment({
        owner: context.repo.owner, repo: context.repo.repo,
        issue_number: context.issue.number, body: `Triaged: ${title}`,
      });

Safe delimiter-based output for `$GITHUB_OUTPUT`:

- run: |
    delimiter="$(openssl rand -hex 16)"
    echo "title<<${delimiter}" >> "$GITHUB_OUTPUT"
    echo "$PR_TITLE" >> "$GITHUB_OUTPUT"
    echo "${delimiter}" >> "$GITHUB_OUTPUT"
  env:
    PR_TITLE: ${{ github.event.pull_request.title }}

Why This Matters

Expression injection is shell injection via CI. Applies to issue titles, comment bodies, review bodies, branch names, filenames, commit messages, and labels.

**CVEs:** CVE-2026-27701 (LiveCode), Sentry getsentry `0898b3d8`, Sentry `e93ee1ce`.

Detection

rg -n '\$\{\{.*github\.event\.(pull_request\.(title|body)|issue\.(title|body)|comment\.body|review\.body|discussion\.(title|body))' .github/workflows/
rg -n '\$\{\{' .github/workflows/ | rg 'script:'
rg -n '\$\{\{.*github\.(head_ref|event\.commits)' .github/workflows/
rg -n 'GITHUB_ENV|GITHUB_OUTPUT|GITHUB_PATH' .github/workflows/ | rg '\$\{\{'

---

Gate Comment-Triggered Commands with Author Association

`issue_comment` workflows must verify commenter's `author_association` against `MEMBER`, `OWNER`, or `COLLABORATOR` before executing commands.

Correct Pattern

on: issue_comment
jobs:
  deploy-preview:
    if: >
      contains(github.event.comment.body, '/deploy') &&
      contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'),
               github.event.comment.author_association)
    permissions:
      contents: read
      pull-requests: write
    runs-on: ubuntu-latest
    steps:
      - env:
          COMMENT_BODY: ${{ github.event.comment.body }}
        run: ./ci/deploy-preview.sh "$COMMENT_BODY"

For `/ok-to-test` approval, pin checkout to the approved SHA, not current head (TOCTOU risk).

Why This Matters

Without authorization, any GitHub user who can comment on an issue triggers privileged CI actions.

**CVEs:** CVE-2025-53104 (gluestack-ui).

Detection

rg -n 'issue_comment|discussion' .github/workflows/
rg -l 'issue_comment' .github/workflows/ | xargs rg -L 'author_association'
rg -n "contains.*'/deploy\|contains.*'/test\|contains.*'/ok-to" .github/workflows/

---

Pin Third-Party Actions to Full Commit SHAs

Reference third-party actions by 40-character SHA. Tags are mutable — maintainers or attackers can rewrite them.

Correct Pattern

steps:
  - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
  - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af  # v4.1.0
  - uses: tj-actions/changed-files@a4ca7c0a052d49bbf8e69ddca9a3f53dac15c95e  # v45.0.10

Use Dependabot for SHA pin updates:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"

Why This Matters

March 2025: `tj-actions/changed-files` tags v1-v45 were rewritten to exfiltrate secrets from 23,000+ repos via compromised upstream `reviewdog/action-setup`.

**CVEs:** CVE-2025-30066, CVE-2025-30154.

First-party actions (`actions/*`, `github/*`) on tags are acceptable.

Detection

rg -n 'uses:' .github/workflows/ | rg -v 'actions/|github/' | rg -v '@[0-9a-f]{40}'
rg -n 'uses:' .github/workflows/ | rg -v 'actions/|github/' | rg '@v[0-9]|@main|@master|@latest'

---

Restrict Artifact Upload Paths and Disable Credential Persistence

Upload only

Read more
Ships withvexjoy-agent

Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.

Get the whole plugin, auto-invoked