One GitHub Issue, Three Coding Agents, Zero Privileges

·12 min read·Evergreen Tools Team

On August 5, 2026, at Black Hat USA 2026, Novee Security researcher Elad Meged presented research showing that an account with no repository privileges could, by opening a single GitHub issue, reach CI runner secrets in the vendors' own repositories for Anthropic's Claude Code, Google's Gemini CLI, and OpenAI's Codex. The Cloud Security Alliance documented two of the resulting CVEs, the fixed versions, and the hardening guidance in a research note published August 8, 2026 (CSA, August 8, 2026). The configurations tested were not careless third-party integrations but the vendors' own defaults, unmodified - and the researchers later found comparable configurations replicated across well over a hundred public repositories. That makes this a design pattern rather than one vendor's oversight.

An issue anyone can open is an instruction anyone can send

An issue anyone can open is an instruction anyone can send

1. The Shared Entry Point: An Issue Anyone Can Open

Placed side by side, all three attacks enter through the same door: automation that treats issue content as an instruction to an agent, inside a repository artifact that anyone with an internet connection can create. When that link exists, control of a privileged execution environment is handed to a stranger. CSA draws the line back further: in the Clinejection incident in early 2026, a single malicious GitHub issue title enabled CI/CD cache poisoning, theft of npm credentials, publication of a trojanized package that reached roughly 4,000 developer machines, and the silent installation of a highly privileged secondary agent on those machines (CSA, August 8, 2026). GuardFall and GhostCommit point at the same conclusion: the safeguard fails not because the model is unsafe, but because the harness between the model and the real system is.

# .github/workflows/agent-triage.yml - never let an outsider drive the runner
name: agent-triage
on:
  issues:
    types: [opened]

permissions:
  contents: read        # no write, no packages, no id-token

jobs:
  triage:
    if: >-
      github.event.issue.author_association == 'MEMBER' ||
      github.event.issue.author_association == 'OWNER'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - run: ./run-agent.sh
        env:
          AGENT_MODE: suggest-only   # no shell exec, no auto-merge

# The disclosure's common entry point was a trigger reachable by an account
# with no repository privileges. Start by removing that trigger.
Approved domains are exfiltration channels once the agent is attacker-controlled

Approved domains are exfiltration channels once the agent is attacker-controlled

2. Claude Code: Strip the Quotes and the Command Validator Blinks

The path against Anthropic's Claude Code is specific. Its command validator strips single-quoted text before running its checks, so a payload smuggled inside a git push flag reached the runner untouched; after two further patch-and-bypass rounds, the final variant was tracked as CVE-2026-54316. NVD's description is explicit: because huggingface.co was pre-approved as a bare hostname for the WebFetch tool, any path on that domain, including attacker-controlled model repository files, was auto-approved without a permission prompt and without being subject to allowed-tools restrictions. An attacker could therefore encode an API key character by character into requests for files in a public repository, using server-side download counters as a covert out-of-band channel (NVD, CVE-2026-54316, fixed in 2.1.163).

# oidc.py - replace long-lived tokens with short-lived, scoped credentials
import json

def github_app_token(installation_id, repos, permissions):
    # Exchange a workload identity for a token that dies with the job.
    # Scope per repository, per permission. Never a classic PAT.
    body = json.dumps({
        "repositories": repos,
        "permissions": permissions,      # e.g. {"contents": "read"}
        "expires_in": 600,
    }).encode()
    return json.loads(post("/app/installations/%s/access_tokens" % installation_id, body))["token"]

# If a token does leak, a ten-minute read-only token is worth almost nothing
# to an attacker. A classic PAT with repo scope is worth everything.
Treat agents as privileged automated identities, with least privilege and logs

Treat agents as privileged automated identities, with least privilege and logs

3. Gemini CLI and Codex: One 10.0, One 'Working as Designed'

Gemini CLI took a different route with a heavier outcome. Automatic workspace trust in headless mode, combined with an allowlist checked at registration but never enforced at execution, let an attacker read the parent process environment through Linux's /proc filesystem and reach pre-sandbox host-level code execution. That is CVE-2026-12537, CVSS v4 score 10.0, affecting Gemini CLI before 0.39.1 and the run-gemini-cli GitHub Action before 0.1.22 (NVD, CVE-2026-12537). OpenAI's Codex received no CVE: the company characterized the multi-pass architecture, where one invocation writes an instruction file that a later invocation loads and trusts, as the sandbox working as designed rather than a patchable defect. CSA's guidance follows from that: teams running multi-pass Codex workflows should treat it as an architecture defect of their own - split passes into separate jobs with independent clean checkouts, or confine the pass that consumes another pass's output to a read-only sandbox that cannot write AGENTS.md.

# normalize.py - validate the command the shell will run, not the raw string
import shlex

def effectively_runs(raw_command):
    parts = shlex.split(raw_command, posix=True)   # quote removal happens here
    return parts

