Modes
/mode takes no argument and cycles read-only to approve-each to auto and back around.
Shift-Tab does the same without typing. There is no /mode auto form, and typing one is a
command error rather than a mode change. The current mode is shown in the session.
approve-each is the default, not read-only, and that was a measured decision rather than
a preference. A fresh session defaulting to read-only and given a write task was blocked
repeatedly and produced nothing: five denials, then a turn that ended with no tool call and no
file created. What keeps approve-each from being an every-call prompt is the remembered
approval below.
A task that needs to write in read-only reports the block rather than failing the run, and the
run still exits successfully. Cycle out of it and ask again.
Plan mode is not a fourth row in this table. /plan is a TUI overlay. While
it is on, the gate is read-only even under --dangerously-skip-permissions, and the indicator
reads ⏸ plan mode on rather than the stored /mode. /mode and Shift-Tab still cycle the
stored session mode when the overlay is off. While it is on, Shift-Tab does nothing. That stored
mode applies again when you leave the overlay.
What counts as a write
The classification is per tool, and it is deliberately asymmetric.
A name the gate does not recognise is treated as a write. That direction is the point. MCP
opens the tool set to third parties, so “absent from the write list” stops being evidence of
safety. An unfamiliar tool costs you an approval instead of being waved through in all three
modes.
Two names are exceptions on every turn, and neither writes.
dispatch_subagents hands the child
the parent’s own permission mode, so a subagent’s bash re-enters this same gate at the same
mode rather than escaping it. skill reads one file you put in .seri/skills/ yourself.
Plan mode adds ask_plan_questions and submit_plan as reads on that
parent turn. The harness writes the plan file after submit_plan. The model does not hold
write_file.
See Tools for what each one does.
Remembering an answer
An approval prompt can remember your answer. “Always” is narrower than it sounds, in two directions at once. It is scoped to that one tool, and to the project you granted it in. Grants live inpermissions.yaml in your profile root, in two tiers.
Answering “always” only ever writes the project tier. Nothing in seri promotes a grant to
global:, so an approval you gave in one repository does not follow you into the next one.
The project is keyed on the resolved worktree path, case-folded on Windows and macOS, where the
same directory routinely has two spellings.
Only three kinds of grant can persist at all, and the list is closed.
bash and powershell cannot be approved permanently at all. A grant keyed on a tool name
says nothing about what a shell command will do, so an entry reading bash would hand over the
shell for good. seri refuses such an entry on read as well as on write, so hand-editing one into
the file does not work either.
An MCP grant is stored as mcp_<server>_<tool>@<digest>. The digest fingerprints that tool’s
name, description and input schema at the moment you approved it, because a server is a third
party and those three together are the whole contract you were shown. Change the tool on the
server’s end and the digest stops matching, so seri asks again rather than trusting your old
answer about a tool you never actually saw.
Reviewing what you granted
/permissions lists the write_file and edit grants in effect for the project you are in, and
labels each one by the tier it came from.
A tool with no grant in either tier is left out rather than shown as a third state, and grants
belonging to other projects are not listed.
Skipping the gate
--dangerously-skip-permissions runs every tool with no prompt for the whole process.
Cycling to auto is the same idea inside the session, with a keystroke to get back out of it.
Plan mode still strips writes from that parent turn, including under this
flag.
--permission-prompts none is narrower. The permission mode still decides. Anything that would have
asked is denied instead. Reads in approve-each still run. See
Launching seri.
Deny rules
permissions.yaml also has a deny: list. seri never writes it. You add entries by hand.
glob, grep, read_file, or write_file, then a path pattern. A missing
path still comes back as a permission denial, not as “path not found”, so a search cannot probe
whether the file exists. A search rooted above a denied tree still descends into it.
Each entry must be that tool(pattern) string. A number, a boolean, or an object such as
path: /tmp/secret is skipped. seri warns with deny[n] and the type it saw. Sibling string
rules still apply. seri doctor fails the permissions row on those warnings and tells you to
edit the list. Do not delete the file, or you drop the string denials that still work.
A deny runs before a PreToolUse hook and before the gate. /mode does not lift it.
Paths outside the working directory
A path-bearing tool (read_file, grep, glob, write_file) that points outside the session’s
working directory asks once for that run. A second call in the same run does not ask again.
bash, edit, and MCP tools have no working-directory question.
On a real terminal, a “yes” latches for the rest of the run. A “no” blocks further outside paths
without asking again. A dummy prompt, a non-TTY run, and --permission-prompts none are not a live
human: the call is blocked, not declined.
SERI_BLOCK_READS_OUTSIDE_WORKING_DIRECTORIES=true is a standing deny. Only the exact string
true turns it on. Unset keeps the one-shot prompt. /config edits the same key. See
Settings.
Packed renderer URLs in auto
Inauto, a bash, powershell, or other network write that packs a substantial payload into a
public diagram-renderer URL is blocked. The hosts are mermaid.ink, mermaid.live, kroki.io,
plantuml.com, and www.plantuml.com. The block does not apply when this turn asked to render or
export on that renderer.
grep, write_file, and memory_write still run when the same URL is only in the file or the
pattern. approve-each still asks. /mode to auto is what this rail is for.
Classifier blocks in auto
permissions.yaml may set autoModeOnBlock: ask or deny. In auto, a classifier match is
deny unless you set ask, which falls through to the ordinary permission prompt with the
classifier’s reason attached. A non-TTY run is always deny.
The built-in classifier currently allows every call. The key is what a later deny-by-default
class uses. seri never writes this key.
Containment
bash, powershell, and MCP calls are screened for inspectable containment escapes: cloud
metadata (IMDS) fetches, egress evasion, and cross-tenant identity verbs. Other tools pass without
inspection, including write_file with an IMDS-shaped path.
A hit is a permission denial with reason containment. It runs before a PreToolUse hook and
before the gate, in every permission mode, so you are never asked to approve a call this rail will
refuse. /mode and --dangerously-skip-permissions do not lift it. Unparseable inspectable input
always blocks.
SERI_CONTAINMENT_ESCAPE_EXPECTED=true skips a table hit. Only the exact string true is
expected. Unset, TRUE, and an empty value are not. Unparseable input still blocks when expected is
set. This key is not in /config. See Settings.
What the gate cannot override
These rails run before the gate, in this order, and none of them is a/mode question.
- A
deny:path rule. - Containment screening on inspectable shell and MCP calls.
- A
PreToolUsehook. See Hooks.
auto and --dangerously-skip-permissions
included, and you are never asked to approve a call a hook is about to refuse. Path deny and
containment are the same shape: a diagnosis, not a prompt.
