From 32dde62520a4b45c383cb9172e662fef4f051032 Mon Sep 17 00:00:00 2001 From: ivanpaghubasan Date: Mon, 19 Oct 2026 13:19:27 +0800 Subject: [PATCH 1/2] feat(ci): add CI-001..005 for coding agents in GitHub Actions workflows Two packs under ci/, schema_version 24, and the README and CLAUDE.md family entries. --- CLAUDE.md | 1 + README.md | 8 +++- ci/agent_hardening.yaml | 92 ++++++++++++++++++++++++++++++++++++++ ci/untrusted_triggers.yaml | 78 ++++++++++++++++++++++++++++++++ manifest.yaml | 2 +- 5 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 ci/agent_hardening.yaml create mode 100644 ci/untrusted_triggers.yaml diff --git a/CLAUDE.md b/CLAUDE.md index fb86040..c5ee082 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -128,6 +128,7 @@ enum values used by the `repo_has_sdk_in_code` predicate: | `vercel_ai` | Vercel AI SDK | | `autogen` | AutoGen / AG2 | | `raw_llm_sdk` | Bare `anthropic` / `openai` client usage (no agent framework); category `raw_llm_sdk` loads unconditionally, the rule's own predicate gates | +| `github_actions` | Coding agents in GitHub Actions workflows (repo-root `.github/workflows/*.yml`); category `ci` loads unconditionally, the rule's own `repo_ci_agent_*` predicate gates. One finding per agent job, carrying the step's file and line | | `agent_config` | Pre-trust harness config committed to the repo (`.claude/settings.json`, `.mcp.json`, `.gemini/.env`, `.vscode/settings.json`, `.codex/config.toml`); category `agent_config` loads unconditionally, the rule's own `repo_*` predicate gates. Findings carry the offending file and line | Repo-scope rules typically combine `applies_to` with a `repo_has_sdk_in_code` diff --git a/README.md b/README.md index 3a9cf38..231eecc 100644 --- a/README.md +++ b/README.md @@ -155,10 +155,14 @@ vercel_ai/ Vercel AI SDK rules (VAI-NNN, all typescri agent_config/ Pre-trust harness config rules (AGCFG-NNN, MCPCFG-NNN; repo scope, no SDK gate) ├── mcp_client_config.yaml MCPCFG-002 └── preauth_execution.yaml AGCFG-004..007 + +ci/ Coding agents in GitHub Actions workflows (CI-NNN; repo scope, no SDK gate) +├── untrusted_triggers.yaml CI-001, CI-002 +└── agent_hardening.yaml CI-003..005 ``` -The `agent_config` families are cross-harness config rules, so they sit outside the -SDK-prefix numbering below (AGCFG-001..003 and MCPCFG-001 are unused). +The `agent_config` and `ci` families are cross-harness config rules, so they sit outside the +SDK-prefix numbering below (AGCFG-001..003 and MCPCFG-001 are unused; CI rules number by topic). ID prefix denotes SDK; `NNN` tool scope, `1NN` agent / subagent scope, `2NN` repo scope. Two packs predate that convention: `autogen/` numbers its agent diff --git a/ci/agent_hardening.yaml b/ci/agent_hardening.yaml new file mode 100644 index 0000000..f2ad5cf --- /dev/null +++ b/ci/agent_hardening.yaml @@ -0,0 +1,92 @@ +policy: + id: ci_agent_hardening + name: Hardening of coding-agent jobs on untrusted GitHub Actions triggers + category: ci + description: > + Repo-scoped rules for coding-agent jobs in GitHub Actions workflows that + run on an event any GitHub user can cause, with or without an actor gate: + the content of the event is untrusted input either way. Covers persisted + checkout credentials, unrestricted shell and missing egress control. The + pack loads on each scan and each rule's predicate gates it. One finding + is reported per offending agent job. + +rules: + - id: CI-003 + title: Checkout persists credentials in an agent job + severity: high + confidence: 0.7 + applies_to: + - github_actions + scope: repo + match: + repo_ci_agent_checkout_persists_credentials: true + explanation: > + A job that runs a coding agent on an untrusted trigger checks out the + repository with actions/checkout and does not set persist-credentials: + false. The actions/checkout documentation describes persist-credentials + as whether to configure the token or SSH key with the local git config, + with a default of true. Commands run by an agent steered by the event's + content run on a runner where that configuration was applied. + Limits of this check: it reads persist-credentials on actions/checkout + steps in the same job as the agent step. A false value (boolean or the + string) is the sole value treated as safe, so an expression counts as + persisting. Credentials set up another way, such as git config in a run + step or a token passed to an action, are not seen. + fix: > + Set persist-credentials: false on each actions/checkout step in the agent + job. If the agent must push, give it a narrowly scoped token through the + action's own input instead of the checkout's stored credential. + + - id: CI-004 + title: Agent step grants unrestricted shell + severity: high + confidence: 0.8 + applies_to: + - github_actions + scope: repo + match: + repo_ci_agent_unrestricted_shell: true + explanation: > + A coding agent on an untrusted trigger may run any shell command. Its + tool allow-list grants Bash with no pattern, Bash(*), or a pattern that + starts with a shell, interpreter or download tool (bash, sh, zsh, cmd, + powershell, pwsh, python, python3, node, curl, wget, nc, ncat); or the + step skips approvals or the sandbox (--dangerously-skip-permissions, + --permission-mode bypassPermissions, Gemini --yolo or a bare + run_shell_command, Codex --dangerously-bypass-approvals-and-sandbox, + sandbox: danger-full-access or safety-strategy: unsafe). Text from the + triggering event can then make the agent run arbitrary commands on the + runner with the job's token and secrets. + fix: > + Allow the exact commands the task needs, for example Bash(npm test) or + run_shell_command(git diff), and remove any flag that skips approvals or + the sandbox. Keep Codex on its default safety strategy with a workspace-write + sandbox. + + - id: CI-005 + title: Agent job with shell or web access has no egress restriction + severity: medium + confidence: 0.5 + applies_to: + - github_actions + scope: repo + match: + repo_ci_agent_no_egress_restriction: true + explanation: > + A coding agent on an untrusted trigger can run shell commands or fetch + URLs (a Bash, WebFetch or WebSearch grant, a Gemini shell or web tool, or + a Codex full-access sandbox), and no step-security/harden-runner step with + egress-policy: block runs before it in the job. If the event's content + gets the agent to read a token or secret, nothing in the workflow stops + it sending the value to an outside host. This is an inference from the + workflow file alone. + Limits of this check: network controls outside the workflow file, such as + a self-hosted runner's network policy or organization settings, are not + visible. The harden-runner step is recognised by its action name and + egress-policy: block, and must come before the agent step in the same + job. An agent step with no tool configuration is counted as having + neither shell nor fetch access. + fix: > + Add step-security/harden-runner as the first step of the agent job with + egress-policy: block and allowed-endpoints limited to the model API and + GitHub, or run the agent on a runner whose network is restricted. diff --git a/ci/untrusted_triggers.yaml b/ci/untrusted_triggers.yaml new file mode 100644 index 0000000..e90811e --- /dev/null +++ b/ci/untrusted_triggers.yaml @@ -0,0 +1,78 @@ +policy: + id: ci_untrusted_triggers + name: Coding agents on untrusted GitHub Actions triggers + category: ci + description: > + Repo-scoped rules for GitHub Actions workflows in the repo-root + .github/workflows directory that run a coding agent on an event any GitHub + user can cause: issues, issue comments, pull request review comments, + discussions, discussion comments and pull_request_target. The pack loads + on each scan and each rule's predicate gates it. One finding is reported + per offending agent job. + +rules: + - id: CI-001 + title: Agent step runs on an untrusted trigger without an actor gate + severity: critical + confidence: 0.7 + applies_to: + - github_actions + scope: repo + match: + repo_ci_agent_untrusted_trigger_ungated: true + explanation: > + A workflow runs a coding agent (claude-code-action, claude-code-base-action, + run-gemini-cli, codex-action, or the claude, gemini, codex, cline or aider + CLI) on an event that any GitHub user can cause, such as opening an issue + or posting a comment, and nothing limits who that user is. No if: on the + job, the step or a job it needs restricts author_association to OWNER, + MEMBER or COLLABORATOR or names the allowed actors, and the action's own + write-access check is absent or switched off. claude-code-action and + codex-action check for write access themselves; that check is treated as + switched off when allowed_non_write_users or allowed_bots is set to '*' + or when allow-users is set to '*'. The event's title, body and comments + reach the agent as input, so any user who can cause the event can give the + agent instructions. + Limits of this check: an if: expression is matched as text and is not + evaluated, so a condition that joins an allow-list with another branch + (A || allow-list) counts as gated. An actor check made in an earlier step, + an environment with required reviewers, and a label gate are not + recognised. A reusable workflow is followed one hop when it is a local + file; remote reusable workflows and composite actions are not followed. + fix: > + Add a job-level if: that admits trusted actors, for example + contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), + github.event.comment.author_association). For claude-code-action and + codex-action, keep the built-in write-access check by removing '*' from + allowed_non_write_users, allowed_bots and allow-users. + + - id: CI-002 + title: Ungated agent job holds a write token or extra secrets + severity: critical + confidence: 0.75 + applies_to: + - github_actions + scope: repo + match: + repo_ci_agent_untrusted_trigger_privileged: true + explanation: > + An agent job that any GitHub user can trigger, with no actor gate (the + CI-001 condition), also holds authority beyond the model key: its + GITHUB_TOKEN has write-all or write access to contents, pull-requests, + actions or packages; it references a secret outside the agent's + model-key input; or it passes secrets: inherit to a reusable workflow. + Instructions from the triggering event can then act with that token or + read that secret. + Limits of this check: permissions are read from the job, then the + workflow, then the calling job; when none of them declares a permissions + block nothing is flagged, because the repository default is not visible. + A secret counts as the model key by where it is referenced (the agent + step's model-key input, or a model-key environment variable such as + ANTHROPIC_API_KEY), not by its name. Secrets passed to a remote reusable + workflow are not seen. The write scopes counted are contents, + pull-requests, actions and packages; other scopes such as id-token are + not counted. + fix: > + Give the agent job a read token (permissions: contents: read) and pass it the + model key alone. Move any step that needs write access or another secret + into a separate job that runs after a trusted-actor check. diff --git a/manifest.yaml b/manifest.yaml index f203f22..1d40fab 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -11,4 +11,4 @@ # # This file is metadata, not a rule: the engine's loader skips manifest.yaml # when walking the pack for policy files. -schema_version: 23 +schema_version: 24 From 8a197e1e6546ce26ce4282a71e1e722fccff4c9f Mon Sep 17 00:00:00 2001 From: ivanpaghubasan Date: Mon, 19 Oct 2026 13:53:08 +0800 Subject: [PATCH 2/2] docs(ci): state that CI findings are per agent step, per checkout step for CI-003 --- CLAUDE.md | 2 +- ci/agent_hardening.yaml | 3 ++- ci/untrusted_triggers.yaml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c5ee082..cbe5d8f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -128,7 +128,7 @@ enum values used by the `repo_has_sdk_in_code` predicate: | `vercel_ai` | Vercel AI SDK | | `autogen` | AutoGen / AG2 | | `raw_llm_sdk` | Bare `anthropic` / `openai` client usage (no agent framework); category `raw_llm_sdk` loads unconditionally, the rule's own predicate gates | -| `github_actions` | Coding agents in GitHub Actions workflows (repo-root `.github/workflows/*.yml`); category `ci` loads unconditionally, the rule's own `repo_ci_agent_*` predicate gates. One finding per agent job, carrying the step's file and line | +| `github_actions` | Coding agents in GitHub Actions workflows (repo-root `.github/workflows/*.yml`); category `ci` loads unconditionally, the rule's own `repo_ci_agent_*` predicate gates. One finding per step that runs an offending agent (CI-003: per checkout step), carrying that step's file and line | | `agent_config` | Pre-trust harness config committed to the repo (`.claude/settings.json`, `.mcp.json`, `.gemini/.env`, `.vscode/settings.json`, `.codex/config.toml`); category `agent_config` loads unconditionally, the rule's own `repo_*` predicate gates. Findings carry the offending file and line | Repo-scope rules typically combine `applies_to` with a `repo_has_sdk_in_code` diff --git a/ci/agent_hardening.yaml b/ci/agent_hardening.yaml index f2ad5cf..2161679 100644 --- a/ci/agent_hardening.yaml +++ b/ci/agent_hardening.yaml @@ -8,7 +8,8 @@ policy: the content of the event is untrusted input either way. Covers persisted checkout credentials, unrestricted shell and missing egress control. The pack loads on each scan and each rule's predicate gates it. One finding - is reported per offending agent job. + is reported for each step that runs an offending agent; for CI-003, for + each actions/checkout step in an offending agent job. rules: - id: CI-003 diff --git a/ci/untrusted_triggers.yaml b/ci/untrusted_triggers.yaml index e90811e..782c29c 100644 --- a/ci/untrusted_triggers.yaml +++ b/ci/untrusted_triggers.yaml @@ -8,7 +8,7 @@ policy: user can cause: issues, issue comments, pull request review comments, discussions, discussion comments and pull_request_target. The pack loads on each scan and each rule's predicate gates it. One finding is reported - per offending agent job. + for each step that runs an offending agent. rules: - id: CI-001