Configure tool permissions
Declare which tool calls, shell commands, and file paths are allowed, require confirmation, or are forbidden.
What permissions control
Cantrip wraps every subagent tool call in a three-layer gate. User hooks run first and can mutate or veto calls. Governance policies come next — coarse, category-scoped allow/deny/review. Permissions are the declarative layer on top, matching the post-hook arguments so a command your hook rewrote still sees the right pattern.
Each call resolves to one of three outcomes:
allow— the call proceeds silently.ask— the subagent pauses while a CONFIRM task surfaces on the queue; the user typesyesornoto unblock it. A timeout auto-denies.deny— the call is refused with a clear error; the agent sees the denial and can course-correct.
File locations
Permissions are read from two files, composed in order so repo rules override user rules when both match:
~/.config/cantrip/permissions.yaml— your personal defaults, applied to every charm.<charm>/.cantrip/permissions.yaml— the per-charm overlay, committed alongside the code.
Both files are optional. When neither exists, the built-in defaults (below) still apply.
Schema
tools:
"<tool-name-glob>": "allow" | "ask" | "deny"
bash:
"<command-glob>": "allow" | "ask" | "deny"
paths:
"<path-glob>": "allow" | "ask" | "deny"
agents:
<category-name>:
tools: { ... }
bash: { ... }
paths: { ... }
bash_tools:
- run_command
The three sections match independently; the
most restrictive outcome wins when they disagree
(deny > ask > allow). Within a
section, last-match-wins — the later-written
glob takes effect. Globs use standard Unix shell syntax
(*, ?, [abc]) and are
case-sensitive.
What each section matches
tools— globs on the tool name (fs_read,juju_deploy,git_*).bash— globs on the shell command string passed to tools listed inbash_tools(default:run_command). Anargvlist is shell-joined before matching.paths— globs on thepath,file_path, orfilenameargument of any tool call.
A typical file
tools:
# Shell wrappers always need a second look.
"run_command": "ask"
bash:
# Specific commands get more specific answers. The later rule
# for ``git push`` wins for those commands; everything else that
# just hit "*" stays ``ask``.
"*": "ask"
"git status": "allow"
"git log *": "allow"
"git push *": "ask"
"rm -rf *": "deny"
"sudo *": "ask"
paths:
# Credentials are never OK to read.
"*.env": "deny"
"**/secrets/**": "deny"
agents:
# The research subagent is read-only — writes are out.
research:
tools:
"fs_write": "deny"
"charmcraft_*": "deny"
Built-in defaults
Even with no file present, Cantrip ships these safe defaults:
rm -rf *andrm -fr *—deny.sudo *andgit push *—ask.- Reads of
.env(including*.envand*/.env) —deny. - Everything else —
allow.
Your rules override them because built-ins are composed first and your file appends later.
Per-agent overrides
The agents: block tightens or loosens rules for a
specific subagent category. Agent names match the task category
(research, build, deploy,
test, debug, infra). Per-agent
rules are evaluated after the top-level rules, so a
later-written agents.research.tools entry overrides
any earlier global rule.
Transferring from OpenCode
The section / outcome shape is deliberately identical to
OpenCode's opencode.json permission: block,
so a rule like {"bash": {"*": "ask", "git *": "allow", "rm *":
"deny"}} carries over one-for-one. The only differences are:
- Cantrip's file is YAML, not JSON.
- Cantrip adds a
pathssection with argument globbing for file tools. - Cantrip's per-agent key is
agents:(matching subagent categories) rather than a flat scope.
Resolving an ask
When a subagent hits an ask, a CONFIRM task appears
in the work-queue widget with the tool name, the reason the rule
matched, and the command or path at issue. Respond in the chat
with yes to approve or no to refuse.
A PERMISSION_DECIDED event lands on the event bus each
time an ask opens or resolves, so the TUI and Web
transcripts record the full decision trail.
Approvals are scoped to that single call — approving
git push origin main does not approve a later
git push --force. Edit the rule to
allow if you want the decision to stick.
What permissions do not do
- They do not bypass governance policy. A tool
category blocked by
cantrip.policies.yamlstays blocked even ifpermissions.yamlsaysallow. - They do not replace the subprocess sandbox.
A
bash: "*": "allow"still runs inside the namespace-isolated shell; permissions gate whether a call fires, not how. - They do not apply to MCP-provided tools — those are
gated by per-server
allowed_toolsinmcp.yaml.