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:

File locations

Permissions are read from two files, composed in order so repo rules override user rules when both match:

  1. ~/.config/cantrip/permissions.yaml — your personal defaults, applied to every charm.
  2. <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

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:

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:

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