Skip to main content
BlogSecurity

Claude Code and Gemini CLI GitHub Actions Secrets Vulnerability

How one GitHub issue reached CI secrets in Claude Code Action, Gemini CLI and Codex workflows, with patched versions and a hardening checklist.

Matthews Wong

Software Engineer

October 202611 min read
Claude Code and Gemini CLI GitHub Actions Secrets Vulnerability

Key Takeaway

In August 2026 Novee Security showed that a GitHub issue from an account with no repository privileges could reach CI secrets through Claude Code Action, Gemini CLI and Codex workflows. Patches shipped: Claude Code 2.1.163, gemini-cli 0.39.1, run-gemini-cli 0.1.22. The lasting defence is structural: keep untrusted text away from write-capable agents that hold secrets.

The disturbing part of the Black Hat USA 2026 talk is how little the attacker needed. Elad Meged of Novee Security presented on 5 August, and the premise was an issue opened by an account with no privileges on the repository. No fork, no stolen token, no compromised maintainer. The issue text was enough to move a coding agent, running inside the vendor's own default workflow, towards the secrets sitting on the CI runner.

This post is a reading of the primary record, not a reproduction. I read the Cloud Security Alliance research note, the Novee write-up, the GitHub advisory for Gemini CLI, the NVD entry for the Claude Code CVE, Anthropic's own security guidance for the action and GitHub's secure-use reference. I have not run any exploit and I do not describe payloads. Everything below is about what the three failures have in common and what a defender changes this week.

What an issue could reach, and why that is a CI problem

Each of the three vendors ran an agent from a GitHub Actions workflow that fired on public input: an @claude comment, a newly opened issue, an issue opened or labelled. According to Novee, the researchers tested the vendors' own default, unmodified workflow configurations in their own repositories, and the CSA note says comparable configurations were later found in well over a hundred other public repositories.

Novee's framing is that each flaw sat at a handoff, a place where one part of the harness passes a value to another and an assumption fails. Anyone with write access to a repository can read its secrets, as GitHub's own guidance puts it, so an agent that can run shell commands on a runner inherits that power. The question for a defender is never whether the model can be talked into something. It is what the process holding the secrets will do when it is.

Claude Code Action: three rounds of patch and bypass

Novee describes three rounds. In round one, Bash tools were approved by prefix match, and quoted content was stripped before 23 validation checks ran, so a payload hidden in a flag value was never inspected. That gave code execution on the runner and exposure of workflow secrets. Anthropic removed most Bash tools and added an allowlist for git push.

Round two used a hardcoded list of commands treated as read-only. Some utilities on that list were not covered by a separate path-check list, so they could read files outside the workspace, and a default setting published tool output to a public Step Summary where masking missed reversed secret strings. Anthropic disabled that setting and sanitised output. Its current security guidance still warns that show_full_output can expose credentials and that logs are public on public repositories.

Round three is the one with a CVE. The NVD entry for CVE-2026-54316 says that from version 0.2.54 until 2.1.163 the bare hostname huggingface.co was pre-approved for the WebFetch tool, so any path on it, including attacker-controlled model repositories, was fetched without a prompt or an allowedTools restriction. Hugging Face counts those fetches as downloads, which made a covert channel: Novee describes up to 64 repositories encoding secret characters in their download counters. The fix is 2.1.163, and the NVD text itself notes that reliable exploitation needed untrusted content in the context window.

Gemini CLI: trust and a readable parent process

The advisory GHSA-wpqr-6v78-jr5g, which carries CVE-2026-12537 and a CVSS score of 10.0, names two headless-mode defects. Headless runs automatically trusted the workspace when loading configuration and environment variables, so malicious settings in a local .gemini directory could lead to code execution on untrusted content. And under --yolo, fine-grained tool allowlists in settings.json were ignored, so an allowlisted run_shell_command could be driven by prompt injection.

Novee's chain starts at an issue-deduplication workflow that runs for any GitHub user who opens an issue, in yolo mode, with a config meant to allow only echo and gh issue view. A prefix-match check registered the full shell tool instead. The second assumption was secret isolation: the child process environment was cleaned, but the parent held the secrets and shared a UID and PID namespace with the child, so the parent environment stayed readable. Novee reports 114 repositories running the same pattern.

The fixed versions are gemini-cli 0.39.1 (and 0.40.0-preview.3) and run-gemini-cli 0.1.22. The advisory warns that some workflows may break or fail silently after the update, because folder trust is now explicit in headless mode. The documented workaround for trusted inputs is the environment variable GEMINI_TRUST_WORKSPACE set to true. For untrusted inputs you follow the hardening guidance in the run-gemini-cli repository first.

Codex: no CVE, and the sandbox did what it said

