Skip to content
Development
Agent

security-ci-cd

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

From plugin
vexjoy-agent
449198 skills198 agents12 commands83 hooks

Claude Code

$ npx -y skills add notque/vexjoy-agent --agent claude-code

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
@notque
Ships withvexjoy-agent

Essays and writing behind this toolkit live at vexjoy.com. VexJoy Agent connects plain-English requests to specialist agents, skills, and workflows. /do selects the knowledge and tools needed for your task.

44964 skills198 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
449
Star history of notque/vexjoy-agent on GitHub, a chart by star-history.com

Other agents on vexjoy-agent.

modules

modules

Agent

**Scope**: Module selection patterns, builtin vs command/shell decisions, collection modules, and version-specific module changes…

@notque@notqueInstall Agent
testing

testing

Agent

**Scope**: Molecule test scenarios, ansible-lint rules, idempotency validation, and check-mode patterns **Version range**:…

@notque@notqueInstall Agent
communication-patterns

communication-patterns

Agent

**Scope**: Failure modes in agent output style — over-reporting, self-congratulation, verbose narration, and hedging. Covers what…

@notque@notqueInstall Agent

More agents from other plugins.

a11y-architect
Auto-invoked

a11y-architect

Agent

Accessibility Architect specializing in WCAG 2.2 compliance for Web and Native platforms. Use PROACTIVELY when designing UI…

ecc13d293k
@affaan-m@affaan-mInstall Agent
analyze-pass-impact

analyze-pass-impact

Agent

Analyzes how a specific topic affects a group of compiler passes. Used by the /plan-update skill to parallelize research across…

react2d252k
@facebook@facebookInstall Agent
orchestrator

orchestrator

Agent

- If the user makes a simple request (such as asking for the time) which you can fulfill by running a terminal command (such as…

codex2d135k
@openai@openaiInstall Agent
cavecrew-builder

cavecrew-builder

Agent

Surgical 1-2 file edit. Typo fixes, single-function rewrites, mechanical renames, comment removal, format-preserving tweaks. Hard…

caveman3d119k
@juliusbrussee@juliusbrusseeInstall Agent

Questions about security-ci-cd.

Which coding agents can use security-ci-cd?
security-ci-cd installs in Claude Code and Cursor: each loads subagents from .claude/agents, so copy the file there. Flowy's auto-invocation runs in Claude Code only.