plan), edit files without asking (acceptEdits), or never
pause (dontAsk). Switch it in the middle of a session, the way you would work
with a coding agent: plan first, then let it edit.
needsApproval, not a second system: they apply last, to what the rules,
guardrails and the tool’s own needsApproval decided.
The modes
plan: a call to a tool that is not read-only is refused withkind: 'denied'and the reasonThe agent is in plan mode: it may read but not change anything. Describe the change instead.- also when anallowrule matched it. A run that starts in plan mode also gets one paragraph in its system prompt telling the model so. A call to an unknown tool still gets the normal not-found error, so the model sees the real problem.acceptEdits: a call that would pause for approval runs without asking when its tool is a file edit (editsFiles, below). Everything else is unchanged.dontAsk: a call that would pause for approval - anaskrule,needsApproval, orask_question- is refused with the reasonThe agent is in dontAsk mode: calls that need approval are refused.Calls anallowrule approved and calls that need no approval run. Nothing pauses, so anapprovecallback is never called.
Which tools are read-only
Plan mode lets a tool run only when it says it changes nothing. A tool that says nothing is treated as one with side effects and is refused.- A tool with
annotations: { readOnlyHint: true }. Built in:read_file,list_dir,glob,grep,todo_read,current_date,day_name,web_fetch,load_skill, everyrecall_<name>memory tool, andagent_status/agent_await(they watch background sub-agents). An OpenAPI tool for aGEToperation has it too, and an MCP tool keeps the hint its server sent: it is the server’s word, not a guarantee, so only connect servers you trust. - The built-in
ask_question(it only asks; the call still pauses for the answer) andtask(its sub-agent inherits plan mode, see below). - Hosted provider tools run inside the provider’s request,
so no call can be refused. Plan mode sends
webSearch()andfileSearch()(they read) and leavescodeInterpreter()and everyhostedTool()out of the model request; the other modes send them all.
File edits: the editsFiles marker
acceptEdits approves only tools marked as file edits: write_file and
edit_file from createFsTools(), and any tool defined with editsFiles: true
(stored as metadata.editsFiles). A tool is never a file edit by its name
alone, so a tool of your own named write_file is not approved silently.
Order of evaluation
For each tool call, in this order:- Argument validation, then
preToolCallhooks. A hook deny ends here. - Permission rules (
permissions). Adenyrule ends here. - Tool guardrails. A block ends the run.
- The tool’s own
needsApproval. A deny ends here; analloworaskrule replaces its ask. - The permission mode, on what is left.
deny rule and a
needsApproval deny still deny in every mode, and acceptEdits never approves
a call a guardrail blocked.
Setting and switching the mode
createAgent({ permissionMode }): the agent’s mode. A function is read at every tool call, so a mode you switch while a run is going applies to its next call.agent.send(message, { permissionMode })/agent.stream(...): the mode for that run only.agent.session({ permissionMode }), thensession.setPermissionMode(mode)and thesession.permissionModegetter. The session reads its mode at every tool call of a turn, sosetPermissionMode()during a turn (from atool.startlistener, say) applies from the next tool call of that turn. A session defaults to the agent’s mode.
setPermissionMode(). Each switch is
reported to onPermissionModeChange with { sessionId, from, to, at }:
The audit log
While a mode other than'default' is set, every tool call’s decision is
audited, even when the agent sets neither permissions nor
onPermissionDecision: it goes to onPermissionDecision (when set) and is
streamed as a permission.decision event. The entry’s mode field is set when
the mode changed the call’s outcome: 'plan' or 'dontAsk' with
decision: 'deny' and the mode’s reason, 'acceptEdits' with
decision: 'allow'. When the mode denied a call an allow rule matched, the
entry keeps that rule’s rule.index. Mode switches go to
onPermissionModeChange. onPermissionDecision gets who the run acts for as
its second argument, { principal }; the entry and the event carry no caller
identity (see auth).
Sub-agents
A sub-agent (thetask tool, a background task, createDelegateTool()) runs
under the lead’s mode while the lead’s mode is not 'default', and under its
own permissionMode otherwise. A sub-agent created with
permissionMode: 'plan' stays in plan mode whatever the lead’s mode is. The mode is read at each of the sub-agent’s tool
calls, so switching the lead’s session mode applies to a sub-agent that is
running. That is what makes task safe in plan mode: the sub-agent cannot
change anything either.
A remote sub-agent (remoteAgent()) runs on another server
under that server’s own permissions, so it cannot be held to the lead’s mode: in
plan mode a task call to it is refused without contacting it, and in
dontAsk mode a remote approval fails the task instead of pausing the lead.
Resuming after an approval
The mode is not saved in checkpoints or approval snapshots. A run continued byagent.approvals.resolve() uses the paused session’s current mode when the
paused run belonged to a session, else the agent’s mode (not the mode a
send() call passed). So a turn paused in 'default' and resolved after
session.setPermissionMode('dontAsk') runs the approved call and then refuses
the calls after it that would ask. In plan mode the approved call itself is
refused too when its tool is not read-only.
A resume in another process uses the mode the resuming agent or session has.