⌂ Home
Gemini CLI: From Zero to Production
Track 2 — Skills & Agentic Workflows Module 5 of 18
📄 M04 ⏱ 50 min 💪 Beginner
← M03: skill.md & Custom Commands M05: Requirements & Specs →
Module 04 · Track 2: Skills & Agentic Workflows

Plan Mode

Before Gemini touches a single file in TaskFlow, it can show you exactly what it intends to do — and why. Plan ModeA two-phase Gemini CLI workflow. Phase 1 produces a numbered execution plan with no file changes. Phase 2 executes only after you explicitly approve, edit, or reject the plan. splits agentic tasks into a reasoning phase and an execution phase, giving you a mandatory review window between Gemini's thinking and its actions on your codebase.

What You'll Learn

  • Understand the two-phase Plan Mode flow and why it prevents half-implemented features
  • Activate Plan Mode three ways: --plan flag, /plan toggle, and GEMINI.md default
  • Read a real plan output on TaskFlow and identify what's safe to approve and what needs editing
  • Edit individual plan steps before execution begins
  • Handle multi-file plans that span routers/tasks.py and routers/tags.py
  • Know exactly which scenarios benefit from Plan Mode — and which don't

Why Planning Matters

Analogy — The Contractor's Estimate

BEFORE: Imagine you hire a contractor to renovate your kitchen. You hand them a key and come back three days later. Sometimes the result is great. More often, you find they knocked down a wall you wanted to keep, installed the wrong countertop, and ran plumbing to the wrong side — because you never agreed on the plan before they started swinging hammers.

PAIN: The same problem happens when AI agents immediately start rewriting files. You say "add a completed_after filter to GET /tasks/" and Gemini starts modifying routers/tasks.py right away. By the time it's done — or fails halfway — you're staring at a half-migrated router with broken imports, a partially updated schemas.py, and no easy way to know what changed.

MAPPING: Plan Mode is the contractor's written estimate: a numbered list of every file to touch, every change to make, and the rationale for each step — before a single file is modified. You read it, mark up the parts you disagree with, and only then say "go." The work is the same; the difference is you had a chance to catch the wrong-wall mistake before the damage was done.

Technical Definition — Plan Mode

Plan ModeA two-phase Gemini CLI workflow. Phase 1 reads files and produces a structured, numbered execution plan. Phase 2 executes each step only after explicit approval. No file modifications occur in Phase 1. is a two-phase workflow in Gemini CLI. Phase 1 sends your task to Gemini, which reasons about it and emits a structured, numbered plan as its only output — no file writes, no shell commands that modify state, no irreversible actions. Phase 2 begins only after you explicitly approve, and executes each numbered step in sequence. Between phases, you can approve the full plan, reject it, or edit individual steps.

The key constraint: Phase 1 is read-only. Gemini can read files to build context (it reads @sample-project/taskflow/routers/tasks.py to understand the current code), but it writes nothing until you approve.

Why It Matters

In a 2024 study of developer AI workflows, teams that reviewed agent plans before execution caught scope creep in 68% of multi-file tasks — meaning in two out of three cases, the AI was planning to change more files than asked. Plan Mode makes that visible before any damage is done. For TaskFlow specifically: a single "add a filter" prompt can silently cascade into changes to the router, the schema, the tests, and sometimes the database migration — Plan Mode shows all four steps before any of them execute.

Enabling Plan Mode

Three ways to activate Plan Mode, in order of reach:

Method 1 — CLI Flag (one task)

# Run a single task in plan mode — plan before executing
gemini --plan "Add a completed_after filter to GET /tasks/ in @sample-project/taskflow/routers/tasks.py"

# Equivalent using -p shorthand
gemini -p "Add a completed_after filter to GET /tasks/"
# Run a single task in plan mode
gemini --plan "Add a completed_after filter to GET /tasks/ in @sample-project/taskflow/routers/tasks.py"

# Equivalent using -p shorthand
gemini -p "Add a completed_after filter to GET /tasks/"
WHAT: Runs exactly this one task in plan mode. Gemini shows the plan, waits for your approval, then executes. Subsequent tasks in the same session are not automatically in plan mode.

