Permissions
Makima uses a permission system to decide what each tool is allowed to do and when to ask you first.
Rules come from four layers, combined for resolution:
- Session rules, set during the current session (in-memory only)
- Config rules, loaded from TOML permission files
- Builtin rules, the hardcoded defaults
- Plugin rules, declared by plugins via
maki.api.register_permission_rule
Any matching deny blocks the tool. No exceptions, so a config deny always beats a plugin allow.
A tool.<name>.input hook runs before any of this. Rules are
resolved against the call as the hook left it, so what the prompt shows you is
what runs.
Check Flow
For every tool call, each scope resolves like this:
tool call
│
deny rule matches? ── yes ──► blocked. no exceptions
│ no
allow rule matches? ── yes ──► runs
│ no
plan-file write? ── yes ──► runs
│ no
YOLO active? ── yes ──► runs
│ no
▼
default: prompt / allow / deny
Deny rules are checked across all layers before anything else, so a deny cannot be bypassed by YOLO. Mode tool selection and native write-target restrictions also apply before permission approval. A custom write-restricted mode can explicitly offer a third-party local tool. Native mutation tools must match the permitted normalized target, including after an input hook changes their arguments. MCP execution and deferred MCP search remain blocked in write-restricted modes. Approval and YOLO do not bypass these restrictions. default resolves per-tool first, then global. The built-in default is "prompt".
Mode restrictions do not provide a sandbox for plugin effects. Lua filesystem writes require the plugin's fs_write permission and have no plan-path restriction. See Modes for configuration ownership, admission snapshots, and asynchronous saving. If a pinned MCP prompt disappears before execution, setup fails rather than using a replacement.
Builtin Defaults
File-write tools are pre-allowed inside the project working directory (cwd at session start, canonicalized). Paths outside that tree still need a prompt or an explicit allow rule:
| Tool | Scope | Notes |
|---|---|---|
write | <cwd>/** | Outside cwd requires permission |
edit | <cwd>/** | Outside cwd requires permission |
multiedit | <cwd>/** | Outside cwd requires permission |
edit_lines | <cwd>/** | Same, when the opt-in tool is enabled |
insert_lines | <cwd>/** | Same, when the opt-in tool is enabled |
task | * | Subagent spawning always allowed |
The memory plugin uses a plugin rule to pre-allow the file-write tools inside its notes directory (under makima's state dir), so the agent can edit memory notes directly without a prompt.
These tools have no builtin allow rule, so they prompt (or follow your default) every time unless you add rules:
bash- Shell commands (scopes come from tree-sitter parsing)websearch- Web search querieswebfetch- URL fetching
Tools that never declare permission scopes (for example read, glob, grep, index, memory, skill, todo_write) skip the permission manager entirely. They always run. If you need to block one of them, turn the plugin off in init.lua (plugins.read = { enabled = false }) rather than using permissions.toml.
Container tools like batch and code_execution prompt for each inner tool individually.
TOML Configuration
There are two permission files:
- Global:
~/.config/makima/permissions.toml - Project:
.makima/permissions.toml(takes precedence over global)
default = "deny"
[bash]
allow = [
"cargo *",
"git *",
]
deny = [
"rm -rf *",
"sudo *",
]
[read]
default = "allow"
[mcp.deepwiki]
allow = ["search", "fetch"]
[mcp.github]
deny = ["admin_delete"]
Each tool gets its own section with allow and deny arrays. Values are glob-like scope patterns.
Note: In MCP server sections (
[mcp.*]), the boolean formsallow = trueanddeny = trueare deprecated and ignored. Usedefault = "allow"ordefault = "deny"instead. For native tool sections (e.g.[bash]),allow = truestill works.
The default key
Controls what happens when no allow or deny rule matches. Can be "prompt" (built-in default), "deny", or "allow". Set it globally or per-tool:
default = "deny"
[bash]
default = "prompt"
allow = ["cargo *"]
Here everything is denied by default, except bash which still prompts, and cargo * commands which are allowed.
Project files cannot set default = "allow" (top-level, per-tool, or MCP). That value is ignored so a project cannot grant itself full access. Project allow lists still work. Put default = "allow" only in the global file.
Scope Patterns
| Pattern | Matches |
|---|---|
* or ** | Any value (full wildcard) |
prefix* | Values starting with prefix |
cmd * | Bare cmd or cmd plus args (pwd * matches pwd and pwd -L, not pwdx) |
dir/** | dir itself or anything under it (path-aware on Windows and Unix) |
exact | Exact match only |
MCP Tool Permissions
MCP tools use natural TOML nesting. Server names are table keys under [mcp], tool names are array values:
# Global permissions.toml (default = "allow" is ignored in project files)
[mcp.deepwiki]
allow = ["search", "fetch"]
[mcp.github]
deny = ["admin_delete"]
[mcp.lean-lsp]
default = "allow" # allow all tools on this server (global only)
Tool names must match ^[a-zA-Z0-9_-]{1,64}$ (no dots, max 64 chars). Server names cannot contain dots.
Permission Prompts
When a gated tool needs permission, Makima asks you.
| Key | Action |
|---|---|
y | Allow once (immediate) |
s | Allow for this session (confirm with Enter or y; any other key cancels) |
a | Always allow for this project (confirm; saved to .makima/permissions.toml) |
A | Always allow globally (confirm; saved to ~/.config/makima/permissions.toml) |
n | Open deny guidance editor (type optional guidance, then Enter to deny once; Esc cancels) |
d | Deny always for this project (confirm) |
D | Deny always globally (confirm) |
Session and always-allow / always-deny choices need a second key (Enter or y) so a fat-finger does not rewrite your rules. Deny-once with n lets you type a short reason the agent will see.
Scope Generalization
When you pick "always allow" (or always deny for MCP), the saved scope is generalized so it stays useful beyond that one call:
- bash:
cargo test --allbecomescargo * - write / edit / multiedit / edit_lines / insert_lines:
/path/to/file.rsbecomes/path/to/** - MCP tools: always
*(per-tool, so allowingdeepwiki.searchwill not coverdeepwiki.fetch) - webfetch / websearch (and anything else gated): the exact URL or query string is stored as-is
For MCP tools, both allow and deny decisions generalize to * (the entire tool). MCP inputs are opaque JSON with no meaningful scope pattern. Denying a single MCP invocation denies that tool until you revoke the rule.
Auto Mode (bash)
Auto mode puts a separate-context classifier model in front of every bash command. A silent, throwaway session (empty history, its own model unless auto_model pins one) reviews the command against a safety policy and returns accept or deny with a one-line reason. Approved commands run as usual. A deny or classifier failure asks you for permission first (unless YOLO is on, see below): the normal allow / deny / remember prompt appears and you decide. A classifier failure never approves a command by itself.
The classifier sees only the command and working directory in a silent throwaway session. Its turn is never relayed into your main conversation, so nothing about the main run leaks into its decision and its reasoning does not clutter the thread. The default system prompt is the codex-guardian policy (bundled, auto_classifier_prompt.lua).
Enable it like YOLO:
-- ~/.config/makima/init.lua
maki.setup({
plugins = {
bash = {
auto_mode = true,
},
},
})
Or toggle it live with /automode, or start the session with --automode. The live /automode toggle wins while running; a config or --automode seed is only the starting state.
The tri-state verdict is strict:
- approve (
approved: true) runs the command. The tool view shows anauto-mode: allowedline so you can see the classifier let it through. - deny (
approved: false) asks you for permission first: the same allow / deny / remember prompt as any other gated tool. The command then runs exactly as it would with auto mode off: allow once, allow for the session, or always-allow all work, and your existing permission rules still apply. With YOLO on there is no prompt and the deny rejects the command with the classifier's reason instead. - error (classifier failure, timeout, or a non-boolean verdict) asks you for permission through the same fallback as deny. The command never auto-runs. With YOLO on, the error rejects the command without prompting.
Things to watch:
- A deny or classifier error asks instead of rejecting, so a stricter or unreliable classifier means more prompts, not silent rejections. If that creates too much friction, tune the prompt, choose another
auto_model, or disable auto mode. - Permission rules (allow once, allow for the session, always-allow, explicit denies) enter through the deny and error fallback prompts. An approve verdict remains the classifier's call alone.
- YOLO makes deny and error verdicts reject outright with no prompt. This is YOLO's "no prompts, gates stay authoritative" reading, consistent with explicit deny rules still applying under YOLO.
auto_modelis optional. Unset, the classifier runs on the same model as the current session; set it to pin a dedicated classifier model (a cheap one keeps the gate fast).auto_timeout_secslimits each classification without reducing the command's execution budget.
The /automode toggle only flips auto_mode; it does not change the deny-to-prompt behavior.
YOLO Mode
To skip prompts on gated tools, toggle YOLO with /yolo, or run with --yolo. Explicit deny rules still apply. Tools that never declare permission scopes are unaffected (they never prompted).
To start in YOLO mode every time:
-- ~/.config/makima/init.lua
maki.setup({
always_yolo = true,
})Bash Command Parsing
Bash commands get parsed with tree-sitter to extract individual commands. Something like cd /tmp && cargo test is checked as two separate commands.
Some constructs are too complex to analyze statically, so they always trigger a prompt:
- Command substitution:
$(...), backticks - Process substitution:
<(...),>(...) - Subshells:
(...) - Arithmetic expansion:
$((...))
Brace groups { ... } and control flow (if, for, …) are segmented when possible; they do not by themselves force a prompt the way substitutions do.
Plugin Permissions
Lua plugins have a separate, unrelated gate. A plugin.toml manifest next to the Lua file controls which gated maki.* APIs it may call. No manifest means every gated call is denied, including for your own init.lua. The Lua API reference documents the manifest and lists every permission.
Network Addresses
webfetch, websearch and every plugin that calls maki.net go through one guard. A request to a private, loopback or link-local address is refused, and so is a redirect that lands on one. The model picks these URLs, so a page it reads could otherwise talk it into fetching http://169.254.169.254/ or an admin panel on your LAN.
To reach a service on your own machine or network, list it in net.allowed_private_hosts. An allowed host also keeps plain http:// instead of being upgraded to https://, since a service on your LAN rarely has a certificate.
Session Persistence
When you save a session, its permission rules are saved too. Loading the session restores them.