Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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`
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,14 @@ agent_config/ Pre-trust harness config rules (AGCFG-NNN,
├── harness_guidance.yaml AGCFG-010
├── 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
Expand Down
93 changes: 93 additions & 0 deletions ci/agent_hardening.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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 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
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.
78 changes: 78 additions & 0 deletions ci/untrusted_triggers.yaml
Original file line number Diff line number Diff line change
@@ -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
for each step that runs an offending agent.

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.
2 changes: 1 addition & 1 deletion manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: 27
schema_version: 28
Loading