The Two Root Causes
The attack was not the result of a single misconfiguration. It required two compounding design gaps to be present simultaneously.
Root cause one: headless mode auto-trusted the workspace. When gemini-cli runs in headless mode (as it does in CI), it automatically loaded any .gemini/ configuration it found in the workspace directory without review, sandboxing, or human approval. A malicious repository could plant a .gemini/settings.json that pre-authorized dangerous tool calls, and the agent would accept it without prompting.
Root cause two: --yolo mode ignored tool allowlists. The --yolo flag disables confirmation prompts for tool use. In the vulnerable versions, it also silently ignored the fine-grained tool allowlists defined in settings.json. This meant an operator could define a restricted set of tools, enable --yolo for automation, and believe they had constrained the agent, when in fact the allowlist was not being enforced at all.
The patch in version 0.39.1 addressed both: tool allowlisting is now enforced even under --yolo mode, and shell substitution and redirect injection techniques are blocked in command sanitization.
This Is Not a Gemini-Only Problem
The pattern exposed here is not specific to Google's tooling. Any workflow that follows this structure is vulnerable:
An AI agent is triggered by external input (issues, pull request comments, webhooks, chat messages).
The agent runs with access to CI secrets, tokens, or filesystem credentials.
The agent's tool use is not constrained by a server-side allowlist that cannot be overridden by prompt.
Researchers have documented the same pattern in Claude Code workflows, Codex-based triage bots, and third-party MCP server integrations. The Codex branch-name injection covered in an earlier post follows the same trifecta logic. So does the Clinejection attack against Cline's own GitHub Actions bot.
The trust model assumption: Most CI-integrated AI agents were designed with the assumption that the inputs they process come from trusted collaborators. That assumption is false for any repository that accepts public issues, pull requests, or comments. The agent's trust model must start from the assumption that any user-generated text is adversarial.
What the Vulnerable Workflow Looked Like
The original workflow that Google deployed for issue triage looked roughly like this:
name: Issue Triage
on:
issues:
types: [opened]
jobs:
triage:
runs-on: ubuntu-latest
permissions:
issues: write
contents: read
steps:
- uses: actions/checkout@v4
# persist-credentials defaults to true
- uses: google-gemini/run-gemini-cli-action@v1
with:
prompt: |
Triage the following issue: ${{ github.event.issue.body }}
yolo: true
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
Three problems are visible here:
persist-credentials is not explicitly set to false, so Git credentials land on the filesystem.
The issue body is interpolated directly into the prompt without sanitization.
yolo: true is set, which in the vulnerable version bypassed the tool allowlist entirely.
A hardened version of the same workflow looks like this:
name: Issue Triage (Hardened)
on:
issues:
types: [opened]
jobs:
triage:
runs-on: ubuntu-latest
permissions:
issues: write
contents: read
# Do NOT grant actions:write or contents:write here
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false # No Git creds on filesystem
- name: Sanitize issue body
id: sanitize
run: |
# Strip shell metacharacters from issue body before passing to agent
SAFE_BODY=$(echo "${{ github.event.issue.body }}" | tr -d '`$(){}[]<>|&;\' | head -c 2000)
echo "safe_body=${SAFE_BODY}" >> $GITHUB_OUTPUT
- uses: google-gemini/run-gemini-cli-action@v1
with:
prompt: "Triagethisissue(labelandassignonly):${{steps.sanitize.outputs.safe_body}}"
# yolo: false by default; allowlist is enforced
settings: |
{
"tools": {
"allowed": ["github_add_label", "github_assign_issue"]
}
}
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GITHUB_TOKEN: '' # Explicit empty string prevents token inheritance
What LucidShark Enforces Before This Reaches Your Workflow
LucidShark operates as a pre-commit and MCP-layer gate. It cannot stop a misconfigured GitHub Actions workflow from running in your repository, but it can stop the patterns that enable these attacks from being introduced in the first place.
For teams using Claude Code with LucidShark's MCP integration, the following checks run automatically on every agentic commit:
Unsafe shell interpolation in CI definitions: LucidShark's SAST rules flag patterns where user-controlled inputs (issue bodies, PR titles, comment text) are interpolated directly into shell commands or AI prompts without sanitization in workflow YAML files.
# LucidShark flags this pattern:
prompt: "Reviewthis:${{github.event.issue.body}}"
# Requires explicit sanitization before agent consumption
Overpermissioned workflow tokens: The SCA module checks that workflow permissions blocks grant only the minimum required. Workflows that combine contents:write with agent execution steps that process external input are flagged for review.
Missing persist-credentials: false on agent-executing workflows: Any actions/checkout step followed by an AI agent execution step that lacks persist-credentials: false triggers a warning. The combination is the pattern that made lateral movement possible in the Gemini CLI attack.
Hardcoded or logged secrets near agent execution: LucidShark's secret scanning checks for API key patterns in workflow definitions, prompt templates, and MCP server configurations. Keys appearing near agent invocation points are prioritized.
MCP integration note: When LucidShark runs as an MCP server alongside Claude Code, these checks fire before each commit rather than in a CI loop. Issues are surfaced in your editor with the full context of the agentic session that introduced them, not as a decontextualized CI failure hours later.
The Broader Pattern: Agentic Input Trust
The Gemini CLI attack fits a pattern that is appearing across every AI coding tool: agents are given powerful capabilities and then pointed at untrusted inputs with insufficient guardrails on what those inputs can instruct the agent to do.
The fix is not to stop using agentic automation. It is to apply the same threat modeling to AI agent inputs that you already apply to web application inputs. Treat every issue body, pull request description, and external data source as adversarial until proven otherwise. Constrain what tools the agent can use server-side, not just through prompting. And never let an agent that processes external input run with write access to your main branch.
The CVSS 10 score on GHSA-wpqr-6v78-jr5g is not hyperbole. A single public GitHub issue, with nothing but text, was enough to push code to a repository with 101,000 stars. That is the threat model you are operating under today.
Install LucidShark today. LucidShark catches unsafe CI interpolation patterns, overpermissioned workflow tokens, and missing credential isolation before your agentic workflows commit them. It runs entirely on your machine with zero data sent to the cloud. Install in 30 seconds: npm install -g lucidshark and add it to your Claude Code MCP config. See the full setup guide at lucidshark.com.