def validate(raw_command, allowlist):
    argv = effectively_runs(raw_command)
    if not argv or argv[0] not in allowlist:
        return {"allow": False, "reason": "binary not allowlisted"}
    for token in argv[1:]:
        if token.startswith("-") and any(c in token for c in "|;&$()"):
            return {"allow": False, "reason": "shell metacharacter in flag"}
    return {"allow": True, "argv": argv}

# Ask your vendor whether validators run on the raw model string or on the
# fully normalized command the shell will actually execute.

4. Four Things to Do Immediately

CSA's immediate actions have a deliberate order. First, upgrade to Claude Code 2.1.163 or later, Gemini CLI 0.39.1 or later (0.40.0-preview.3 included), and run-gemini-cli 0.1.22 or later. Second, audit which of your own workflows can be triggered by an outside contributor opening an issue, filing a pull request, or leaving a comment, because that trigger condition was the common entry point across all three vendors. Third, rotate any repository secrets, GITHUB_TOKEN scopes, and model API keys reachable from those workflows if they ran in a vulnerable configuration before patching. Fourth, disable auto-execute modes that skip human approval in any workflow reachable by unauthenticated or low-privilege external input. The order matters: close the door, then change the keys.

# proc_probe_detector.py - the credential-probing signal to alert on
SUSPECT = (
    "/proc/self/environ",
    "/proc/1/environ",
    "/proc/*/environ",
)

def alert(line, allowlisted_pids=()):
    if any(p.replace("*", "") in line for p in SUSPECT):
        pid = extract_pid(line)
        if pid not in allowlisted_pids:
            return {"severity": "high", "signal": "runner-env-probe", "line": line}
    return None

# Gemini CLI's CVE-2026-12537 was reached by reading the parent process
# environment through Linux's /proc filesystem. Almost nothing legitimate in
# a CI runner does that, so the signal has a low false-positive rate.

5. Long-Term Hardening: Treat Agents as Privileged Identities

The most valuable line in the note is a question to put to vendors: does command validation operate on the raw string the model produced, or on the fully normalized command the shell will actually execute? Only the second reliably catches this class of bypass. Five things follow for engineering teams. Restrict triggers to trusted author associations and tighten permissions (example 1). Replace long-lived personal access tokens with short-lived credentials scoped per repository and per permission (example 2). Validate after normalization instead of before (example 3). Alert on credential-probing behavior such as reads of /proc/*/environ (example 4). And inventory every workflow an outsider can trigger along with the secrets it can reach (example 5). Two more rules carry equal weight: treat issue text, pull request descriptions, comments, and auto-loaded files like AGENTS.md, CLAUDE.md, and .env as untrusted input; and read your approved-domain allowlist like a firewall rule, because once an agent is attacker-controlled, an approved domain is a ready-made exfiltration channel.

# workflow_audit.py - find every workflow an outsider can trigger
import glob
import yaml

EXTERNAL = ("issues", "issue_comment", "pull_request_target", "discussion", "workflow_run")

def audit():
    findings = []
    for path in glob.glob(".github/workflows/*.yml"):
        wf = yaml.safe_load(open(path))
        triggers = wf.get(True) or wf.get("on") or {}
        if isinstance(triggers, str):
            triggers = {triggers: {}}
        risky = [t for t in triggers if t in EXTERNAL]
        if risky:
            findings.append({"workflow": path, "triggers": risky, "jobs": list((wf.get("jobs") or {}))})
    return findings

# Rotate any secret reachable from a workflow on this list if it ran in a
# vulnerable configuration before patching. CSA recommends exactly that.

📌 Frequently Asked Questions

Did the attacker need repository privileges?

No. That is the central point of the disclosure: an account with no repository privileges was enough, by opening a GitHub issue. The researchers tested the vendors' own repositories running their own default workflow configurations.

What are the two CVEs?

CVE-2026-54316 affects Claude Code (fixed in 2.1.163), where arbitrary paths under the pre-approved hostname huggingface.co formed a covert exfiltration channel. CVE-2026-12537 affects Gemini CLI before 0.39.1 and run-gemini-cli before 0.1.22, where headless CI workflows allowed reading the parent process environment through /proc, with a CVSS v4 score of 10.0.

Why did Codex get no CVE?

OpenAI characterized its multi-pass architecture - where one invocation can write an instruction file a later invocation loads and trusts - as the sandbox working as designed rather than a patchable defect. CSA therefore recommends self-remediation: split passes into separate jobs with clean checkouts, or confine any pass consuming another pass's output to a read-only sandbox that cannot write AGENTS.md.

Was any of this exploited in the wild?

As of the disclosure date, CSA notes that CISA's exploit-tracking data showed no confirmed active exploitation of the Claude Code or Gemini CLI CVEs. That does not lower the priority, since comparable default configurations were found across well over a hundred public repositories.

Why is an approved domain a risk?

NVD's description of CVE-2026-54316 shows that once huggingface.co was pre-approved as a bare hostname, any path on it was auto-approved and exempt from allowed-tools restrictions, letting an attacker encode data into requests for files in public repositories. Allowlists should be defined by full path and purpose, not by domain.