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:
PreToolUsehooks. An explicit hookdenystops the call before any permission check. A hookallowdoes not skip the rest; it only declines to deny.- Permission rules (
deny>ask>allow), merged from every source.denywins over allow and over always-approve’s normal pass-through. - Remembered grants from earlier cards (when
remember_tool_approvalsis on), scoped to this project. - Built-in auto-approvals (read-only tools and the read-only command list).
- 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.
| Scope | File |
|---|---|
| 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:
dontAskplus narrow allows, or a restrictivePreToolUsehook--sandbox strict(or a custom profile withdeny)- 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.