⌂ Home
Gemini CLI: From Zero to Production
Track 5 · Power Usage
Module 17 of 17 ~50 min Advanced
Final Module Module 16 · Track 5 — Power Usage

Shell Automation

Gemini CLI's real multiplier isn't what it does in a single session — it's what it does unattended. PowerShell loops, Git hooks, file watchers, and scheduled tasks transform Gemini CLI from a conversational tool into a continuous background member of your team: auditing security while you sleep, generating tests as you write code, and crafting commit messages the moment you stage a diff.

What You'll Learn

  • Understand Gemini CLI's role as a shell orchestrator with --no-interactive and --yolo flags
  • Use --sandbox / --docker for isolated, safe tool execution
  • Write PowerShell batch scripts for security audits and auto-documentation
  • Write equivalent Bash scripts with jq for JSON output parsing
  • Build a pre-commit hook that auto-generates conventional commit messages
  • Create a PowerShell FileSystemWatcher that auto-generates tests for new source files
  • Schedule nightly and weekly Gemini CLI tasks with Windows Task Scheduler

Gemini CLI as a Shell Orchestrator

Analogy — The Night-Shift Developer Who Never Sleeps

A team of 5 developers ships code every day, but nobody has time to manually audit every file for security issues, document every new function, or review every PR at 3 AM. The pain: quality checks only happen when someone has spare capacity — which means they happen inconsistently, or not at all. The mapping: Gemini CLI running in shell automation mode is the developer who shows up for the night shift. It audits the codebase while you sleep, generates tests the instant a new file appears, and writes commit messages the moment you run git add. No fatigue, no weekends off.

The Key Flags for Automation

terminal — non-interactive mode flags
# Run a prompt non-interactively (no REPL, exits after response)
gemini -p "Your prompt here" --no-interactive

# Allow Gemini to execute shell commands without asking for confirmation
gemini -p "Your prompt here" --yolo --no-interactive

# Run all tool executions inside an isolated Docker container
gemini -p "Your prompt here" --sandbox --no-interactive

# Combine: sandboxed, non-interactive, quiet output
gemini -p "Your prompt here" --sandbox --no-interactive --quiet
WHAT: --no-interactive makes Gemini CLI behave like a Unix tool — read input, produce output, exit. GOTCHA: --yolo skips ALL tool call confirmations. Only use it in scripts where you've carefully designed the prompt to prevent unintended file mutations. In sandboxed mode, --yolo is safer because all execution is containerized.

Docker Sandbox Mode

Technical Definition — How --sandbox Works

When you pass --sandboxThe --sandbox flag (also --docker) runs all tool call executions — shell commands, file operations — inside an isolated Docker container spun up by Gemini CLI. The container mounts your current directory as read-write but has no access to your host network or other system resources unless explicitly granted. The container is destroyed after the session., Gemini CLI spins up a Docker container with your current directory mounted as a volume. Any shell commands the model executes run inside the container, not on your host system. This means a hallucinated rm -rf / inside the container destroys nothing on your machine.

Windows Setup for --sandbox

Sandbox mode requires Docker Desktop for Windows. Install it from docker.com/products/docker-desktop. The first time you run gemini --sandbox, Gemini CLI pulls the base image (~200 MB). Subsequent runs reuse the cached image. For best performance, use the WSL2 backend in Docker Desktop settings (Docker Desktop → Settings → General → "Use the WSL 2 based engine").

