The Cartographer · 11 min mission
Automation & CI: headless Gemini CLI and the GitHub Action
Script Gemini CLI headless and wire it into CI with the official GitHub Action.
On this page
Run Gemini CLI as a non-interactive Unix command: pass -p "..." or pipe data into it, and it reads stdin, writes the answer to stdout, and exits with a status code you can branch on. This guide covers the headless flags (-p, --output-format, --approval-mode, exit codes), scripting with jq, and the official google-github-actions/run-gemini-cli GitHub Action.
Trigger headless mode
Gemini runs non-interactively in exactly two cases: input or output is not a TTY (piped or redirected), or you pass a query with -p / --prompt. A bare positional query in a terminal (gemini "explain this project") answers and then continues interactively — only the pipe or the flag makes it one-shot. --prompt text is "Appended to stdin input if provided. Forces non-interactive mode," so piping a file and adding -p combines both into one request.
| Pattern | Command | Behavior |
|---|---|---|
| Direct prompt | gemini -p "Write a poem about TypeScript" | Runs once, prints to stdout, exits |
| Pipe a file (macOS/Linux) | cat error.log \| gemini -p "Explain why this failed" | stdin + prompt combined into one request |
| Pipe a file (PowerShell) | Get-Content error.log \| gemini -p "Explain why this failed" | Same as above on Windows |
| Pipe a command | git diff \| gemini -p "Write a commit message for these changes" | Feeds the diff plus the instruction |
| Pipe, no flag | cat logs.txt \| gemini | Piped stdin alone forces non-interactive; runs once and exits |
Machine-readable output: --output-format
--output-format (alias -o, default text) takes three values. For scripting, use json and extract the answer with jq -r '.response' — a plain string you can assign, test, or pipe onward without scraping prose. Use stream-json only when a step must consume tool calls and message chunks as they stream (e.g. a live CI log); for single-shot extraction, json + jq is simpler.
| Value | Shape | Use for |
|---|---|---|
text | Plain answer (default) | Human-readable stdout |
json | One object: response (string), stats (token/latency metrics), optional error | Scripted extraction via jq -r '.response' |
stream-json | Newline-delimited JSON (JSONL), one event per line, ending in result | Live progress / streaming CI logs |
ANSWER=$(gemini --output-format json -p "Summarize CHANGELOG.md in one line" | jq -r '.response')
echo "$ANSWER"| Event | Carries |
|---|---|
init | Session metadata — session ID and the model in use |
message | User and assistant message chunks as they stream |
tool_use | A tool-call request with its arguments |
tool_result | Output from an executed tool |
error | Non-fatal warnings and system errors |
result | Final outcome with aggregated stats and per-model token usage |
Exit codes
Branch your pipeline on $? (PowerShell: $LASTEXITCODE) instead of grepping stdout. The distinction matters in CI: a 42 means the invocation itself is wrong and a retry won't help, while a 1 may be a transient API blip worth one retry.
| Code | Meaning | What it tells you |
|---|---|---|
0 | Success | The answer on stdout is real; proceed |
1 | General error or API failure | Possibly transient — a single retry may be reasonable |
42 | Input error | Invalid prompt or arguments — fix the invocation, do not retry |
53 | Turn limit exceeded | The agent ran out of steps — narrow the task or raise the limit |
Approval modes for unattended runs
A non-interactive prompt still respects the approval model. By default, when the agent wants to edit a file or run a shell command it asks — and in CI there is no human to answer, so the tool call blocks and the run hangs. Pick an approval mode that won't stop to ask. Combine with --sandbox / -s to run in a sandboxed environment, and --skip-trust to clear the folder-trust prompt for the session.
| Flag / value | Effect | Status |
|---|---|---|
--approval-mode=default | Confirm each action | Default — blocks unattended runs |
--approval-mode=auto_edit | Auto-approve edits; still confirm shell commands | Current |
--approval-mode=yolo | Auto-approve everything | Current |
--approval-mode=plan | Read and propose only — never acts | Current (safe for read-only steps) |
--yolo / -y | Auto-approve all actions | Deprecated → use --approval-mode=yolo |
--allowed-tools | Allowlist specific tools | Deprecated → use the Policy Engine |
--sandbox / -s | Run in a sandboxed environment | Current |
--skip-trust | Trust the workspace for this session, skip the folder-trust check | Current |
The official GitHub Action: run-gemini-cli
google-github-actions/run-gemini-cli runs Gemini CLI inside GitHub Actions "both as an autonomous agent for critical routine coding tasks, and an on-demand collaborator." Launched August 6, 2025, still beta; latest release v0.1.22 (2026-04-24); requires Gemini CLI 0.1.18 or later. Workflows reference the major tag @v0; pin to a full SHA in production. The setup centers on the dispatch router (gemini-dispatch.yml): without it, the pre-built sub-workflows (Issue Triage, Pull Request Review, Gemini CLI Assistant) never fire.
Stand up PR review in CI
Scaffold the workflows
In an interactive session run
/setup-github. It writes the dispatch router and pre-built workflows into.github/workflows. Confirmgemini-dispatch.ymlis present — the rest depend on it. (You can copyexamples/workflowsby hand, but you must include the dispatcher.)Wire credentials
Add
GEMINI_API_KEYas a repository secret (or configure WIF for a credential-less setup). Add.gemini/andgha-creds-*.jsonto.gitignoreso nothing leaks into the repo.Set a real prompt
The
promptinput defaults to the literal stringYou are a helpful assistant.Override it — in the PR-review workflow that meansprompt: '/pr-code-review'— or the job runs and reviews nothing.Constrain what it can do
Pass a
settingsJSON string (written to.gemini/settings.json) and use the Policy Engine to allowlist exactly the tools the job needs — not--allowed-tools(deprecated), and avoid blanketyolowhere possible.Harden and pin
Pin
run-gemini-clito a full commit SHA, keep the dispatcher's author-association gate intact, prefer a custom GitHub App (actions/create-github-app-tokenfromAPP_ID+APP_PRIVATE_KEY), and enable branch protection before letting it loose on real PRs.
# .github/workflows/gemini-review.yml (excerpt)
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: 'google-github-actions/run-gemini-cli@v0'
with:
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
# prompt defaults to "You are a helpful assistant." — override it
# or the Action does nothing useful.
prompt: '/pr-code-review'
# settings is a JSON string written to .gemini/settings.json:
# use it to configure the Policy Engine / tool allowlists.
settings: '{ "tools": { "core": ["read_file", "run_shell_command"] } }'Dispatch routing and the security gate
The router reacts to pull_request, pull_request_review, pull_request_review_comment, issue_comment, and issues events. On demand, people drive it with @gemini-cli comments. The security gate exists because the agent holds credentials and can act — without it, a stranger's comment becomes a prompt-injection and credential-exposure vector. If you hand-roll a workflow instead of using the dispatcher, you must replicate these checks.
| Trigger | Routes to | Gate |
|---|---|---|
@gemini-cli /review | Code review | Body startsWith('@gemini-cli') AND author association in OWNER/MEMBER/COLLABORATOR |
@gemini-cli /triage | Issue triage | Same author-association gate |
@gemini-cli /approve | Approve path | Same author-association gate |
@gemini-cli <free text> | General invoke handler | Same author-association gate |
| PR opened | Auto code review | Only if pull_request.head.repo.fork == false |
Local scripting vs. the GitHub Action
Headless CLI (your shell / cron)
Manage auth via the GEMINI_API_KEY env var, choose --approval-mode and --output-format per invocation, branch on the exit code, and parse .response with jq. Total control — including any safety gates you must add yourself.
run-gemini-cli (in CI)
The dispatcher handles event routing and the OWNER/MEMBER/COLLABORATOR gate. Tool permissions go through the settings JSON / Policy Engine, auth prefers WIF (no long-lived keys), and behavior is wired through inputs like prompt and gemini_model rather than flags.
Knowledge check
You run `gemini -p "refactor utils.ts and run the tests"` inside a CI job. The step never finishes — it just hangs. The single most likely cause?
Reach the end and this star joins your charted sky.