Plan Mode
September 2, 2026 · View on GitHub
Plan Mode is a read-only operating mode for the agent. The model can use
Read, Grep, Tree, and TodoWrite to investigate the codebase, but it
cannot write, edit, delete, or run shell commands - those tools stay
advertised in the tool-use API (so the request's tool definitions, and with
them the provider's prompt cache, survive a mode switch) but are disabled at
execution time and return an error if called. Instead, it produces a
plan for the proposed change, persists that plan through the configured
storage backend (on jsonl as a Markdown file under ~/.infer/plans/), and
surfaces it to you for approval before any real work happens.
Why use it
Plan mode exists for tasks where you want a human gate between "understand the change" and "make the change":
- Risky or far-reaching refactors.
- Migrations and schema changes.
- Multi-file features where you want a written record before execution.
- Pair-programming flows: review the plan, push back, iterate.
Every plan - accepted or rejected - is kept in storage (on the jsonl
backend under ~/.infer/plans/) as an audit trail. You can grep, share, or
revisit them later.
How to enter plan mode
In the interactive chat TUI, press Shift+Tab to cycle the agent mode. The status line shows the current mode; cycle until it reads Plan Mode. Press Shift+Tab again to leave.
The cycle is:
Standard → Plan Mode → Auto-Accept → Auto+Judge → Standard → …
When a plan is accepted, the mode automatically switches to Auto-Accept (or to Standard if you chose "Approve Each Step") and the agent begins executing.
What the model is told to do
The plan-mode instructions are delivered by the mode-change system reminder
(defined in config/reminders.go::defaultModeChangeGuidance, key plan) that
is injected when the agent switches into Plan Mode - the system prompt itself
stays byte-stable across mode switches so the prompt/KV cache keeps its prefix
hits. They instruct the model to:
- Use
Read,Grep,Treeto investigate. - Ask clarifying questions in regular assistant turns first - not
call
RequestPlanApprovalwhile gaps remain. - Only call
RequestPlanApprovalwhen the plan is complete. - Pass two arguments to that tool:
title: a short phrase (≤ 60 chars, no slashes or..).plan: the plan body as Markdown using the standard sections below.
Plan structure
The plan body is a Markdown document using these H2 sections, in this order. Sections that don't apply to the task are omitted; no extra top-level sections are introduced.
## Context- why this change is being made: problem, trigger, intended outcome.## Files to Modify- bullet list of paths plus a one-line note per file.## Current Code- short snippets of the existing code being changed (skip for new files).## Changes- concrete edits: function names, signatures, what's added / removed / replaced.## Performance Impact- expected runtime / memory / I/O impact. "Negligible." is a valid answer.## Critical Files- files other code depends on and that must remain backward-compatible.## Edge Cases- inputs and conditions needing explicit handling, plus the handling.## Verification- concrete steps you can run to confirm the change works end-to-end.
The H1 of the saved file is derived from the title argument, e.g.:
# Add Login Flow
## Context
…
Where plans are saved
Plans are persisted through the configured storage backend (storage.type:
jsonl, sqlite, postgres, redis, or d1). Every plan gets a stable ID of the
form <YYYY-MM-DD-HHMMSS>-<slug> (UTC timestamp), returned by the tool as an
infer://plans/<id> URI. Retrieve a plan from any backend with:
infer plans show <id> # full markdown body
infer plans list # all plans with title + creation time
On the default jsonl backend the plan also lands as a plain markdown file:
~/.infer/plans/<YYYY-MM-DD-HHMMSS>-<slug>.md
Plans are user-global, not per-project - every project's plans land in the
same userspace directory. Writes are atomic (<path>.tmp + rename), so
you'll never see a half-written plan file.
<slug> is the title lowercased, with non-alphanumerics collapsed to
-, capped at 60 characters. A plan with the same title in the same second
upserts the previous record instead of creating a duplicate.
Example listing:
$ ls ~/.infer/plans/
2026-04-25-091230-rip-out-old-auth-middleware.md
2026-04-26-143015-add-login-flow.md
2026-04-27-103015-rename-config-loader.md
Accept / Reject UX
When the model calls RequestPlanApproval, the chat TUI:
-
Renders the saved plan in a dedicated panel.
-
Shows a status line:
Plan ready - use arrow keys to select and Enter to confirm. -
Offers three options:
- Accept (
Enter/y) - agent switches to Auto-Accept mode (no per-tool approval prompts) and executes the plan. - Reject (
n) - chat session ends; you can write a follow-up message with feedback and the agent re-iterates the plan. - Approve Each Step (
s) - agent switches to Standard mode and executes the plan, prompting for approval on each action.
- Accept (
In all three cases the stored plan remains as an audit trail. Rejecting a plan does not delete it.
Fresh session on approval
Planning is read-heavy: the model runs Read, Grep, and Tree across the
codebase before it writes a plan. Once you Accept (or Approve Each
Step), that exploration is dead weight for execution. So approving a
plan always:
- Starts a fresh empty session (like
/new) - the planning conversation is preserved in storage but no longer in the working context. - Adds a hidden user message pointing the agent back at the stored plan
(
infer plans show <id>), which it re-reads to execute step by step. The plan itself is never lost - it lives in storage and is captured in the instruction.
This is simpler and more efficient than summarizing the planning conversation:
the plan is already saved on disk, so there is no need to compact the
conversation. The compact.enabled setting is not involved in this flow - it
only controls the automatic mid-conversation compaction (see the
Configuration Reference).
Iterating on a plan
Plan mode encourages a back-and-forth before the tool call lands. Typical flow:
- You: "Plan a refactor that splits
chat_handler.gointo smaller files." - Model: "Before I draft this, three questions: …" (regular assistant turn, no tool call yet).
- You answer.
- Model: (maybe more questions) - repeat until aligned.
- Model calls
RequestPlanApprovalwith the final plan. - You Reject with a comment ("the split is too granular - keep shortcut handling in the same file").
- Model adjusts and re-submits a new plan (a new file lands in
plans/). - You Accept - the agent executes.
Configuration
Plan mode itself has no separate config - it ships with the CLI. You can
customise the per-mode adjustment instructions (delivered by the mode-change
reminder, not the system prompt) and the tool description in
.infer/prompts.yaml:
agent:
mode_adjustment_plan: |-
# your custom plan-mode adjustment instructions
tools:
RequestPlanApproval:
description: |-
# your custom tool description
Empty fields fall back to the built-in reminder guidance (see
config/reminders.go::defaultModeChangeGuidance; it is user-overridable via
the guidance.plan key of the mode-change reminder in reminders.yaml).
Because these instructions ride the mode-change reminder, they are not
delivered when reminders.enabled: false. Environment-variable overrides:
INFER_PROMPTS_AGENT_MODE_ADJUSTMENT_PLANINFER_PROMPTS_TOOLS_REQUEST_PLAN_APPROVAL_DESCRIPTION