Method 2 — /plan Toggle (in-session)

Inside Gemini interactive REPL
gemini

> /plan
  Plan mode: ON  — all agentic tasks will show a plan before executing

> Add a completed_after filter to GET /tasks/
  [Plan displayed here — see Section 3 for a real example]
  Approve? (y/n/edit):

> /plan
  Plan mode: OFF
WHAT: /plan is a toggle. Turn it on at the start of a session when you're doing refactor work; turn it off when you're just asking questions.

Method 3 — GEMINI.md Default (always-on for project)

sample-project/taskflow/GEMINI.md (add this section)
# TaskFlow — Gemini CLI Context

## Gemini Settings
plan_mode: true   # Always show a plan before executing agentic tasks

## Project Overview
TaskFlow is a REST API for managing personal tasks...
(rest of GEMINI.md unchanged)
WHY: For a shared team project like TaskFlow, setting plan_mode: true in GEMINI.md means every developer on the team gets plan mode automatically — no one can accidentally run a direct refactor. It's an architectural guardrail, committed alongside the code.
GOTCHA: plan_mode: true in GEMINI.md also applies to simple tasks. If someone asks "what does list_tasks do?", Gemini still shows a read-only plan (Plan: [1] Read routers/tasks.py) before answering. For read-only questions this adds 2–3 seconds of overhead. See Section 8 for when to override.

Your First Plan: Add a Filter

Real scenario: you want to add a completed_after query parameter to GET /tasks/ so users can fetch tasks created after a given date. Currently routers/tasks.py filters by completed, priority, and tag — but not by date.

The Command

cd sample-project\taskflow
gemini --plan "Add a completed_after query param (Optional[datetime]) to GET /tasks/ in @routers/tasks.py. Return only tasks where created_at >= completed_after when the param is provided."
cd sample-project/taskflow
gemini --plan "Add a completed_after query param (Optional[datetime]) to GET /tasks/ in @routers/tasks.py. Return only tasks where created_at >= completed_after when the param is provided."

The Plan Output

Gemini produces a plan that looks like this. Let's walk through each step:

⚙ Gemini Plan — Add completed_after filter to GET /tasks/
1.
READ routers/tasks.py
Understand the existing list_tasks function signature and current filter pattern
2.
READ schemas.py
Verify Task response schema includes created_at field for the filter to apply correctly
3.
READ models.py
Confirm Task.created_at column type (DateTime) supports >= comparison in SQLAlchemy
4.
WRITE routers/tasks.py
Add completed_after: Optional[datetime] = Query(None) parameter and .filter(Task.created_at >= completed_after) clause to list_tasks()
5.
WRITE schemas.py
No change needed — created_at is already in Task response schema. (Skipping this step.)
2 files to modify  |  3 files to read  |  0 shell commands Approve? (y/n/edit):

What Just Happened?

Gemini read the three relevant files in Phase 1 and produced a precise plan. Notice step 5: it planned to modify schemas.py but then decided it wasn't needed — and documented that reasoning in the plan itself. That's Plan Mode working correctly: showing its thinking so you can agree or disagree before any file changes happen.

What to Check in a Plan

  • Are all the right files listed? For a filter change on list_tasks, you expect routers/tasks.py to be in WRITE — if it's missing, the plan is wrong.
  • Are any extra files listed that you didn't ask to change? If Gemini plans to write main.py or auth.py, ask yourself why before approving.
  • Does the rationale make sense? Step 3 says "confirm DateTime supports >= comparison" — that's a real reason, not filler. A plan that says "just because" is a red flag.
  • Are there any EXEC steps? Shell execution in a plan should always be reviewed carefully — know what command will run.

Inspecting and Editing Plans

Animation 1 — Two-Phase Plan Mode Flow
Phase 1 — Plan
Gemini Pro
Reads files, reasons about the task, emits a numbered plan. No file writes.
Phase 2 — Execute
Gemini Flash
Executes each step in sequence. File writes happen here. Checkpoints before each step.
Press Play to see the two-phase flow

