Asking for a plan you can actually reject
Plan mode gates the writes. It doesn't make the plan worth rejecting — that's a shape you ask for. Six blocks that turn a step list into claims you can check.
The plan you can't argue with
Plan mode has a reputation as a slower way of saying yes and it's earned. You enable plan mode, get a nice numbered list back, have a look at it, don't see anything that stands out as obviously wrong so you approve it — and then find the decision you'd have objected to in the 40th file
The thing is, the plan wasn't bad, there was nothing in it that could've been bad, here's an example:
Let's say we have two tools and a plan mode for each of them (we had), both plans are for the same task in a two-files repository where fetch() retries 3 times with 0s delay. (This is not a tool comparison, the prompts were different too which is the entire thing) I isolated a variable that is the requested shape of the output.
There's plan mode 1:
Antigravity CLI
Prompt: task
Plan: plan without any code, in the prompt I've explicitly said so, here's an abridged excerpt (sub-bullets under items 3–5 are equivalent):
#### 1. Audit Current Retry Loop
* Locate the target function or execution block handling retries in app.py.
* Identify existing retry triggers (e.g., specific HTTP status codes, connection
errors, or custom exceptions).
* Note current parameters (e.g., fixed sleep duration, loop bounds).
#### 2. Define Configuration Constants
* Initial Delay (BASE_DELAY) ... Backoff Factor (BACKOFF_FACTOR) ...
Maximum Delay (MAX_DELAY) ... Maximum Attempts (MAX_RETRIES) ...
Jitter Flag (USE_JITTER) ...
#### 3. Refactor Delay Calculation Logic ...
#### 4. Refactor Exception & Flow Handling ...
#### 5. Verification & Testing Strategy ...And plan mode 2:
Claude Code
Same task, but the prompt in this case was the 6-block-long shape this lesson is about, here's an abridged version as well:
## 3. DECISIONS
- Sleep function: use stdlib time.sleep, rejected a custom/async sleep — no async
code anywhere in this file, stdlib is simplest and needs no new dependency.
- Backoff base/formula: use base_delay * (2 ** attempt) with base_delay = 0.1s,
rejected a configurable cfg["backoff_base"] — load_config() currently only
returns retries/timeout, and nothing supplies a backoff value today.
Could go either way ...
- No sleep after the final failed attempt: skip the sleep on the last iteration,
rejected always-sleep-then-check ...
- Jitter: not adding it, rejected — nothing indicates concurrent callers where
thundering-herd matters ...
## 4. ASSUMPTIONS
- UNVERIFIED: no test suite exists to check backoff behavior against — confirmed
via find . -iname "*.py" ...
- UNVERIFIED: exact desired base delay (0.1s) — not specified ...
## 5. NOT DOING
- Jitter.
- Configurable backoff parameters via load_config()/env vars.
- A max-delay cap.
- Changing which exceptions are caught (still only TimeoutError).
- Logging/metrics around retries.You can see eleven different reasons why to reject it which is roughly 4 seconds per bullet. Like: The delay is too low for this particular API, it should be configurable and there should be a cap.
For example, an earlier run of Claude Code in this very tool (same tool, same repo, same model, but a bare one-sentence-long task with no shape), behind the scenes set the maximum delay to 30s. In contrast to that, in this shaped run we've explicitly excluded the max-delay cap in NOT DOING. So it's like the only thing that differed was the shape, and even then we've made a genuine design decision which turned out to be different. And it's like only in this particular run we've been aware of that
In other words — plan mode doesn't make a plan rejectable, you do by requesting a different shape
What makes a line rejectable
Can you come up with an actual reason and say "no" to it in less than 10 seconds without looking at the code? If no (and "yes" is the only thing you can say), it's fluff — decoration pretending to be work
Here are some types of lines that pass this test:
A path to a file — you can easily verify if app.py really exists in the repository but you can't check if there's actually a retry module
A decision with the alternative it replaced — you can reject a choice, you can't reject "refactor the logic"
An assumption it couldn't prove — that's almost always a reason why plans go wrong
Something it didn't do — if you declare what's not part of the task upfront, excluding it costs nothing
A command showing it works — if it can't come up with such command, it doesn't know what done looks like
But here are some types of lines that don't pass:
Audit the current implementation — you postpone your actual opinion to "audit"
Refactor X — you postpone your actual opinion to "refactor"
Add appropriate error handling — you postpone your actual opinion to "add"
Add tests — you postpone your actual opinion to "add"
Ensure backwards compatibility — you postpone your actual opinion to "ensure"
These all are postponing the actual judgment
The six blocks
To make a plan easy to reject, ask for it in a fixed structure with some context around each block. Here are the six blocks:
GOAL — a single-sentence description of the outcome you'll see once you're done, so you can be certain they're not working on sth else before you even begin reading
FILES — paths to every file that will be created/modified/removed during the implementation, so you can diff the change with it at the end to see if there's any drift
DECISIONS — every design decision, the alternative it replaces and a short reasoning around it, to transform a description into an actual argument
ASSUMPTIONS — anything that's not certain just based on exploring the repository, marked as unverified with a command or file path that could be used to verify it
NOT DOING — everything it's deliberately leaving alone
DONE — a command that shows the implementation works
Blocks 4 and 5 are the ones people usually omit but are actually the most valuable. Forcing it to write assumptions down forces it to notice it's making them. And forcing it to create an exclusion list is basically locking the blast radius and communicating it to you during the revision which is still relatively cheap.
Also for block 3, it'd be good if we could tell it to mark everything that's really balanced so the reader sees a fork instead of just an obvious default when in fact it was a toss-up
The recipe
Activate plan mode before your initial prompt, enabling it during the run is a separate thing we cover in the repair-prompts lesson
Send both the task and the shape in one message, if you send the task only you'll get a narration of the diff
Read blocks 4, 5 and 3 — assumptions, exclusions and decisions, this is where the harm is, while the list is usually the least valuable part but catches the attention first
Refuse a specific line rather than the plan, providing the block number, line number and reason, pointing at a file is actionable, saying you don't like it is not
Ask for the entire revised plan instead of the diff, a half-approved plan is a plan nobody saw, ask even if your harness enforces it
Approve only once you can come up with an observation that could prove it wrong, if you can't think of anything that would mean DONE is incomplete so is the plan
Keep the approved plan somewhere the agent can access it, approving opens the floodgates so after that the plan becomes a piece of documentation and to be useful as a piece of documentation it needs to stay visible
A few examples of what doesn't work:
"Try again" — this is a re-roll, not a rejection, you're throwing away an entire planning pass in favour of another run which usually produces the same content worded differently
"Do it properly" — at least one of these harnesses interprets a vague "no" as an invitation to keep talking so it asks more questions and doesn't come back with a plan, tell it what you want instead of just being unhappy with sth
In Claude Code
The plan is the permission mode so there are four ways to get into it in docs:
Shift+Tabyou can cycle between default → acceptEdits → plan during the session/plan [description]you can type from the input line, task description is optional--permission-mode planflag when you launch claudesettings file entry
{"permissions": {"defaultMode": "plan"}}setting the plan as a project's default mode
Even the model itself can enter plan mode using the EnterPlanMode tool.
The really important thing is that by pressing Ctrl+G you can load the proposed plan into your default editor and change it directly before Claude acts on it, this is the most raw form of saying no as it's about striking ideas, correcting wrong assumptions etc and sending the file back, no chat needed.
Approval — in the docs there are four ways of approving a plan:
"Yes, and use auto mode" — or "Yes, auto-accept edits" when auto mode isn't available to your account; either way the session leaves plan mode and starts editing
"Yes, manually approve edits" — approve, then review each edit individually
hand it to Ultraplan — you can send the plan to Ultraplan's app for a browser review
"No, keep planning" — stay in plan mode to still have the plan; this is what we're focusing on today as it's the most important thing and usually overlooked
The gotcha. It's worth keeping in mind that when you approve the plan (no matter how) you exit the plan mode, because as per docs the plan mode is a one-shot gate so once you approve it, you enter another permission mode the "approve" command refers to, and can edit. To get into planning again, use Shift+Tab or /plan prefix.
Second gotcha, for headless runs. Another thing worth mentioning is that if you run claude with -p flag it doesn't have an approver so it doesn't show ExitPlanMode. Run it and it opens by saying ExitPlanMode isn't available, then prints the plan directly as plain text:
ExitPlanMode isn't available in this session, so here's the plan directly.As a result — your repo is not changed, git status is clean, gate still locked. And that's what headless plan mode is about, it means you outline the plan and stop, not outline the plan and wait. So just make sure to save it.
In Codex CLI
/plan enters plan mode and can optionally carry a prompt with it. It's about the tui feature, so there's no plan option in codex --help or in codex exec --help, therefore no headless codex exec --plan equivalent. Once it's done, the plan is returned wrapped in <proposed_plan> tag and the client removes this tag from the stream and displays it as a separate cell.
Codex implements more of the six blocks on the tool side than anything else here; the 0.146.0 binary has embedded instructions for plan mode that split unknowns into two groups:
things which are knowable by investigating the repo or the system — do it before asking
preferences and trade-offs which aren't knowable — raise early, providing 2–4 bullet points with alternatives and recommending a default; if you don't get a response, go with the default and log it as an assumption in the plan.
1. **Discoverable facts** (repo/system truth): explore first. ...
2. **Preferences/tradeoffs** (not discoverable): ask early. ...
* Provide 2–4 mutually exclusive options + a recommended default.
* If unanswered, proceed with the recommended option and record it as an
assumption in the final plan.Those two are blocks 3 and 4, natively supported by the tool.
The tool is also instructing to emit the <proposed_plan> tag only when the plan is ready — as in fully decided and leaves nothing for the implementer to decide; if there's a "TBD" in the plan, it means it's not ready by the tool's standards so we should stay in Plan mode instead of approving it.
The tool can only do reading, searching, static analysis or dry runs during planning; it can also run tests and builds even if they write to caches or build artifacts (target/, .cache/, snapshots) but not if they modify repo-tracked files; it's not possible to edit any file, run formatters that rewrite files or apply patches/migrations.
The tool is resilient, its mind can't be changed by the user's intent, tone and imperative phrasing; in the 0.146.0 binary the tui asks "Implement this plan?" after emitting it with three possible answers:
building it in the current session
building it in a new thread seeded with the plan (so it gets the plan as a statement of intent in a clean context which is the most effective way to make sure a planning conversation doesn't turn into an implementation one)
or rejecting and staying in Plan mode
If you reject the plan, it has a standard shape; if you don't communicate your concern well enough for the tool to understand what needs to be changed to address it, it'll ask about it and stay in the planning mode until you provide an answer, without emitting the <proposed_plan> tag. It's important to know that revisions are always full replacements, not incremental changes; therefore if you want to reject the plan, communicate the missing information or the decision you want instead of communicating your discontent.
update_plan is a todo/checklist tool and has nothing to do with Plan mode; people often confuse it but:
it doesn't enter nor leave Plan mode
running it while in Plan mode results in an error
therefore seeing a checklist in a regular session doesn't mean planning
In Cursor
Cursor ships two read-only modes — Ask and Plan — which are not the same thing, so it's good to highlight that based on what's said in the cursor-agent --help output:
--mode <mode> Start in the given execution mode. plan:
read-only/planning (analyze, propose plans, no
edits). ask: Q&A style for explanations and
questions (read-only). (choices: "plan", "ask")
--plan Start in plan mode (shorthand for --mode=plan).
(default: false)Ask — is asking questions (about code) which doesn't produce anything
Plan — is a planning phase with an approval-before-writes checkpoint which produces a document
Half the write-ups out there point at Ask as Cursor's planning gate, so just FYI — it's the wrong one. Starting plan mode looks like this:
cursor-agent --mode plan "add exponential backoff to the retry loop in app.py"Shift+Tab cycles between Agent → Plan → Ask modes
/plan [prompt] changes the mode to plan and submits a prompt or with no argument displays the current plan
The thing is — looking at the last case you can see that plan is not a chat message which disappears, but persistent state that can be addressed; in fact it's a virtual file that you can read and edit, stored in your home directory (unless you change it), but also can be saved to workspace using Save to workspace option so it can be shared. Then in the editor, in the Agent panel, to accept it you need to click Build button. But don't assume it exists in the terminal, because even if you look at the CLI documentation you'll see how to enter plan mode and what's /plan about, but nothing about the Build button.
The gotcha. -p is not a gate. Its own help line says so:
-p, --print Print responses to console (for scripts or non-interactive use).
Has access to all tools, including write and shell.
(default: false)Headless on its own is the opposite of read-only, so in a scripted run --mode plan is the part carrying the gate. We couldn't exercise the two together, so take that from the flag semantics rather than from us.
Second gotcha, which stops you before anything else does: cursor-agent refuses to start in a directory it hasn't been told to trust, plan mode included. Ours came back with
⚠ Workspace Trust Required
Cursor Agent can execute code and access files in this directory.
Do you trust the contents of this directory?
/private/tmp/a-plan-check
To proceed, you can either:
• Run 'agent' interactively to decide
• Pass --trust, --yolo, or -f if you trust this directoryTrust the workspace once, interactively, before you script against it. Reaching for --yolo to get a read-only run moving is a trade nobody meant to make.
Cursor's own docs also give the clearest statement of the reject loop anywhere: whenever the output is not perfect you should avoid further prompts for correction, just revert the changes and focus on improving the plan, then run it again. That's what Cursor says — usually faster than correcting the agent and leads to cleaner output
In Antigravity CLI
So basically the binary has two execution modes, accept-edits and plan, and you pick one with a single --mode option. As of 1.1.9 the help lists both:
--mode Set the agent execution mode for this session (accept-edits, plan)It's either plan, or accept-edits, you can choose which one you'd like to use during a session by setting it in the --mode option on the very beginning (plan) or by pressing Shift+Tab to cycle between them until you see plan in the status bar, in both cases it will be set for the entire session.
Just so you know, the documentation is pointed about the flag being --mode and not --permission-mode — agy uses --mode for execution overrides.
Also /plan is different than the other harnesses, with the rest of them when you type /name it changes the behaviour of the CLI for the rest of the session (to accept-edits for instance) while with /plan we just prepend it to a prompt so using --mode plan you tell the CLI to always prepend /plan before your prompt and if you want to do it for a single prompt you can type /plan yourself. So /plan in Claude Code or Codex switches a mode; here it just decorates a message.
The plan is an Artifact as well, a markdown implementation plan that gets added to the same queue of approves/rejects for code diffs and you can navigate it using keys:
ctrl+r - open the Artifact Picker Panel
y - approve
n - reject
p - preview
shift+a - approve all
The reason this is particularly useful is that rejection is recorded on the Artifact itself, so the plan carries what was decided about it and you can always come back to it later.
Just as a heads-up if you'd like to use --print option with plan remember that it consumes the prompt as its own argument and -p is an alias for --print so for example it might be tempting to run:
agy -p --mode plan "add exponential backoff to the retry loop in app.py"but actually in such scenario --mode is being consumed by -p as it's the prompt of the --print option so the model sees --mode instead of your prompt and then tells you what --mode means, it doesn't show any error or warning. Instead you need to run:
agy --mode plan --print "add exponential backoff to the retry loop in app.py"The other thing is that during our test we observed the plan was created without reading a single file, the first point of the plan was about finding a function and it included configuration constants and HTTP status codes not present in the repository at all. We can see we had this write protection set up correctly and git status was showing no changes but it's hard to reject a plan if it doesn't have any base. So just to make sure always ask the model to first open the file and then do a show of the lines that are gonna be modified.
In Kimi Code CLI
There are four ways you can enter the planning mode from the user's side:
using --plan flag
running /plan or /plan on/off command in the session
hitting Shift-Tab
setting default_plan_mode to true in config.toml
In addition to that, the agent can enter it using EnterPlanMode tool.
The structure of the keys in the config file shows that this is a separate layer of a boolean nature which sits above the permission mode (not one of its options). So it's not that there are for example three values of default_permission_mode (manual, yolo and auto) and we set it to true or false (plan mode); instead we can set both default_permission_mode and default_plan_mode to anything. In other words, plan mode in Kimi is not the same as it's in Claude Code, where plan is one of permission-mode values. That separation is what decides how the two settings combine, which comes up in the caveats below.
Of all the harnesses in this lesson, Kimi probably follows the assumptions behind the lesson the closest. When you run ExitPlanMode it loads the plan file and displays it to you. Then it waits for your input. The approval panel you get in this situation is a little different than in other cases — it gives you four fixed options:
Approve
Reject
Reject and Exit
Revise
As you can see, even though you can reject the plan, it's a pre-defined button so it's not like you need to come up with an appropriate phrase; "rejecting" is different from "rejecting and leaving planning", for example. Additionally, if you want to, the agent can provide you with between one and three of its alternative solutions (with labels and descriptions), which you can choose during the approval phase as well. That's block 3 of the six, rendered as a menu.
What's more, across all Kimi's approval panels, hitting Esc, Ctrl-C or Ctrl-D is treated as a rejection (the same key we press to abort if something goes wrong during a turn is used to say "no" too).
But there are a few caveats:
plan mode doesn't mean read-only in Kimi, and the docs outline it precisely:
Write and Edit are available only for editing the current plan file
TaskStop is not allowed at all
the rest of tools (including Bash) are still subject to the existing permission rules
This means that if you have an allow rule for Bash in your config, you can use the same set of commands during planning as outside it — planning is exactly as loose as your rules are.
The only harness whose docs explicitly say that plan takes precedence over yolo is Kimi. In Kimi, using --yolo automatically approves calls of regular tools, and the docs say twice that it doesn't skip the plan-exit approval. Other harnesses are mostly quiet about this, but in Claude Code for example it's the opposite — if you start a session with bypassed permissions, then the plan mode's restrictions don't apply as well
auto mode approves the plan exits on your behalf, marking them as "Auto-approved" in the transcript
lastly, using --prompt and --plan is not possible; you can't even run the program with both of these flags, because non-interactive sessions use auto mode by default, so in such scenarios it's not possible to plan and postpone approval at the same time. You simply can't plan headlessly in Kimi
The limits
It's not a write lock
Claude Code:
Reads files
Runs shell commands to explore the repo
Writes a plan
Doesn't edit source code
Codex:
The boundary is repo-tracked state, so tests and builds that only write to caches or target/ are explicitly allowed
Kimi Code:
Write and Edit tools are restricted to the plan file
Two more tools are blocked outright while planning
Everything else including Bash is still bound by the normal permissions
Which means it's basically a tracked files lock, not a write lock in general
Approval ends it
According to the Claude Code docs, approving plan mode exits this mode and enters whatever the approve permission is called, which is editing for Claude Code so after that it's just a regular session with an attached document. So keep in mind you'll need to make sure the outcome is in line with the plan afterwards as that's your responsibility then
But there's one thing that makes it easy — the FILES block, diffing the change against it takes seconds and will catch any drift
It's not a specification
Plans are written before anyone touches the code, what matters during the implementation are the things we weren't aware of at the planning stage, and if you don't ask your agent to notify you about such things, they will go unrenegotiated
Headless has nobody to approve
If there's no one present to approve it, the approval step can't happen so where harnesses allow it, plan mode reduces to printing a plan and exiting. Which is useful in CI (you post it to the issue and a human approves later), but not an actual gate
Kimi Code goes a different way and doesn't even run in this setup
The gate isn't uniformly a gate
Claude Code:
If you enable bypass permissions, plan mode stops being a gate at all — it instructs the model to plan without editing but actually allows editing during planning
Kimi Code:
Plan-exit approval is still in place even with --yolo
So same feature name, opposite guarantees. Make sure which behaviour applies before relying on sth
Plan mode is a cheap point in time to say no, not no in itself, the no is usually the result of what you ask it for, and that prompt goes six blocks long
plan-request.md# Ask for a plan you can reject
Paste this under your task. Checked against the plan mode of Claude Code, Codex CLI,
Cursor, Antigravity CLI and Kimi Code CLI.
---
Before you change anything, give me the plan in exactly this shape:
1. GOAL — one sentence, as an observable end state.
2. FILES — every path you will create, edit or delete. Exact paths.
3. DECISIONS — each decision you made, the option you rejected, and why. One line
each. Mark any that could genuinely go either way as COULD GO EITHER WAY.
4. ASSUMPTIONS — anything you could not confirm from the repo, marked UNVERIFIED,
with the file or command I could check it against.
5. NOT DOING — the adjacent things you are deliberately leaving alone.
6. DONE — the check that proves it worked, as a command I can run.
Read the files before you write any of this. Do not write the implementation.
---
## Reading it
Read 4, then 5, then 3. Then the rest, if you still want to.
The step list is the least informative part of a plan and the first place your eye goes.
## Rejecting it
Name the block, name the line, give the reason. One line.
Assumption 2 is wrong — the timeout is set per-caller, see client.py.
Decision 3 went the wrong way. I want the configurable version.
NOT DOING is empty. Tell me what you're leaving alone.
DONE isn't a command. Give me something I can run.
Then: "give me the revised plan in full, not a diff of the old one."
These two don't work:
Try again. -> a re-roll, not a rejection; costs a whole pass
No, do it properly. -> some harnesses read this as "keep talking",
and you get discussion instead of a new plan
## Approving it
Only when you can answer: what would I observe if this turned out to be wrong?
If the answer isn't in DONE, the plan isn't finished.
## After approving
Approval opens the write gate. Diff the change against FILES — anything outside
that list is drift, and it costs you ten seconds to find it here instead of in review.
## Make it the default (optional)
Paste into your memory file (CLAUDE.md / AGENTS.md / the equivalent):
When I ask for a plan, use six blocks: GOAL (observable end state), FILES
(exact paths), DECISIONS (each with the option you rejected and why),
ASSUMPTIONS (marked UNVERIFIED, with how I'd check them), NOT DOING, and
DONE (a command I can run). Read the files first. Flag anything that could
go either way instead of picking silently. If I reject part of it, give me
the revised plan in full.