Skip to main content
Every tool call passes a gate before it runs, on every OS. The gate is the base safety layer, and nothing else in seri reaches around it.

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 in permissions.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.
An MCP grant does not appear in /permissions. The prompt offers “always” for an MCP tool and the store accepts it, but the panel only ever renders the two built-in names, so a permanent MCP approval can be made and then neither seen nor revoked there. Edit permissions.yaml in your profile root to remove one.

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.
--dangerously-skip-permissions and auto are for attended, high-trust work. Neither is a default, and neither is remembered for you. --permission-prompts none does not turn the gate off. It only removes the prompt.

Deny rules

permissions.yaml also has a deny: list. seri never writes it. You add entries by hand.
A rule names one of 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

In auto, 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.
  1. A deny: path rule.
  2. Containment screening on inspectable shell and MCP calls.
  3. A PreToolUse hook. See Hooks.
A hook that blocks a call blocks it in every mode, 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.