Permissions#
The permission system gates every tool call by matching it against
patterns in [permissions]. Three rule lists, evaluated in
deny → ask → allow → mode default order.
Modes#
[permissions]
mode = "prompt" # "prompt" | "allow" | "deny""prompt"— for unmatched calls, ask the user via a modal."allow"— for unmatched calls, auto-allow."deny"— for unmatched calls, auto-deny.
--yolo (or /yolo on) overrides the mode and auto-allows everything
except patterns explicitly listed in deny. Use it for unattended
runs.
Pattern syntax#
All patterns have the shape tool(arg-pattern):
allow = ["bash(git *)", "edit(./src/**)", "web_fetch(domain:example.com)"]
ask = ["bash(git push *)"] # always prompt, even when otherwise allowed
deny = ["bash(rm -rf *)", "edit(./.env)"]Per-tool argument matching:
| Tool | Match against | Example |
|---|---|---|
bash | The shell command | bash(git *), bash(git push *) |
read / write / edit / grep | The path arg | edit(./src/**), read(**/*.md) |
glob | The pattern arg | glob(**/*.go) |
web_fetch | The URL | web_fetch(domain:example.com) |
web_search | The query | web_search(*), web_search(rust *) |
spawn_agent | The role arg | spawn_agent(reviewer) |
| anything else (MCP, custom) | All args as k=v string | mcp__github__list_issues(repo=foo) |
Bash patterns:
- A single token (
bash(git)) matches the command’s first word only. - Multi-word patterns (
bash(git push *)) match the whole command with*crossing spaces and slashes. - Allow rules gate on shell metacharacters: any of
;&|<>$`()\newline present in the command must also appear in the pattern, sobash(git *)will not auto-allowgit status; rm -rf ~. Opt in explicitly with patterns likebash(git * | *)if you genuinely want pipes auto-allowed. - Deny rules are segment-aware:
bash(rm -rf *)blocks chained variants likedo_evil; rm -rf /,cd / && rm -rf *, andls | rm -rf *by splitting the command on top-level shell separators. Each segment is tested both raw and normalised — collapsed whitespace (rm -rf), path stripped to basename (/bin/rm,./rm→rm), shell-escapes removed (\rm,r\m), command word unquoted ("rm"), and command-substitution bodies ($(...), backticks, one level of nesting) re-split and re-tested — so$(rm -rf /)and/bin/\rm -rf /are caught too. Deny rules are still guardrails, not walls — they don’t follow interpreter indirection (eval,sh -c,xargs), process substitution, or here-docs. For real isolation against a hostile model or hostile codebase, set[backend] type = "podman"(or"lima").
Path patterns (read/write/edit/grep/glob) use doublestar globs.
./src/** matches everything under ./src/ recursively.
web_fetch(domain:...) matches the URL’s host (case-insensitive).
The bare-host pattern domain:example.com also matches subdomains
like api.example.com.
The ask tier#
ask rules force a prompt even when the call would otherwise be
auto-allowed. Useful for blast-radius commands you’ve broadly
permitted:
allow = ["bash(*)"] # let the agent run anything…
ask = ["bash(git push *)", # …but always confirm a push
"bash(rm -rf *)"] # or a recursive deleteThe modal still shows up; declining still works.
.ensoignore#
A first-class file at the project root, gitignore-style:
# .ensoignore
secrets/**
*.pem
.env
config/credentials.tomlEach non-empty, non-comment line is added as a deny pattern for
read, write, edit, grep, and glob. Patterns are also fed to
the @-file picker so ignored files don’t appear there.
! negation is not supported — use explicit [permissions] allow
rules for exceptions.
“Allow + Remember” + turn-scoped grants#
When a permission prompt fires, the modal offers four decisions:
y— allow this single call.n— deny (Esc is a shortcut for deny).a— Allow + Remember: allow this call and persist a rule.t— turn-scoped: allow this call (and any further calls matching the same pattern) for the rest of the current turn only. The grant is dropped when the turn quiesces, so the next user message starts from the same baseline. Useful when the model is about to chain several similar tool calls and you don’t want to either prompt for every one or commit to a permanent rule.
a (allow + remember) writes the pattern to
<cwd>/.enso/config.local.toml (project-scoped, gitignored). The
pattern derivation:
| Tool | Generalisation |
|---|---|
bash | First word + * (so git status becomes bash(git *)). |
read/grep | Project-scoped: a path inside cwd → <tool>(<cwd>/**); a path outside cwd → that exact cleaned path. Remembering a read no longer grants whole-filesystem access. |
glob | The exact pattern you ran: glob(<pattern>). |
write/edit | Exact path: write(src/x.go) or edit(.env). |
web_fetch | Exact URL. |
| anything else | <tool>(*). |
You can also write rules manually anywhere in the layered config — project, user, or system level. See Config reference for the layering rules.
additional_directories#
Workspace-extension setting — tell the agent (and the @-picker) about directories it can operate on alongside cwd:
[permissions]
additional_directories = ["~/notes/projects/alpha"]The directories are mentioned in the system prompt so the model knows they exist. The @-picker walks them. Combined with the sandbox, file tools are confined to cwd + these directories.
Layered config and where rules live#
Permission patterns merge across these files in order (lowest → highest precedence):
/etc/enso/config.toml— system-wide.$XDG_CONFIG_HOME/enso/config.toml(≈~/.config/enso/) — user.<cwd>/.enso/config.toml— project, committed.<cwd>/.enso/config.local.toml— project, gitignored. The “Allow + Remember” target.-c <path>on the command line — one-off override.
The security lists (permissions.allow/ask/deny and
web_fetch.allow_hosts) are unioned across tiers — deduped and
grow-only — so a higher-priority layer can’t wipe a more-trusted tier’s
deny list with deny = [], and deny always wins in matching. One
project remembering bash(make *) doesn’t leak the rule to another
project. Both <cwd>/.enso/config.toml and config.local.toml are
trust-gated: a project-supplied one you didn’t author trips the trust
prompt before it loads (enso’s own “Allow + Remember” writes are
auto-trusted).