Gemini CLI GitHub Action Review
Overview
google-github-actions/run-gemini-cli runs gemini --yolo (all tool calls auto-approved; as of the action and CLI versions reviewed) inside a GitHub Actions job. Whatever text reaches the prompt (issue titles/bodies, comments, PR descriptions, file contents) is attacker-controlled unless proven otherwise, and the job usually holds a GitHub write token and often a GCP access token. Reviewing these workflows is therefore a trust-boundary exercise: who can reach the agent, what can the agent do once reached, and what credentials are lying around when it does.
This skill packages a repeatable review, a static scanner, a run-history scanner, and a routing regression-test template, distilled from a real remediation (see the case study).
Prerequisites: python3 with PyYAML, gh (authenticated) and jq, and Node.js for the routing test.
Bundled Resources
scripts/audit-gemini-workflows.py— Static scanner. Point it at a repo (or a workflows directory); it reports findings G1–G13, indexed in the checklist. Exit 1 on HIGH/CRITICAL, 2 on unparsed files;--jsonfor machines. It is a heuristic (atrustedmention in a jobif:is not proof the condition is right) and never replaces the routing test.scripts/scan-run-history.sh— Read-onlyghsweep of past runs: finds privileged Gemini jobs (invoke/review) that succeeded for event types outsiders can trigger, and shows the triggering account's repo permission. Use to answer "has this already been exploited?". It only covers the most recentLIMITruns, makes a few API calls per run, and exits 2 if any lookup failed (incomplete result).assets/dispatch-routing-test.py— Template harness that extracts a dispatch workflow's command-routing script and asserts a matrix of events x author associations x bodies. Adapt the CONFIG block; always run it against the old workflow as a negative control.- Threat Model & Checklist (
references/threat-model-and-checklist.md) — The full review checklist with rationale and fix snippets. - Verifying Gemini CLI Settings (
references/verifying-cli-settings.md) — How to prove which settings keys a given CLI version actually reads (the flatcoreToolstrap). - Case Study (
references/case-study.md) — Anonymized account of an exploited dispatch routing flaw and the layered fix.
Workflow
Step 1: Inventory
./scripts/audit-gemini-workflows.py /path/to/repoList every workflow that uses run-gemini-cli and every workflow that calls it via uses: ./.github/workflows/.... The triggers of the caller decide who can reach the agent.
Step 2: Walk the trust chain (the order matters)
- Reach — Which events can start it?
issues(opened/reopened),issue_comment,pull_request_review*, andpull_request_targetare reachable by any GitHub user unless gated. A gate on the comment path does not cover a separateissuesbranch. - Routing — If a script parses
@gemini-cli ...commands out of event text, the parse order and the author check must make it impossible for an untrusted author to select a privileged command. Classify new issues/PRs before honouring any command prefix, and make the command parser depend on the author check. - Capability — Check the
settingsJSON: nested"tools": {"core": [...]}allowlist,"experimental": {"enableAgents": false},"tools": {"exclude": [...]}. The legacy flat"coreTools"key was not enforced in the 0.66.0 setup we observed; newer versions may migrate or read legacy keys, so verify behaviourally per version (see references). - Credentials —
permissions:(id-token: write, issues/PR write), WIF provider + service-account roles,gha-creds-*.jsonwritten into the workspace,GOOGLE_CLOUD_ACCESS_TOKENin the CLI environment. Untrusted-text jobs (triage) should getGITHUB_TOKEN: ''and the narrowest cloud identity. - Human gate — A prompt that says "wait for /approve" is not a control. Use a GitHub Environment with required reviewers on the job that holds credentials.
- Drift — Pin
gemini_cli_versionand the action by SHA. An unpinnedlatestsilently changes settings schema, trust prompts and defaults.
Step 3: Check whether it was already exploited
./scripts/scan-run-history.sh OWNER/REPO gemini-dispatch.yml 200Look for successful privileged jobs triggered by accounts with no repo permission. If found: preserve run logs and issue edit history (attackers edit bodies afterwards), assume the cloud identity and any job secrets were exposed, audit that identity's activity since the first run, and coordinate with your security team before public write-ups.
Step 4: Fix in layers (do not rely on one)
- Route untrusted issue authors to triage only; gate
invoke/reviewon a computedtrustedoutput (OWNER/MEMBER/COLLABORATOR or manual dispatch). - Enforce the allowlist with
tools.core; disable subagents; exclude web tools; dropcat/grep/head/tailfrom agents that don't need file reads (they can read credential files). - Put the credentialed job in an Environment with required reviewers.
- Reduce the cloud identity (dedicated service account,
roles/aiplatform.user, WIF attribute conditions on repo and workflow ref). - Pin CLI version and action SHA; set
GEMINI_CLI_TRUST_WORKSPACEdeliberately (trueonly where the workspace is empty or trusted;falsemay make headless runs exit 1 in the versions reviewed).
Step 5: Prove it
- Run the routing harness (needs Node and PyYAML) and expect 0 UNSAFE routes on the fix and a non-zero UNSAFE count on the old workflow (negative control):
./assets/dispatch-routing-test.py .github/workflows/gemini-dispatch.yml- Re-run
./scripts/audit-gemini-workflows.pyand confirm findings dropped. - Canary each workflow after merging (triage, review, invoke behind the approval gate): previously-unenforced allowlists become enforced and can break agents that quietly depended on a tool.
- Live negative test: on a test repo or fork (and after telling your security team), have a non-collaborator account open an issue starting with
@gemini-cli; only triage should run and no privileged job or cloud login should appear.
Pitfalls
- Fixing the comment gate but not the
issuesbranch (or the reverse). - A maintainer's
@gemini-clicomment on an outsider's issue still feeds that outsider's text to the agent; capability and credential limits are what protect you there. - Skipped steps hide breakage: a Gemini step guarded by "has work to do" can be broken for weeks and only fail on the first real input.
- Pushing or merging changes under
.github/workflows/requires a token with theworkflowscope; otherwise merge in the web UI.