Animation — Sandbox Container Lifecycle
gemini --sandbox -p "audit *.py for OWASP issues"
waiting
Docker: pull gemini-cli-sandbox:latest (or use cache)
waiting
Docker: start container, mount ./project as /workspace
waiting
Gemini reads /workspace/*.py — all reads containerized
waiting
Model produces findings — no file mutations occurred
waiting
Docker: container destroyed, nothing persists outside /workspace
waiting
With the isolation model established, let's write real automation scripts — starting with the PowerShell patterns that are most useful on Windows developer machines.

PowerShell Automation Patterns

Pattern 1 — Batch Security Audit

# Batch OWASP Top 10 security audit across all Python files
# Outputs WARNING lines for HIGH or CRITICAL issues
$auditResults = @()

Get-ChildItem -Recurse -Filter "*.py" | ForEach-Object {
    $filePath = $_.FullName
    $fileName = $_.Name

    # Run Gemini non-interactively on each file
    $result = Get-Content $filePath -Raw |
        gemini -p "Review this code for OWASP Top 10 security issues.
For each issue found, output a line in this exact format:
SEVERITY:CATEGORY:LINE:DESCRIPTION
Use CRITICAL, HIGH, MEDIUM, or LOW for severity.
If no issues, output: CLEAN" --no-interactive 2>$null

    if ($result -match "HIGH|CRITICAL") {
        Write-Warning "Security issue in $fileName"
        $auditResults += [PSCustomObject]@{
            File     = $fileName
            Findings = $result
        }
    }
}

# Summary report
if ($auditResults.Count -gt 0) {
    Write-Host "`n=== SECURITY AUDIT FINDINGS ===" -ForegroundColor Red
    $auditResults | ForEach-Object {
        Write-Host "`nFile: $($_.File)" -ForegroundColor Yellow
        Write-Host $_.Findings
    }
    exit 1  # Non-zero exit for CI integration
} else {
    Write-Host "Security audit passed — no HIGH/CRITICAL issues found." -ForegroundColor Green
    exit 0
}
#!/usr/bin/env bash
# Batch OWASP Top 10 security audit
set -euo pipefail

FOUND_ISSUES=0

while IFS= read -r -d '' file; do
    filename=$(basename "$file")

    result=$(cat "$file" | gemini -p "Review this code for OWASP Top 10 security issues.
Output each issue as: SEVERITY:CATEGORY:LINE:DESCRIPTION
Use CRITICAL, HIGH, MEDIUM, or LOW. Output CLEAN if none." \
        --no-interactive 2>/dev/null)

    if echo "$result" | grep -qE "^(HIGH|CRITICAL):"; then
        echo "WARNING: Security issues in $filename" >&2
        echo "$result"
        FOUND_ISSUES=1
    fi
done < <(find . -name "*.py" -not -path "./.git/*" -print0)

if [ "$FOUND_ISSUES" -eq 1 ]; then
    echo "=== Audit complete: HIGH/CRITICAL issues found ===" >&2
    exit 1
else
    echo "Audit passed — no HIGH/CRITICAL issues found."
    exit 0
fi
WHAT: Both scripts iterate over every Python file, pipe the content to Gemini with a structured output prompt, then check for HIGH/CRITICAL severity lines. WHY: The structured output format (SEVERITY:CATEGORY:LINE:DESCRIPTION) makes the result machine-parseable. GOTCHA: This approach sends file content to the Gemini API — review your organization's data policies before running on proprietary source code.

Pattern 2 — Auto-Document TypeScript

# Auto-add JSDoc comments to all exported TypeScript functions
# --yolo: allow Gemini to write files without confirmation
# --sandbox: run in container for safety

Get-ChildItem src/ -Filter "*.ts" -Recurse | ForEach-Object {
    $filePath = $_.FullName

    Write-Host "Documenting: $($_.Name)"

    gemini -p "@$filePath
Add JSDoc comments to all exported functions and classes that don't already have them.
Rules:
- Preserve all existing code exactly
- Add @param, @returns, @throws tags with types
- Keep comments concise (1-2 sentences for @description)
- Do not add JSDoc to private functions
Write the updated file in place." `
        --yolo `
        --no-interactive `
        --sandbox 2>$null

    if ($LASTEXITCODE -eq 0) {
        Write-Host "  Done: $($_.Name)" -ForegroundColor Green
    } else {
        Write-Warning "  Failed: $($_.Name)"
    }
}
#!/usr/bin/env bash
# Auto-add JSDoc to TypeScript source files
set -euo pipefail

while IFS= read -r -d '' file; do
    echo "Documenting: $(basename "$file")"

    gemini -p "@${file}
Add JSDoc comments to all exported functions and classes without existing docs.
Rules: preserve all code, add @param/@returns/@throws, keep descriptions concise.
Write the updated file in place." \
        --yolo \
        --no-interactive \
        --sandbox 2>/dev/null

    if [ $? -eq 0 ]; then
        echo "  Done: $(basename "$file")"
    else
        echo "  Failed: $(basename "$file")" >&2
    fi
done < <(find src/ -name "*.ts" -not -name "*.d.ts" -print0)

Pre-Commit Hook: Auto Commit Messages

A pre-commit hookA Git hook script that runs before git commit completes. If the script exits with a non-zero code, the commit is aborted. Pre-commit hooks are stored in .git/hooks/pre-commit and run in the context of the repository root. that generates conventional commit messages is one of the highest-ROI automations you can add to a project. Staged diffs go in, a formatted commit message comes out — every time.

Animation — Pre-Commit Hook Execution Flow
git commit -m "wip" → pre-commit hook fires
waiting
Hook reads: git diff --cached (staged diff)
waiting
gemini -p "[diff]" → generates conventional commit message
waiting
Message written to .git/COMMIT_EDITMSG
waiting
git commit uses AI-generated message
waiting
# .git/hooks/prepare-commit-msg (PowerShell version)
# Save as: .git/hooks/prepare-commit-msg.ps1
# Then create a launcher: .git/hooks/prepare-commit-msg

param($COMMIT_MSG_FILE, $COMMIT_SOURCE)

# Only run for regular commits (not merges, amends)
if ($COMMIT_SOURCE -and $COMMIT_SOURCE -ne "") { exit 0 }

# Get the staged diff
$diff = git diff --cached --stat
$diffContent = git diff --cached

if (-not $diffContent) { exit 0 }

# Generate commit message via Gemini
$prompt = @"
Given this git diff, write a conventional commit message.

Format: (): 

Types: feat, fix, refactor, test, chore, docs, perf, style
Scope: the primary module/file affected (omit if unclear)
Subject: imperative mood, max 72 chars, no period at end

Rules:
- Only output the commit message — no explanation
- If the change is complex, add a blank line then a 2-3 sentence body

Diff stats:
$diff

Full diff:
$diffContent
"@

$message = $prompt | gemini -p $prompt --no-interactive 2>$null

if ($message -and $LASTEXITCODE -eq 0) {
    # Write AI message to the commit message file
    $message | Out-File -FilePath $COMMIT_MSG_FILE -Encoding utf8 -NoNewline
    Write-Host "AI commit message generated." -ForegroundColor Cyan
}
#!/usr/bin/env bash
# .git/hooks/prepare-commit-msg
# chmod +x .git/hooks/prepare-commit-msg

COMMIT_MSG_FILE="$1"
COMMIT_SOURCE="$2"

# Only run for regular commits
[ -n "$COMMIT_SOURCE" ] && exit 0

DIFF=$(git diff --cached)
[ -z "$DIFF" ] && exit 0

DIFF_STAT=$(git diff --cached --stat)

MESSAGE=$(gemini -p "Given this git diff, write a conventional commit message.

Format: (): 

Types: feat, fix, refactor, test, chore, docs, perf, style
Subject: imperative mood, max 72 chars, no period at end
Only output the commit message — no explanation.

Diff stats:
$DIFF_STAT

Full diff:
$DIFF" --no-interactive 2>/dev/null)

if [ $? -eq 0 ] && [ -n "$MESSAGE" ]; then
    echo "$MESSAGE" > "$COMMIT_MSG_FILE"
    echo "AI commit message generated." >&2
fi
WHAT: The prepare-commit-msg hook fires before the editor opens for the commit message — meaning you can override the message file before the user sees it. WHY: Conventional Commits enforce a standard every time, without requiring developers to remember the format. GOTCHA: On Windows, Git hooks must be executable shell scripts. For PowerShell hooks, create a wrapper prepare-commit-msg (no extension) that calls pwsh -File .git/hooks/prepare-commit-msg.ps1.

File Watcher: Auto-Generate Tests

A FileSystemWatcherA .NET class (System.IO.FileSystemWatcher) built into PowerShell that monitors a directory for file system changes — Created, Changed, Deleted, Renamed events. It fires events asynchronously without polling, making it highly efficient for watching source directories. running in the background watches your src/ directory. Every time a new .ts file is created, it automatically asks Gemini to generate a corresponding Jest test suite.

PowerShell — FileSystemWatcher for auto test generation
# watch-and-test.ps1 — Run in a background terminal
# Watches src/ for new .ts files and auto-generates Jest tests

$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "$PWD\src"
$watcher.Filter = "*.ts"
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true

$action = {
    $filePath = $Event.SourceEventArgs.FullPath
    $fileName = [System.IO.Path]::GetFileNameWithoutExtension($filePath)
    $testPath = $filePath -replace "\\src\\", "\tests\" -replace "\.ts$", ".test.ts"

    # Skip test files themselves
    if ($filePath -match "\.test\.ts$") { return }

    # Skip if test already exists
    if (Test-Path $testPath) { return }

    Write-Host "New file detected: $fileName.ts — generating tests..."

    # Ensure tests directory exists
    $testDir = Split-Path $testPath -Parent
    if (-not (Test-Path $testDir)) { New-Item -ItemType Directory -Force $testDir | Out-Null }

    # Generate test file
    $prompt = "@$filePath
Generate a comprehensive Jest test suite for this TypeScript file.
Requirements:
- Import the module correctly using the relative path
- Test each exported function with at least 3 cases: happy path, edge case, error case
- Mock external dependencies
- Use describe/it/expect syntax
- Include beforeEach/afterEach cleanup where needed
Write only the test file content — no markdown, no explanation."

    $testContent = gemini -p $prompt --no-interactive 2>$null

    if ($LASTEXITCODE -eq 0 -and $testContent) {
        $testContent | Out-File -FilePath $testPath -Encoding utf8
        Write-Host "  Test generated: $([System.IO.Path]::GetFileName($testPath))" -ForegroundColor Green
    } else {
        Write-Warning "  Test generation failed for $fileName.ts"
    }
}

# Register the Created event
Register-ObjectEvent $watcher "Created" -Action $action | Out-Null

Write-Host "Watching src/ for new TypeScript files..." -ForegroundColor Cyan
Write-Host "Press Ctrl+C to stop."

# Keep the script alive
try {
    while ($true) { Start-Sleep -Seconds 1 }
} finally {
    $watcher.EnableRaisingEvents = $false
    $watcher.Dispose()
    Write-Host "`nWatcher stopped."
}

What Just Happened?

The FileSystemWatcher subscribes to Created events on src/. When a new .ts file appears, the event fires synchronously in a background thread, calls Gemini with the file as context, and writes the generated test file to tests/ with the matching path structure. By the time a developer runs vitest, the test scaffolding is already there.

Scheduled Tasks

Windows Task Scheduler runs Gemini CLI scripts on a defined schedule — no process needs to be running. Here are two production-ready scheduled tasks:

Animation — Nightly Security Audit Execution
Task Scheduler: trigger at 02:00 AM (nightly)
waiting
PowerShell: launch nightly-security-audit.ps1
waiting
Gemini CLI: audit all *.py, *.ts, *.js files
waiting
Write findings to logs/security-audit-YYYYMMDD.json
waiting
If HIGH/CRITICAL: send email via PowerShell Send-MailMessage
waiting

Task 1 — Nightly Security Audit (2 AM)

# Register the nightly security audit task
$action = New-ScheduledTaskAction `
    -Execute "pwsh.exe" `
    -Argument "-NonInteractive -File C:\dev\myproject\scripts\nightly-security-audit.ps1" `
    -WorkingDirectory "C:\dev\myproject"

$trigger = New-ScheduledTaskTrigger -Daily -At "02:00"

$settings = New-ScheduledTaskSettingsSet `
    -ExecutionTimeLimit (New-TimeSpan -Hours 1) `
    -RestartCount 2 `
    -RestartInterval (New-TimeSpan -Minutes 5)

Register-ScheduledTask `
    -TaskName "GeminiCLI-NightlySecurityAudit" `
    -TaskPath "\GeminiCLI\" `
    -Action $action `
    -Trigger $trigger `
    -Settings $settings `
    -RunLevel Highest `
    -Force
# Register nightly security audit via schtasks CLI
schtasks /create ^
    /tn "\GeminiCLI\NightlySecurityAudit" ^
    /tr "pwsh.exe -NonInteractive -File C:\dev\myproject\scripts\nightly-security-audit.ps1" ^
    /sc DAILY /st 02:00 ^
    /rl HIGHEST ^
    /f

# Register weekly dependency review (Sunday 3 AM)
schtasks /create ^
    /tn "\GeminiCLI\WeeklyDependencyReview" ^
    /tr "pwsh.exe -NonInteractive -File C:\dev\myproject\scripts\weekly-dep-review.ps1" ^
    /sc WEEKLY /d SUN /st 03:00 ^
    /rl HIGHEST ^
    /f

# List tasks in the GeminiCLI folder
schtasks /query /tn "\GeminiCLI\" /fo LIST

The nightly-security-audit.ps1 Wrapper

scripts/nightly-security-audit.ps1
param(
    [string]$ProjectPath = $PSScriptRoot + "\..",
    [string]$LogDir = "$PSScriptRoot\..\logs"
)

# Ensure GEMINI_API_KEY is available (set as System env var)
if (-not $env:GEMINI_API_KEY) {
    Write-Error "GEMINI_API_KEY not set. Aborting audit."
    exit 1
}

$date = Get-Date -Format "yyyyMMdd"
$logFile = "$LogDir\security-audit-$date.json"
New-Item -ItemType Directory -Force $LogDir | Out-Null

$findings = @()

Get-ChildItem -Path $ProjectPath -Recurse -Include "*.py","*.ts","*.js" |
    Where-Object { $_.FullName -notmatch "(node_modules|\.git|dist|build)" } |
    ForEach-Object {
        $result = gemini -p "@$($_.FullName)
Audit for OWASP Top 10 issues. Output JSON array: [{severity,category,line,description}]
Output empty array [] if clean." --no-interactive 2>$null

        $issues = $result | ConvertFrom-Json -ErrorAction SilentlyContinue
        if ($issues -and $issues.Count -gt 0) {
            $findings += @{ file = $_.Name; issues = $issues }
        }
    }

# Write JSON report
$report = @{ date = $date; totalFiles = (Get-ChildItem -Recurse $ProjectPath -Include "*.py","*.ts","*.js").Count; findings = $findings }
$report | ConvertTo-Json -Depth 10 | Out-File $logFile -Encoding utf8

# Alert if high/critical issues found
$critical = $findings | Where-Object { $_.issues | Where-Object { $_.severity -in "HIGH","CRITICAL" } }
if ($critical) {
    Write-Warning "ALERT: $($critical.Count) file(s) with HIGH/CRITICAL security issues found. See $logFile"
    exit 1
}

Write-Host "Nightly audit complete. No HIGH/CRITICAL issues. Report: $logFile"
Course Complete!

You've completed all 17 modules of Gemini CLI: From Zero to Production. Here's what you've built:

M00–M03 Setup, prompting, GEMINI.md, Skills
M04–M06 Plan Mode, requirements, architecture design
M07–M09 Implementation, testing, deployment
M10–M11 MCP servers, extensions ecosystem
M12–M14 GitHub Actions, Workspace, CI/CD
M15–M16 Memory, checkpoints, shell automation

You've covered the full AI SDLC: from writing the first requirement to deploying, monitoring, and automating the entire development lifecycle with Gemini CLI. The Capstone is your graduation project — apply every technique from every track to build and ship a production-ready API from scratch.

Build the Capstone →

Knowledge Check

Five questions on shell automation, Docker sandboxing, and scheduled tasks.

1. What does the --yolo flag do when running Gemini CLI in a script?

A
Enables debug logging for all tool calls
B
Uses a more creative model temperature setting
C
Skips all tool call confirmation prompts, allowing unattended execution
D
Disables safety filters on the model response
Correct! --yolo bypasses the "Do you want to run this command?" prompts that appear in interactive mode. It's essential for unattended scripts but should always be paired with --sandbox when file mutations are involved.
The --yolo flag skips all tool call confirmation prompts. Without it, Gemini CLI would pause and wait for user input (y/n) before executing any shell command — which hangs automation scripts.

2. The --sandbox flag runs tool executions in a Docker container. Your project directory is mounted at which path inside the container?

A
/home/gemini
B
/workspace
C
/project
D
/tmp/gemini
Correct! Gemini CLI mounts your current working directory at /workspace inside the sandbox container. This means @./myfile.ts in a prompt is accessed as /workspace/myfile.ts from the container's perspective.
The project directory is mounted at /workspace inside the Docker sandbox container. Any file operations (reads, writes) that Gemini performs go through this mount point.

3. You want a Git hook that generates a commit message before the editor opens (so the user sees it pre-filled). Which hook name should you use?

A
pre-commit
B
commit-msg
C
prepare-commit-msg
D
post-commit
Correct! prepare-commit-msg fires after the commit message file is created but before the editor opens. Writing to the file at this stage pre-fills the editor. commit-msg runs after the user writes a message and is used for validation only.
The prepare-commit-msg hook fires just before the editor opens, giving you a chance to pre-fill the commit message file. pre-commit runs before any message is prepared. commit-msg runs after the user writes their message.

4. The PowerShell FileSystemWatcher script watches src/ for new .ts files and generates tests. Where does it write the test file?

A
In the same src/ directory with a .test.ts suffix
B
In a tests/ directory, mirroring the src/ path structure
C
In ~/.gemini/tests/ as a global test cache
D
Next to the original file with a __test__ prefix
Correct! The script uses a path substitution — replacing \src\ with \tests\ in the path — to mirror the source directory structure in the tests directory. So src/auth/jwt.ts generates tests/auth/jwt.test.ts.
The FileSystemWatcher script replaces the src path segment with tests to generate a mirror path. So src/api/users.ts creates tests/api/users.test.ts. The directory is created automatically if it doesn't exist.

5. Why must you set GEMINI_API_KEY as a Windows System environment variable (not just a user variable) for scheduled tasks?

A
Gemini CLI only reads System-level environment variables by design
B
Scheduled tasks run under the SYSTEM account, which doesn't inherit user-level environment variables
C
User environment variables are cleared after each reboot
D
PowerShell scripts cannot read user environment variables
Correct! Windows Task Scheduler tasks run under the SYSTEM or a specified service account. These accounts don't have access to your user profile's environment variables. Setting the API key as a System variable (via System Properties → Advanced → Environment Variables → System variables) makes it available to all accounts.
Scheduled tasks run under the SYSTEM account (or a service account) that doesn't inherit the logged-in user's environment. To make the API key available to the scheduled task, it must be set as a System-level environment variable in System Properties → Advanced → Environment Variables → System variables.