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:

ToolMatch againstExample
bashThe shell commandbash(git *), bash(git push *)
read / write / edit / grepThe path argedit(./src/**), read(**/*.md)
globThe pattern argglob(**/*.go)
web_fetchThe URLweb_fetch(domain:example.com)
web_searchThe queryweb_search(*), web_search(rust *)
spawn_agentThe role argspawn_agent(reviewer)
anything else (MCP, custom)All args as k=v stringmcp__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, so bash(git *) will not auto-allow git status; rm -rf ~. Opt in explicitly with patterns like bash(git * | *) if you genuinely want pipes auto-allowed.
  • Deny rules are segment-aware: bash(rm -rf *) blocks chained variants like do_evil; rm -rf /, cd / && rm -rf *, and ls | 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, ./rmrm), 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 delete

The modal still shows up; declining still works.

.ensoignore#

A first-class file at the project root, gitignore-style:

# .ensoignore
secrets/**
*.pem
.env
config/credentials.toml

Each 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:

ToolGeneralisation
bashFirst word + * (so git status becomes bash(git *)).
read/grepProject-scoped: a path inside cwd → <tool>(<cwd>/**); a path outside cwd → that exact cleaned path. Remembering a read no longer grants whole-filesystem access.
globThe exact pattern you ran: glob(<pattern>).
write/editExact path: write(src/x.go) or edit(.env).
web_fetchExact 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):

  1. /etc/enso/config.toml — system-wide.
  2. $XDG_CONFIG_HOME/enso/config.toml (≈ ~/.config/enso/) — user.
  3. <cwd>/.enso/config.toml — project, committed.
  4. <cwd>/.enso/config.local.toml — project, gitignored. The “Allow + Remember” target.
  5. -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).