The Three Approval Choices

After Gemini shows the plan, you have three options:

y — Approve & Execute
e — Edit a Step
n — Reject

Approving the Full Plan

Approving the completed_after plan
Approve? (y/n/edit): y

✦ Executing step 1: Reading routers/tasks.py...
✦ Executing step 2: Reading schemas.py...
✦ Executing step 3: Reading models.py...
✦ Executing step 4: Writing routers/tasks.py...
✦ Step 5 skipped (no changes needed to schemas.py)

Done. 1 file modified.
WHAT: Steps 1–3 are read-only and run quickly. Step 4 is the first write — it adds the completed_after parameter and filter clause to list_tasks(). Step 5 was already identified as a no-op in the plan and is skipped cleanly.

Editing a Plan Step

Suppose the plan's step 4 is too vague — you want to ensure the datetime is timezone-aware and that the filter uses created_at specifically, not due_date accidentally. Here's how to edit before approving:

Editing step 4 before execution
Approve? (y/n/edit): e

Which step to edit? (1-5): 4

Current step 4:
  Add completed_after: Optional[datetime] = Query(None) and
  .filter(Task.created_at >= completed_after) to list_tasks()

Your edit (press Enter to keep current, or type replacement):
  Add completed_after: Optional[datetime] = Query(None, description="Filter tasks
  created after this datetime (UTC ISO-8601)"). Add null check:
  if completed_after: query = query.filter(Task.created_at >= completed_after)
  Ensure the added import: from datetime import datetime

Updated plan. Re-run approval? (y/n/edit): y
WHY: edit rather than reject and restart. Editing step 4 takes 10 seconds. Rejecting the plan and re-prompting regenerates the entire plan (~2–3 extra seconds plus re-reading all files). For small corrections, edit is always faster.
GOTCHA: edited steps are not validated. If you type a step that contradicts another step (e.g., you edit step 4 to "delete the function entirely" while step 2 reads schemas that depend on it), Gemini will attempt to execute your edit literally. Always re-read the full plan after editing to check for contradictions.

What Just Happened?

You intercepted the plan between Phase 1 and Phase 2, surgically corrected one step, and re-approved. The edit added: a null check (preventing always-filtering when the param is absent), a proper Query description (which FastAPI uses in the auto-generated docs at /docs), and the datetime import. The final implementation will be noticeably better than what would have run without the edit.

Multi-File Plans

TaskFlow has three routers. What happens when a change spans multiple of them? Plan Mode shines brightest here — it makes the cascade of changes visible all at once.

Animation 2 — Multi-File Plan Assembly
Press Play to see the plan build for: add pagination to all list endpoints

The Command

gemini --plan "Add pagination (skip, limit query params, defaults 0 and 20) to all list endpoints: @routers/tasks.py and @routers/tags.py. Add an X-Total-Count response header to each."
gemini --plan "Add pagination (skip, limit query params, defaults 0 and 20) to all list endpoints: @routers/tasks.py and @routers/tags.py. Add an X-Total-Count response header to each."

What the Plan Looks Like

⚙ Gemini Plan — Add pagination to list endpoints
READ routers/tasks.py Understand list_tasks signature, return type, and existing query structure
READ routers/tags.py Understand list_tags signature and return type
READ schemas.py Verify Task and Tag response models — X-Total-Count requires response_model=List[...], must use JSONResponse override
WRITE routers/tasks.py Add skip: int = Query(0, ge=0) and limit: int = Query(20, ge=1, le=100). Add .offset(skip).limit(limit). Get total count before pagination. Return JSONResponse with X-Total-Count header.
WRITE routers/tags.py Same pagination pattern. Note: tags list is typically small (<100) — limit default of 20 may surprise users. Consider limit=100 default for tags.
2 files to modify  |  3 files to read   Approve? (y/n/edit):
Gemini's Unsolicited Observation

Notice step 5: Gemini flagged that a default limit=20 for tags "may surprise users" since tag lists are typically small. This kind of inline reasoning is Plan Mode's second benefit — not just safety, but architectural commentary before you commit to the implementation. You can edit step 5's default to limit=100 before approving.

Approval Gates in Team Workflows

Plan Mode isn't only for interactive sessions. You can embed it in your pull-request workflow to catch unreviewed AI changes before they merge.

Saving a Plan to File

# Generate and save a plan without executing it
gemini --plan --dry-run "Add due_date filter to GET /tasks/" > plan.txt

# Review plan.txt in your PR description
Get-Content plan.txt
# Generate and save a plan without executing it
gemini --plan --dry-run "Add due_date filter to GET /tasks/" > plan.txt

# Review plan.txt in your PR description
cat plan.txt

Using Plan Mode as a Pre-Commit Check

sample-project/taskflow/.git/hooks/pre-commit (example pattern)
#!/bin/bash
# If a GEMINI_TASK env var is set, require a plan review before commit
if [ -n "$GEMINI_TASK" ]; then
  echo "Generating plan for: $GEMINI_TASK"
  gemini --plan --dry-run "$GEMINI_TASK" > .gemini/last-plan.txt
  echo "Plan saved to .gemini/last-plan.txt — review before proceeding."
  echo "To approve and commit: GEMINI_APPROVED=1 git commit"
  if [ -z "$GEMINI_APPROVED" ]; then
    exit 1  # Block commit until human reviews plan
  fi
fi
WHAT: If a developer sets GEMINI_TASK to their AI prompt before committing, this hook generates a plan and saves it. The commit is blocked unless they also set GEMINI_APPROVED=1, forcing them to read the plan first. This is a lightweight process gate, not a CI system — it runs in the developer's local shell.
Warning — Plans Are Not Contracts

A plan describes Gemini's intention, not a strict contract. In rare cases (complex file interactions, large context windows), the execution may deviate slightly from the plan. Always run your tests after execution, regardless of plan approval. Plan Mode reduces risk — it doesn't eliminate it.

When NOT to Use Plan Mode

Animation 3 — When to Use Plan Mode

Select a scenario to see the recommendation.

Task Type Plan Mode? Reason
Read-only question ("what does list_tasks do?") SKIP No files will be modified; plan overhead adds 2–3s with zero safety benefit
Single-line fix (rename a variable) SKIP The change is atomic and self-evident; reviewing a 3-step plan for a 1-line edit wastes more time than it saves
Adding a new query parameter to one router USE Likely touches the function signature + imports + possibly schemas; worth 3s to see the scope
Refactor spanning multiple routers USE High risk of scope creep and broken imports; plan makes cascade visible
Adding a new model + router + schema USE Multiple files, multiple write operations; plan catches missing or extra steps before any damage
Running tests or reading logs SKIP Read-only shell operations; plan overhead with no write-protection benefit
Rule of Thumb

Use Plan Mode when the number of files Gemini will write is unknown to you before issuing the prompt. If you already know "this touches exactly one function in one file," skip it. If you're not sure, use it — the overhead is 2–3 seconds and never more than a few hundred tokens.

Lab: Plan a Feature

Use Plan Mode to plan adding a due_date filter to TaskFlow's GET /tasks/ endpoint. Then approve the plan and verify the implementation. This is a real change — models.py already has a due_date field on the Task model, so this lab is immediately testable.

1

Start TaskFlow

cd sample-project\taskflow
pip install -r requirements.txt
uvicorn main:app --reload
cd sample-project/taskflow
pip install -r requirements.txt
uvicorn main:app --reload
2

Open a second terminal and run Plan Mode

cd sample-project\taskflow
gemini --plan "Add a due_before query param (Optional[datetime]) to GET /tasks/ in @routers/tasks.py. Filter: return only tasks where due_date is not null AND due_date <= due_before. Add a null check so the filter is skipped when the param is absent."
cd sample-project/taskflow
gemini --plan "Add a due_before query param (Optional[datetime]) to GET /tasks/ in @routers/tasks.py. Filter: return only tasks where due_date is not null AND due_date <= due_before. Add a null check so the filter is skipped when the param is absent."
3

Read the plan — check for these things

  • Does it plan to read models.py? (It should, to confirm the due_date column type)
  • Does it plan to write only routers/tasks.py? (No other files should need changing)
  • Does the write step mention the null check? (If not, edit that step before approving)
  • Does it plan to write any test files? (If so, that's scope creep — reject or edit it out)
4

Approve or edit, then execute

Approval
# If the plan looks correct:
Approve? (y/n/edit): y

# If step 4 is missing the null check, edit it:
Approve? (y/n/edit): e
Which step? 4
[type the corrected step description]
5

Verify the implementation

# Check the OpenAPI docs for the new parameter
Start-Process "http://localhost:8000/docs"

# Or curl test directly (replace TOKEN with your actual JWT)
$TOKEN = "your_jwt_token_here"
$headers = @{ Authorization = "Bearer $TOKEN" }

# Filter tasks due before 2026-12-31
Invoke-WebRequest -Uri "http://localhost:8000/tasks/?due_before=2026-12-31T00:00:00" `
  -Headers $headers | Select-Object -ExpandProperty Content

# Baseline: all tasks (should be more or equal)
Invoke-WebRequest -Uri "http://localhost:8000/tasks/" `
  -Headers $headers | Select-Object -ExpandProperty Content
# Check the OpenAPI docs for the new parameter
open http://localhost:8000/docs

# Or curl test directly (replace TOKEN with your actual JWT)
TOKEN="your_jwt_token_here"

# Filter tasks due before 2026-12-31
curl -s "http://localhost:8000/tasks/?due_before=2026-12-31T00:00:00" \
  -H "Authorization: Bearer $TOKEN" | python -m json.tool

# Baseline: all tasks (should be more or equal)
curl -s "http://localhost:8000/tasks/" \
  -H "Authorization: Bearer $TOKEN" | python -m json.tool

What Just Happened?

You used Plan Mode to see the entire change before a single byte was written to routers/tasks.py. You had the opportunity to catch a missing null check before it became a production bug — every request without due_before would have returned zero tasks instead of all tasks. The plan exposed that risk in 3 seconds. Fixing it after deployment would have taken much longer to diagnose.

Knowledge Check

Five questions based on the TaskFlow scenarios you worked through.

1. During Phase 1 of Plan Mode, Gemini reads routers/tasks.py to understand the existing list_tasks function. What does Phase 1 NOT do?

Read multiple context files
Write any files or execute state-changing shell commands
Produce a numbered execution plan as output
Reason about the task and determine which files are relevant

2. You run gemini --plan "Add pagination to GET /tasks/" and the plan shows 6 steps — including writing main.py, which you did not ask to change. What is the correct response?

Approve — Gemini always knows which files need to change
Edit the plan to remove the main.py step, then approve — this is exactly what Plan Mode is designed to catch
Reject and re-prompt without mentioning pagination to avoid the main.py issue
Approve and then manually revert main.py after execution

3. You want Plan Mode to be active for ALL Gemini CLI sessions in the TaskFlow project without requiring the --plan flag every time. Where do you configure this?

In a global ~/.gemini/config.yaml file
In sample-project/taskflow/GEMINI.md with plan_mode: true
In a .gemini/settings.json file in the project root
By creating an alias: alias gemini='gemini --plan' in your shell profile

4. Which of the following TaskFlow tasks does NOT benefit from Plan Mode?

"Explain how the JWT authentication flow works in auth.py"
"Add a rate_limit field to the User model and update all related schemas"
"Refactor all four task endpoints to use a shared get_task_or_404 helper"
"Add X-Total-Count headers to both list_tasks and list_tags"

5. In the lab, you used Plan Mode to add a due_before filter. The plan's write step was missing a null check. Why does this matter specifically for TaskFlow's GET /tasks/ endpoint?

The null check improves performance by reducing database query time
Without it, Gemini CLI would refuse to execute the plan
Without the null check, every call to GET /tasks/ without the due_before param would filter by due_date <= None, which in SQLAlchemy returns no rows — silently breaking the default task list for all users
A missing null check causes a Python syntax error that prevents the server from starting