OpenAI's case is the subtlest because nothing broke. The openai/codex issue deduplicator ran two Codex passes in one job with a shared checkout, on issues opened or labelled, with allow-users set to a wildcard. Novee says the default workspace-write sandbox protected .git, .codex and .agents but not AGENTS.md, Codex's default instruction file, so pass one could write the file that pass two would load as authoritative instructions.

A failed validation between the passes launched the second one, which means the validation failure itself delivered the poisoned instructions. OpenAI treated the sandbox as behaving as documented, so there is no CVE and no version to upgrade. Per Novee it fixed its own repository about three days after the report: separate jobs, no checkout, a read-only sandbox for Codex steps, and AGENTS.md documented as untrusted input.

The three agents side by side

The table puts the three failures in one frame. The column worth studying is the third. None of these was a model refusing badly. Each was a deterministic piece of harness code trusting a value it had no reason to trust.

AgentPublic triggerAssumption that failedIdentifier and fix
Claude Code ActionAn @claude comment in tag mode, the defaultQuoted text is safe to skip; a hostname on the allowlist means every path is safeCVE-2026-54316, fixed in Claude Code 2.1.163
Gemini CLI and run-gemini-cliAny user opening an issue, with the agent in yolo modeThe allowlist is enforced; the parent process is out of reach of the childCVE-2026-12537, fixed in gemini-cli 0.39.1 and run-gemini-cli 0.1.22
OpenAI CodexIssues opened or labelled, any author allowedA file the first pass writes is not input to the second passNo CVE; workflow hardening in openai/codex

A note on scoring. Anthropic rated CVE-2026-54316 at CVSS v4 6.0, Moderate, while the NVD record carries a CVSS v3.1 score of 9.1, Critical, from nvd@nist.gov. The two numbers describe the same flaw under different methods and different views of how hard it is to get untrusted content into the context. Use the severity to rank your own patch queue by what the workflow can reach, not by whichever score you saw first.

The trade-off: a patched agent in an unchanged workflow

The first failure mode is the one the Novee write-up flags for Codex: the fix did not reach downstream copies of the workflow. A vendor can harden its own repository in three days, but a copy of that YAML in yours stays exactly as it was. Upgrading the action closes the specific CVE and leaves the shape of the workflow, a public trigger feeding a write-capable agent, intact.

The second is the cost of the fix. Google shipped a breaking change to the trust model for headless runs rather than a narrow patch, and the advisory warns of silent failures. If you bump the version and the workflow starts doing nothing, the temptation is to set GEMINI_TRUST_WORKSPACE to true everywhere to make it green again. That restores the original exposure. Set it only where every input is from a trusted collaborator.

The third is the weak mitigation. Anthropic's guidance says that with allowed_non_write_users the scrub of secrets from subprocess environments is best effort and reduces rather than eliminates risk, and that a tool allowlist may not eliminate token-recovery risk either. Controls inside the agent are a second layer. The first layer has to be outside it.

Per the CSA note, Google removed three ADK workflows (issue-analyze.yml, issue-fix.yml, pr-analyze.yml) within days of the disclosure. If the team that wrote an agent deleted a workflow rather than patch it, deleting one of yours is a legitimate option.

A workflow that survives the same issue

The structure below is my synthesis of Anthropic's security guidance and GitHub's secure-use reference, not a vendor template. It does four things: a read-only default token, a trigger gated on who is commenting, an untrusted value handed to the shell through an environment variable instead of expression interpolation, and a tool allowlist narrow enough to be boring.

# .github/workflows/claude-triage.yml
name: claude-triage
on:
  issue_comment:
    types: [created]

# Read-only default. Grant more per job only when a step proves it needs it.
permissions:
  contents: read

jobs:
  triage:
    # The action also checks that the commenter has write access. This gate
    # means a stranger's comment never starts a job that holds the model key.
    if: >-
      contains(github.event.comment.body, '@claude') &&
      contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'),
               github.event.comment.author_association)
    runs-on: ubuntu-latest
    permissions:
      contents: read
      issues: write          # comment and label only. No contents: write.
    steps:
      # Wrong: expression interpolation pastes issue text into the script.
      #   run: echo "Title is ${{ github.event.issue.title }}"
      # Right: hand it over as data through an environment variable.
      - name: Log the title safely
        env:
          ISSUE_TITLE: ${{ github.event.issue.title }}
        run: printf 'Triaging: %s\n' "$ISSUE_TITLE"

      # Pin to a full-length SHA from the action's own repository, not a tag.
      - uses: anthropics/claude-code-action@<full-length-commit-sha>
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          # The job-scoped token expires with the job. Never a personal token.
          github_token: ${{ secrets.GITHUB_TOKEN }}
          # Narrow enough to be boring: read the issue, nothing else.
          claude_args: '--allowedTools "Bash(gh issue view:*)"'

# Do not add allowed_non_write_users or allowed_bots unless the job's
# permissions are limited to something like issues: write.

