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-interactiveand--yoloflags - Use
--sandbox/--dockerfor isolated, safe tool execution - Write PowerShell batch scripts for security audits and auto-documentation
- Write equivalent Bash scripts with
jqfor 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
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
# 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
--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
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.
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").
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
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.
# .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
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.
# 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:
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
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"
You've completed all 17 modules of Gemini CLI: From Zero to Production. Here's what you've built:
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?
--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.--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?
/home/gemini
/workspace
/project
/tmp/gemini
/workspace inside the sandbox container. This means @./myfile.ts in a prompt is accessed as /workspace/myfile.ts from the container's perspective./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?
pre-commit
commit-msg
prepare-commit-msg
post-commit
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.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?
src/ directory with a .test.ts suffix
tests/ directory, mirroring the src/ path structure
~/.gemini/tests/ as a global test cache
__test__ prefix
\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.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.