Headless Mode & CI/CD
Gemini CLI was built for the terminal — but it also runs perfectly without one. The --no-interactive flag turns Gemini into a scriptable backend: pipe diffs into it, capture structured JSON back out, wire it into GitHub Actions, Windows Task Scheduler, or pre-commit hooks. This module shows you every pattern for automating Gemini outside of an interactive session.
What You'll Learn
- Use
-pand--no-interactiveto run Gemini from scripts and pipelines - Pipe stdin into Gemini for diff review, log analysis, and data processing
- Parse
--output-format jsonresults with PowerShell andjq - Set up a complete GitHub Actions PR review workflow using the official action
- Build nightly audit scripts, pre-commit hooks, and file-watch automations in PowerShell
- Use
--yolosafely in trusted pipelines and know when it is dangerous
What Headless Mode Is
Before: Imagine a skilled inspector who reviews parts on an assembly line — but only when they're standing at their workstation, looking at a screen, waiting to respond to prompts. The inspector is brilliant, but the line can only run when they're present.
Pain: Interactive AI tools have the same constraint: they require a human in the loop at every step. That works during development, but not at 2 AM during a nightly build, in a pull request triggered by a bot, or in a scheduled security audit that runs every Sunday.
Mapping: Headless modeRunning Gemini CLI with --no-interactive (or -p flag alone) so it reads a prompt, executes, writes output to stdout, and exits — no terminal UI, no interactive prompts, suitable for scripts and CI pipelines. removes the workstation. Gemini reads its instructions from a flag or stdin, does its work, writes structured output to stdout, and exits with a machine-readable return code. The inspector is now a robot that runs 24/7 without supervision.
Headless mode is activated by the -p "prompt" shorthand or the --no-interactive flag (or both). In this mode Gemini CLI skips the REPL loop, does not render the terminal UI, reads any additional input from stdin if provided, executes the prompt, writes output to stdout, and exits with code 0 on success or non-zero on error. The --output-format json flag further wraps the response in machine-parseable JSON. The --yolo flag auto-approves all tool calls (file writes, shell commands) without asking.
Key Flags Reference
These four flags cover 90% of headless automation scenarios.
| Flag | Effect | Typical Use |
|---|---|---|
| -p "prompt" | Pass the prompt inline; implies headless if no terminal is attached | Scripting, cron, pipelines |
| --no-interactive | Explicitly disable the interactive REPL; exit after one turn | CI jobs, pre-commit hooks |
| --output-format json | Wrap response in JSON: {"response":"...","model":"..."} |
Downstream parsing, dashboards |
| --yolo | Auto-approve all tool calls (file writes, shell execs) without prompting | Trusted pipelines only — see --yolo section |
| --model gemini-2.0-flash | Override the default model; Flash is faster and cheaper for CI tasks | Cost-sensitive pipelines |
# Basic: one-shot prompt, exit immediately
gemini -p "Explain what this function does" --no-interactive
# JSON output for downstream parsing
gemini -p "Audit this code for SQL injection" --output-format json --no-interactive
# Specify a faster model to reduce CI cost
gemini -p "Write a changelog entry" --model gemini-2.0-flash --no-interactive
# Auto-approve all tool calls (write files, run shell commands)
# Use ONLY in trusted, isolated environments
gemini -p "Add JSDoc to all exported functions" --yolo --no-interactive
# Basic: one-shot prompt, exit immediately
gemini -p "Explain what this function does" --no-interactive
# JSON output for downstream parsing
gemini -p "Audit this code for SQL injection" --output-format json --no-interactive
# Specify a faster model to reduce CI cost
gemini -p "Write a changelog entry" --model gemini-2.0-flash --no-interactive
# Auto-approve all tool calls (write files, run shell commands)
# Use ONLY in trusted, isolated environments
gemini -p "Add JSDoc to all exported functions" --yolo --no-interactive
$(…) or $( ) subshell syntax, or check $LASTEXITCODE / $? in your CI logic.--no-interactive, if Gemini detects a TTY it will start the interactive REPL. Always add the flag in scripts to avoid your pipeline hanging forever waiting for keyboard input.What Just Happened?
The animation shows the complete headless flow: a trigger (CI push, cron, or manual script) invokes Gemini with a structured prompt. Gemini runs, exits, and returns JSON on stdout. A downstream consumer (script, dashboard, comment bot) reads that JSON and takes action — all without a human in the loop.
Reading from stdin — Pipe-Friendly Patterns
Headless mode reads stdin automatically when it is piped. This means you can feed Gemini any data your shell can produce: git diffs, log files, file contents, command output, or CSV data. Gemini reads the piped content as context for the -p prompt.
Common Pipe Patterns
# Pattern 1: Review a diff for security issues
Get-Content changes.diff | gemini -p "Review this diff for security issues. List each issue with severity (HIGH/MEDIUM/LOW) and line number." --no-interactive
# Pattern 2: Auto-generate a conventional commit message
git diff HEAD~1 | gemini -p "Generate a conventional commit message for these changes. Format: type(scope): description" --no-interactive
# Pattern 3: Root-cause an error log
Get-Content error.log | gemini -p "Root cause this error log. Identify the primary exception, likely cause, and suggest a fix." --no-interactive
# Pattern 4: Summarize a large file before editing
Get-Content src/legacy-module.py -Raw | gemini -p "Summarize what this module does, its public API, and any obvious technical debt." --no-interactive
# Pattern 5: Capture output to a variable for processing
$suggestion = git diff HEAD~1 | gemini -p "Write a one-line summary of these changes" --no-interactive
Write-Host "Gemini says: $suggestion"
# Pattern 1: Review a diff for security issues
git diff HEAD~1 | gemini -p "Review this diff for security issues. List each issue with severity (HIGH/MEDIUM/LOW) and line number." --no-interactive
# Pattern 2: Auto-generate a conventional commit message
git diff HEAD~1 | gemini -p "Generate a conventional commit message for these changes. Format: type(scope): description" --no-interactive
# Pattern 3: Root-cause an error log
cat error.log | gemini -p "Root cause this error log. Identify the primary exception, likely cause, and suggest a fix." --no-interactive
# Pattern 4: Summarize a large file before editing
cat src/legacy-module.py | gemini -p "Summarize what this module does, its public API, and any obvious technical debt." --no-interactive
# Pattern 5: Capture output to a variable for processing
suggestion=$(git diff HEAD~1 | gemini -p "Write a one-line summary of these changes" --no-interactive)
echo "Gemini says: $suggestion"
@file reference syntax when the content is dynamically generated at runtime. A git diff does not exist as a named file — it is computed on demand and piped straight through.@path/to/file syntax in the prompt instead of piping, as that triggers file reading via the tool API which handles large files more efficiently.Gemini exits with code 0 on success and non-zero on error. In PowerShell, check $LASTEXITCODE. In bash, check $?. You can also instruct Gemini in the prompt to return a specific marker word ("FAIL" / "PASS") and parse stdout for it — which is more reliable than hoping a soft-quality judgment maps to an exit code.
--output-format json.JSON Output for Programmatic Processing
Plain text output is great for humans but painful for automation. The --output-format json flag wraps Gemini's response in a structured JSON envelope that your scripts can reliably parse.
The JSON envelope looks like: {"response": "...", "model": "gemini-2.0-flash", "tokenCount": 342}. The response field contains the model's text output. If you also instruct Gemini (in the prompt) to return JSON inside its response, you get double-encoded JSON — unwrap the outer envelope first, then parse the inner response.
Parsing Results: PowerShell and jq
# ── STEP 1: Run Gemini with JSON output ──────────────────────────────────
# The prompt instructs Gemini to return its own JSON inside the response.
# We ask for a list of security findings.
$prompt = @"
Review the following diff for security vulnerabilities.
Return ONLY valid JSON in this exact shape:
{
"findings": [
{"severity": "HIGH|MEDIUM|LOW", "line": 42, "description": "..."}
],
"summary": "one-sentence overall assessment"
}
"@
$rawOutput = git diff HEAD~1 | gemini -p $prompt --output-format json --no-interactive
# ── STEP 2: Unwrap the outer envelope ────────────────────────────────────
# $rawOutput is {"response": "{...inner JSON...}", "model": "..."}
$envelope = $rawOutput | ConvertFrom-Json
$innerJson = $envelope.response
# ── STEP 3: Parse the inner response JSON ────────────────────────────────
$findings = $innerJson | ConvertFrom-Json
# ── STEP 4: Act on the findings ──────────────────────────────────────────
$highCount = ($findings.findings | Where-Object { $_.severity -eq "HIGH" }).Count
if ($highCount -gt 0) {
Write-Error "Security gate failed: $highCount HIGH severity findings detected"
Write-Host ($findings.findings | ConvertTo-Json -Depth 5)
exit 1
} else {
Write-Host "Security gate passed. Summary: $($findings.summary)"
exit 0
}
#!/usr/bin/env bash
# ── STEP 1: Run Gemini with JSON output ──────────────────────────────────
PROMPT='Review the following diff for security vulnerabilities.
Return ONLY valid JSON:
{"findings":[{"severity":"HIGH|MEDIUM|LOW","line":42,"description":"..."}],"summary":"..."}'
RAW_OUTPUT=$(git diff HEAD~1 | gemini -p "$PROMPT" --output-format json --no-interactive)
# ── STEP 2: Unwrap the outer envelope and parse inner JSON ───────────────
# jq first extracts .response, then parses that string as JSON
INNER=$(echo "$RAW_OUTPUT" | jq -r '.response')
HIGH_COUNT=$(echo "$INNER" | jq '[.findings[] | select(.severity=="HIGH")] | length')
# ── STEP 3: Gate on findings ──────────────────────────────────────────────
if [ "$HIGH_COUNT" -gt 0 ]; then
echo "Security gate FAILED: $HIGH_COUNT HIGH severity findings"
echo "$INNER" | jq '.findings[] | select(.severity=="HIGH")'
exit 1
else
SUMMARY=$(echo "$INNER" | jq -r '.summary')
echo "Security gate passed. $SUMMARY"
exit 0
fi
ConvertFrom-Json / jq to get the envelope, then a second parse on .response — is the standard pattern when you need structured data from Gemini.$innerJson = $innerJson -replace '```json\s*|\s*```',''; in bash, INNER=$(echo "$INNER" | sed 's/^```json//;s/```$//').GitHub Actions Integration
The official google-github-actions/gemini-cli-action@v1 action bundles Gemini CLI, authenticates via Workload Identity, and exposes the same -p / --no-interactive interface you already know. The workflow below reviews every PR and posts findings as a GitHub comment.
PR Review Workflow
name: Gemini PR Review
on:
pull_request:
types: [opened, synchronize]
permissions:
contents: read # read the diff
pull-requests: write # post comments
jobs:
review:
runs-on: ubuntu-latest
steps:
# ── 1. Check out the PR branch ──────────────────────────────────────
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # full history so we can diff against base
# ── 2. Install and authenticate Gemini CLI ──────────────────────────
- name: Set up Gemini CLI
uses: google-github-actions/gemini-cli-action@v1
with:
# Store your Gemini API key as a GitHub secret
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
# ── 3. Run the diff review ──────────────────────────────────────────
- name: Review PR diff
id: review
run: |
DIFF=$(git diff origin/${{ github.base_ref }}...HEAD)
PROMPT="You are a senior code reviewer. Review the following PR diff.
Return a JSON object:
{
\"summary\": \"one paragraph\",
\"issues\": [{\"severity\":\"HIGH|MEDIUM|LOW\",\"file\":\"...\",\"line\":1,\"message\":\"...\"}],
\"approvalRecommendation\": \"APPROVE|REQUEST_CHANGES|COMMENT\"
}
Diff:
$DIFF"
RESULT=$(echo "" | gemini -p "$PROMPT" --output-format json --model gemini-2.0-flash --no-interactive)
# Strip outer envelope
INNER=$(echo "$RESULT" | jq -r '.response' | sed 's/^```json//;s/```$//')
echo "review_json=$INNER" >> $GITHUB_OUTPUT
# ── 4. Format and post the comment ──────────────────────────────────
- name: Post review comment
uses: actions/github-script@v7
with:
script: |
const review = JSON.parse(`${{ steps.review.outputs.review_json }}`);
let body = `## Gemini Code Review\n\n**Recommendation: \`${review.approvalRecommendation}\`**\n\n${review.summary}\n\n`;
if (review.issues.length > 0) {
body += `### Issues Found\n\n`;
for (const issue of review.issues) {
const emoji = issue.severity === 'HIGH' ? '🔴' : issue.severity === 'MEDIUM' ? '🟡' : '🟢';
body += `${emoji} **${issue.severity}** — \`${issue.file}:${issue.line}\` — ${issue.message}\n\n`;
}
} else {
body += `No issues found. ✅\n`;
}
body += `\n*Reviewed by Gemini CLI ${new Date().toISOString()}*`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body
});
fetch-depth: 0 is critical — without full history, git diff origin/main...HEAD fails because the merge-base commit is not available in a shallow clone.GEMINI_API_KEY directly in workflow files. Always use ${{ secrets.GEMINI_API_KEY }}. Add the secret via Settings → Secrets → Actions in your GitHub repo.A team of 10 engineers with 5 PRs per day per person means 50 reviews weekly. With this workflow, every PR gets a Gemini first-pass in under 60 seconds, catching obvious issues (SQL injection, hardcoded secrets, missing null checks) before a human reviewer even opens the diff. The average cost per review at Gemini Flash pricing is under $0.01.
PowerShell Automation Scripts
Headless Gemini is a first-class PowerShell citizen. The patterns below cover the three most impactful Windows automation scenarios: nightly dependency audits, commit message quality gates, and automatic test generation.
Nightly Dependency Audit
#!/usr/bin/env pwsh
# Nightly dependency security audit — runs via Windows Task Scheduler
# Saves a report to C:\Reports\gemini-audit-YYYY-MM-DD.json
param(
[string]$ReportDir = "C:\Reports",
[string]$EmailTo = "" # optional: address for alert emails
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$Today = Get-Date -Format "yyyy-MM-dd"
# ── 1. Ensure report directory exists ──────────────────────────────────
New-Item -ItemType Directory -Force -Path $ReportDir | Out-Null
# ── 2. Gather package manifests from current working directory ─────────
$manifests = @()
if (Test-Path "package.json") { $manifests += "package.json" }
if (Test-Path "requirements.txt") { $manifests += "requirements.txt" }
if (Test-Path "pyproject.toml") { $manifests += "pyproject.toml" }
if (Test-Path "Gemfile") { $manifests += "Gemfile" }
if ($manifests.Count -eq 0) {
Write-Warning "No package manifests found in $(Get-Location)"
exit 0
}
$combinedContent = ""
foreach ($file in $manifests) {
$combinedContent += "=== $file ===`n"
$combinedContent += (Get-Content $file -Raw)
$combinedContent += "`n`n"
}
# ── 3. Run Gemini audit ────────────────────────────────────────────────
$prompt = @"
You are a security auditor. Review the following package manifests for:
1. Packages with known CVEs published in the last 90 days
2. Packages pinned to versions older than 1 year with active security advisories
3. Packages with unusual version patterns suggesting supply chain risk
Return ONLY this JSON (no markdown fences):
{
"auditDate": "$Today",
"highRisk": [{"package":"","version":"","reason":"","cve":""}],
"mediumRisk": [{"package":"","version":"","reason":""}],
"summary": "",
"upgradeCount": 0
}
Manifests:
$combinedContent
"@
Write-Host "[$Today] Running Gemini dependency audit..."
$raw = ($combinedContent | gemini -p $prompt --output-format json --model gemini-2.0-flash --no-interactive)
$envelope = $raw | ConvertFrom-Json
$innerText = $envelope.response -replace '```json\s*','' -replace '\s*```',''
$report = $innerText | ConvertFrom-Json
# ── 4. Save report ────────────────────────────────────────────────────
$reportPath = Join-Path $ReportDir "gemini-audit-$Today.json"
$report | ConvertTo-Json -Depth 10 | Out-File -FilePath $reportPath -Encoding UTF8
Write-Host "Report saved: $reportPath"
# ── 5. Alert on high-risk findings ────────────────────────────────────
if ($report.highRisk.Count -gt 0) {
Write-Warning "HIGH RISK: $($report.highRisk.Count) package(s) require immediate attention"
$report.highRisk | ForEach-Object { Write-Warning " - $($_.package) $($_.version): $($_.reason)" }
if ($EmailTo) {
# Requires SMTP relay configured in Windows — adjust server/port as needed
Send-MailMessage -To $EmailTo -From "gemini-audit@$(hostname)" `
-Subject "[$Today] Gemini Dependency Audit: $($report.highRisk.Count) HIGH risk findings" `
-Body $report.summary `
-SmtpServer "localhost"
}
exit 1 # non-zero exit triggers Task Scheduler "on failure" action
}
Write-Host "Audit complete. $($report.upgradeCount) upgrade(s) recommended. No HIGH risk findings."
exit 0
Installing as a Windows Scheduled Task
# Run once as Administrator to register the nightly audit task
$scriptPath = "$PSScriptRoot\scripts\nightly-audit.ps1"
$action = New-ScheduledTaskAction -Execute "pwsh.exe" -Argument "-NonInteractive -File `"$scriptPath`""
$trigger = New-ScheduledTaskTrigger -Daily -At "02:00AM"
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 10) -RunOnlyIfNetworkAvailable
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest
Register-ScheduledTask `
-TaskName "GeminiNightlyAudit" `
-Action $action `
-Trigger $trigger `
-Settings $settings `
-Principal $principal `
-Description "Nightly Gemini CLI dependency security audit" `
-Force
Write-Host "Scheduled task registered. Next run: 02:00 AM daily."
Register-ScheduledTask creates a Windows Task Scheduler entry. Running under SYSTEM avoids user-session dependency — the audit fires even when no one is logged in.GEMINI_API_KEY as a system-wide environment variable (setx /M GEMINI_API_KEY "your-key") or store it in a secrets manager and read it inside the script before calling Gemini.--yolo.--yolo Mode: Auto-Approval for Tool Calls
By default, Gemini pauses before every file write or shell command execution and asks for your confirmation. The --yolo flag--yolo disables all confirmation prompts for tool calls. Gemini will write files, execute shell commands, and install packages without asking. Use only in isolated, trusted environments. Never use in interactive sessions on a development machine. removes all those confirmation prompts — Gemini writes files and runs shell commands immediately and silently.
--yolo means Gemini can overwrite any file it has access to and execute any shell command without asking. On a developer workstation, a misbehaving prompt could delete source files, push to git, or exfiltrate secrets to a logging endpoint. Use it only in disposable, sandboxed CI containers where the worst case is a failed build — not a corrupted local repo.
When to Use and When NOT to Use
| Scenario | Use --yolo? | Reason |
|---|---|---|
| GitHub Actions runner (ephemeral container) | SAFE | Container is destroyed after the job; no persistent damage possible |
Docker sandbox (--sandbox flag) |
SAFE | Isolated filesystem; host is protected |
| Nightly cron on a dedicated CI VM with no prod access | SAFE | VM is isolated and regularly reimaged |
| Developer laptop / workstation | DANGEROUS | Can overwrite local files, push git commits, or execute arbitrary commands |
| Production server or VM with live data | DANGEROUS | One bad prompt can corrupt the database, delete logs, or expose secrets |
| Any environment with credentials in env variables | DANGEROUS | A prompt injection could exfiltrate $env:GEMINI_API_KEY or AWS keys |
What Just Happened?
Left: in a CI container, --yolo is safe because the environment is ephemeral. Right: on a developer laptop, the same flag silently pushes commits and triggers deploys. The flag is identical — the environment is what determines safety.
Safe CI Workflow with --yolo
# Safe: auto-doc generation in GitHub Actions (ephemeral runner)
# This job runs on a runner that is destroyed after completion.
# --yolo is fine here — Gemini can safely write doc files.
Get-ChildItem src/ -Filter "*.ts" | ForEach-Object {
gemini -p "@$($_.FullName) Add JSDoc comments to all exported functions. Write the result back to the file." `
--yolo --no-interactive
}
# ANTI-PATTERN — DO NOT DO THIS on a developer machine:
# gemini -p "Refactor the entire codebase" --yolo --no-interactive
#!/usr/bin/env bash
# Safe: auto-doc generation in GitHub Actions (ephemeral runner)
for f in src/**/*.ts; do
gemini -p "@$f Add JSDoc comments to all exported functions. Write the result back to the file." \
--yolo --no-interactive
done
# ANTI-PATTERN — DO NOT DO THIS on a developer machine:
# gemini -p "Refactor the entire codebase" --yolo --no-interactive
Knowledge Check
1. Which flag is required to prevent Gemini CLI from starting an interactive REPL when called from a script?
2. When you pipe a git diff into Gemini headless mode, how does Gemini receive the piped content?
3. You run: gemini -p "..." --output-format json --no-interactive and get {"response":"{\"findings\":[...]}","model":"gemini-2.0-flash"}. What is the correct next step in PowerShell?
.findings on the parsed object
.response, then parse .response again as JSON
Select-String to extract the findings array with regex
--unwrap-json to avoid the double-encoded format
4. Which of these scenarios makes --yolo safe to use?
5. What does the fetch-depth: 0 option do in the GitHub Actions checkout step for the PR review workflow?
git diff origin/main...HEAD can find the merge-base
6. Why might Gemini's JSON response need a markdown fence strip before parsing, even when using --output-format json?
--output-format json flag only affects the outer envelope, not inner content
```json markdown fences inside the response field