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:
--planflag,/plantoggle, 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.pyandrouters/tags.py - Know exactly which scenarios benefit from Plan Mode — and which don't
Why Planning Matters
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.
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.
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/"
Method 2 — /plan Toggle (in-session)
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
/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)
# 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)
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.
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:
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 expectrouters/tasks.pyto 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.pyorauth.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
The Three Approval Choices
After Gemini shows the plan, you have three options:
Approving the Full 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.
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:
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
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.
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
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
#!/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
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.
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
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 |
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.
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
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."
Read the plan — check for these things
- Does it plan to read
models.py? (It should, to confirm thedue_datecolumn 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)
Approve or edit, then execute
# 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]
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?
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?
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?
~/.gemini/config.yaml filesample-project/taskflow/GEMINI.md with plan_mode: true.gemini/settings.json file in the project rootalias gemini='gemini --plan' in your shell profile4. Which of the following TaskFlow tasks does NOT benefit from Plan Mode?
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?
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