05. Permissions, Plan Mode, and Safety

Permission rules and hook blocks

Modes set how often Matcha CLI asks. Rules and hooks set standing yes / ask / no for specific tools, paths, and commands.

Authorization order

A tool call is authorized in this order:

  1. PreToolUse hooks. An explicit hook deny stops the call before any permission check. A hook allow does not skip the rest; it only declines to deny.
  2. Permission rules (deny > ask > allow), merged from every source. deny wins over allow and over always-approve’s normal pass-through.
  3. Remembered grants from earlier cards (when remember_tool_approvals is on), scoped to this project.
  4. Built-in auto-approvals (read-only tools and the read-only command list).
  5. Prompt policy from the current mode (ask / auto / always-approve / dontAsk).

Always-approve short-circuits after step 2: deny rules, hooks, and shell-segment ask rules still apply; remembered grants are skipped.

Where rules live

Rules come from several files and are merged into one set. Severity, not file origin, decides the winner.

ScopeFile
Global~/.matcha/config.toml
Project (shareable)<project>/.matcha/config.toml (every level from the repo root down to cwd)
Project (personal)<project>/.claude/settings.local.json
Managed/etc/matcha/managed_config.toml, ~/.matcha/managed_config.toml
CLI--allow RULE, --deny RULE (repeatable)

There is no native config.local.toml. Interactive “Always allow” decisions are stored outside the repo, per project.

Rule syntax

Native structured form:

[permission]
rules = [
  { action = "allow", tool = "bash", pattern = "git *" },
  { action = "deny",  tool = "bash", pattern = "rm -rf *" },
  { action = "ask",   tool = "edit" },
]

Compact string form (same language as --allow / --deny and Claude settings):

[permission]
allow = ["Bash(git *)", "Grep"]
deny  = ["Bash(rm -rf *)"]

tool names in the structured form are lowercase: bash, read, edit, grep, mcp, webfetch, websearch. String rules use Bash, Read, Edit / Write, Grep / Glob, MCPTool, WebFetch, WebSearch. A bare * matches every tool.

Rules are read when the session starts. Edit the file, then start a new session.

Hooks that matter here are PreToolUse. Project hooks need folder trust (/hooks-trust or --trust). Global hooks in ~/.matcha/hooks/ are always trusted. Hook install and the full lifecycle are covered in Extending Matcha.

When to use rules and hooks

Use a narrow allow for the commands you run every day (git, cargo test) so ask mode stays quiet. Use deny for paths and commands that must never run, including under always-approve. Use a PreToolUse hook when the policy is “only these prefixes, in every mode, including chains I cannot express as one glob.”

Add a project allow / deny

# .matcha/config.toml
[permission]
allow = ["Bash(cargo test *)", "Bash(npm run build)"]
deny  = ["Bash(rm -rf *)"]
matcha -p "Review the API changes" \
  --allow 'Bash(git *)' \
  --deny 'Bash(rm -rf *)'

Bash(git *) is prefix-plus-glob (a trailing space and * keep git a whole word). Bash(git) also matches gitleaks. Matching is case-sensitive.

Chains

deny and ask inspect every segment (&&, ||, ;, |) and the whole string. One denied segment rejects the command. allow matches the whole string only, so Bash(git *) would allow git status && rm -rf /. Pair narrow allows with denies.

Headless with a hard floor

matcha -p "Implement the feature using only git and GitHub CLI" \
  --allow 'Read' \
  --allow 'Grep' \
  --allow 'Bash(git *)' \
  --allow 'Bash(gh *)'

For deny-by-default on tools that have no allow, use Claude defaultMode: "dontAsk" or a PreToolUse hook. You cannot stack a catch-all deny on bash next to allow git — deny wins and blocks git too.

Hook deny

A PreToolUse hook can block a call in every permission mode. Example: ~/.matcha/hooks/git-gh-only.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "git-gh-only.sh", "timeout": 5 }
        ]
      }
    ]
  }
}

The script reads JSON on stdin (toolInput.command) and writes {"decision":"deny","reason":"…"} with exit 2, or {"decision":"allow"}. Split chains yourself; the hook runner will not do it for you. A matcher of Bash also matches the real tool name run_terminal_command.

Install and inspect with /hooks (or Ctrl+L on non–VS Code-family terminals). Press r to reload from disk.

Combine with the sandbox

For untrusted code:

  1. dontAsk plus narrow allows, or a restrictive PreToolUse hook
  2. --sandbox strict (or a custom profile with deny)
  3. Project trust before any SessionStart / project hooks run

In the TUI

Permission decisions appear in the transcript. /always-approve and /auto change the mode, not the rule files. /hooks manages hook sources.