security-ci-cd
Load when reviewing GitHub Actions workflows, CI pipeline configs, action definitions, or CI build scripts.
$ npx -y skills add notque/vexjoy-agent --agent claude-codeHow 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.mdCI/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 testFor 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.mdWhy 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
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 testFor 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.mdWhy 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
Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.
Repo: notque/vexjoy-agent
Other agents on vexjoy-agent.
- ansible-automation-engineer
Ansible automation: playbooks, roles, collections, Molecule testing, Vault security.
Open agent - modules
**Scope**: Module selection patterns, builtin vs command/shell decisions, collection modules, and version-specific module changes **Version range**: ansible-core 2.14+ / Ansible Collections (community.general 7.0+) **Generated**: 2026-04-04 — verify against current Ansible
Open agent - testing
**Scope**: Molecule test scenarios, ansible-lint rules, idempotency validation, and check-mode patterns **Version range**: Molecule 6.0+ / ansible-lint 6.0+ / ansible-core 2.14+ **Generated**: 2026-04-04 — verify against current Molecule and ansible-lint documentation
Open agent - base-instructions
Universal operational rules injected by /do at agent dispatch. Domain-specific rules live in each agent's .md file.
Open agent - communication-patterns
**Scope**: Failure modes in agent output style — over-reporting, self-congratulation, verbose narration, and hedging. Covers what to detect and how to fix each. **Version range**: all versions **Generated**: 2026-05-11
Open agent - combat-effects-upgrade
Zero-dependency combat visual upgrades: CSS particle replacement, Framer Motion combat juice, CSS 3D card transforms.
Open agent