Pin the action to a full-length commit SHA taken from the action's own repository, because GitHub notes that a tag can be moved or deleted if the repository is compromised. Pass the short-lived GITHUB_TOKEN rather than a personal access token, because Anthropic says a static token does not rotate and could be recovered over time through prompt injection.

Split the pipeline into two jobs. The first reads the issue with no secrets and no write permission and emits a plain-text summary as an artifact. The second, with the model key, consumes only that summary in a clean checkout. This is the same separation OpenAI applied to Codex, and it also answers the shared-workspace problem.

What to do on Monday

Work through these in order. The first two take an hour and cover most of the exposure.

  1. Inventory every workflow that runs a coding agent and note its trigger. Anything fired by issues, issue_comment, pull_request_target or workflow_run on a public repository is in scope. The audit command below finds the risky flags.
  2. Upgrade: Claude Code to 2.1.163 or later, gemini-cli to 0.39.1 or later, run-gemini-cli to 0.1.22 or later. If a workflow pins gemini_cli_version, the advisory says to move it to a patched version and audit the workflow settings.
  3. Rotate. The CSA note recommends rotating secrets, GITHUB_TOKEN scopes and model API keys reachable from any workflow that ran in a vulnerable configuration before patching. Treat any key that was in scope as possibly read.
  4. Cut the blast radius: contents read as the default permission, no personal access tokens, bots and non-write users off, yolo or auto-approve modes off for anything reachable by low-privilege input, and an outbound allowlist reviewed like a firewall rule.
  5. Add detection. CSA suggests monitoring for suspicious access such as processes reading /proc/*/environ, and workflow logs on public repositories deserve a look for anything that should never have been printed.
# 1. Which workflows can a stranger trigger, and which flags widen trust?
grep -rEn "issues:|issue_comment|pull_request_target|workflow_run" .github/workflows
grep -rEn "allowed_non_write_users|allowed_bots|yolo|allow-users|GEMINI_TRUST_WORKSPACE" \
  .github/workflows

# 2. Which agent versions are pinned? Compare against the fixed releases:
#    Claude Code 2.1.163, gemini-cli 0.39.1, run-gemini-cli 0.1.22.
grep -rEn "gemini_cli_version|claude-code-action@|run-gemini-cli@" .github/workflows

# 3. Are any actions pinned to a movable tag instead of a 40-character SHA?
grep -rEn "uses: .*@(v[0-9]|main|master)" .github/workflows

# 4. Set the repository default token to read-only (organisation admins can
#    do the same at org level). Needs a token with repo administration rights.
gh api -X PUT repos/OWNER/REPO/actions/permissions/workflow \
  -f default_workflow_permissions=read

If the agent genuinely needs to answer strangers, such as triaging public issues, give it a GITHUB_TOKEN that can only label and comment, no secret beyond its own model key, and a separate repository or environment so a leak costs you that key and nothing else.

The rule I take from this: an agent reading public text and an agent holding secrets must never be the same process. Every one of the three bugs was a different way of letting them become the same process. Patch the versions today, then redesign the workflow so the next handoff bug has nothing to steal.

FAQ

Frequently Asked Questions

Short answers to what readers ask most about this topic.

Can a GitHub issue really leak CI secrets through an AI coding agent?

Per the August 2026 Novee Security disclosure, yes: an issue from an account with no repository privileges reached CI runner secrets in the vendors' own repositories. The agents ran in default workflows that fired on public input. The weakness was in how the harness trusted values, not in the model alone.

Which versions fix the Claude Code and Gemini CLI flaws?

Claude Code 2.1.163 fixes CVE-2026-54316, which affected 0.2.54 up to that release. For Gemini CLI, GHSA-wpqr-6v78-jr5g is fixed in gemini-cli 0.39.1 (and 0.40.0-preview.3) and in run-gemini-cli 0.1.22. Codex has no CVE and no version fix.

Why does CVE-2026-54316 have two different severity scores?

Anthropic rated it CVSS v4 6.0, Moderate, while NVD lists CVSS v3.1 9.1, Critical. The CSA note attributes the gap to scoring methodology and to differing views on how hard it is to get untrusted content into a Claude Code context. Rank your patching by what your workflow can reach.

Is upgrading the agent enough to be safe?

No. Upgrading closes the specific CVE but not the pattern of a public trigger feeding a write-capable agent that holds secrets. Vendor fixes also do not reach downstream copies of their workflows. Also reduce GITHUB_TOKEN permissions, split untrusted input from privileged jobs and rotate secrets.

What should I change in my GitHub Actions workflows this week?

Inventory workflows that run coding agents on issues, comments or pull_request_target, then upgrade the agents and rotate reachable secrets. Set the default token to contents read, avoid personal access tokens, and turn off yolo or auto-approve modes for low-privilege input. Pin actions to full commit SHAs.