Permissions
Every tool call — in the CLI, the Web UI, or an IM channel — passes through the same permission engine before it runs. This page is the rule-file reference; see The agent loop for where it sits in the bigger picture.
~/.octo/permissions.yml
Section titled “~/.octo/permissions.yml”Top-level keys are tool names; each holds an ordered list of rules. Each rule is exactly one of
allow:, deny:, or ask:, and each clause is exactly one of pattern (a substring match, for
terminal), hostname (a glob list, for web_fetch), or path (a glob list with $CWD
expansion, for the file tools). Matches are resolved by tier, not declaration order — deny beats
ask beats allow — and the first match within the winning tier is reported as the reason. No
match in any tier falls through to ask.
terminal: - deny: { pattern: "rm -rf /" } - ask: { pattern: "sudo " } - allow: { pattern: "git status" } # anything else => implicit ask
web_fetch: - deny: { hostname: ["10.*", "192.168.*", "127.*", "localhost", "*.local"] } - allow: { hostname: ["github.com", "*.github.com"] }
write_file: - deny: { path: ["**/.ssh/**", "/etc/**", "**/.env"] } # no allow rule for $CWD/** here — see the note belowBeing inside $CWD is not a free pass for write_file/edit_file — only read_file treats
the whole filesystem (minus the credential-path denies) as safe to read without asking. A write or
edit anywhere, cwd included, falls through to the implicit ask unless a rule says otherwise, so
--permission-mode below is what actually decides whether it prompts, auto-allows, or denies.
A tool key you write fully replaces the built-in default rule list for that tool — it doesn’t merge with it. Add back anything from the defaults you still want.
Matching semantics
Section titled “Matching semantics”- A
terminalpattern ending in/or~is boundary-anchored:deny: {pattern: "rm -rf /"}blocks a root wipe but notrm -rf /Users/me/project. terminalallow rules are stricter thandeny/ask: the command must start with the pattern (after trimming) and contain no shell-chaining metacharacters (; | & $ ( ) < > ```` newline) — sols && rm -rf /can’t ride through anallow: "ls"rule.hostnameglobs match one DNS label per*—*.devmatchesfoo.dev, notfoo.bar.dev.pathglobs support**for any number of path components;$CWDexpands to the engine’s working directory at construction time.- A missing
permissions.ymlisn’t an error — the embedded defaults apply on their own.
--permission-mode
Section titled “--permission-mode”Three values, resolved with the same precedence as everything else (flag > config.yml >
built-in default):
| Mode | What it does to an ask verdict |
|---|---|
interactive (default for the main CLI) |
Passes through unchanged — the caller prompts |
auto |
Resolved to allow, no prompt |
strict |
Resolved to deny — the posture for evals, the IM bridge, and other unattended runs |
Mode only ever resolves the implicit or explicit ask case — an explicit allow or deny from a
matched rule is never overridden by the mode.
Remembering a choice
Section titled “Remembering a choice”Answering an interactive prompt with “always” allows that exact (tool, input) pair for the rest
of the session only — it’s never written to permissions.yml; durable policy stays a deliberate
file edit. This is available identically on all three transports (a TUI/Web modal’s “always” option,
or replying always / always allow / 总是允许 in a chat channel).
write_file/edit_file are the one exception to “exact (tool, input)”: their input also carries
the new content, which differs on every call, so remembering the whole input would never hit the
cache a second time. Those two tools remember by path alone — approve one edit and the rest of the
session doesn’t ask again for that file, but a different file still prompts once.
A deny rule always beats a remembered allow — the rule scan runs first and only consults the
remembered cache when the rule verdict isn’t deny. So tightening permissions.yml after a user
said “always allow” takes effect on the very next call. Flipping to strict mode, on the other
hand, does not retroactively revoke something already remembered — mode only governs unanswered
future prompts. The remembered cache itself is dropped whenever the underlying session is: on
session delete in the Web UI, or on /bind / /unbind / /new / /clear in an IM channel (see
Slash commands).
Next: a PreToolUse hook can add stricter gates on top of these rules — it
can never loosen them, since an explicit deny from a rule is final.