08. Extending Matcha
Hooks
A hook is a **local command** Matcha CLI runs at a lifecycle moment: before a tool, after a tool, at session start, when a turn would stop, and so on. Use it to block a dangerous command, keep a turn going until tests pass, or log what happened.
Quick start
mkdir -p ~/.matcha/hooks~/.matcha/hooks/session-start.json:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "echo 'Matcha session started in '$(pwd)" }
]
}
]
}
}Start or restart a session. Open /hooks and confirm the hook is listed under **Global**.
Where hooks are loaded
All sources merge:
| Scope | Path | Trusted? |
|---|---|---|
| Global | ~/.matcha/hooks/*.json | Always |
| Project | <project>/.matcha/hooks/*.json | Folder trust required |
| Config | ~/.matcha/config.toml ([[hooks.<Event>]]) | Always |
| Plugin | Inside an installed, **trusted** plugin | Per plugin |
| Compat | ~/.claude/settings.json, ~/.cursor/hooks.json, and project twins | Same trust rules |
Project hooks are skipped until you trust the folder. /hooks-trust (or --trust at launch) writes ~/.matcha/trusted_folders.toml. That grant covers **hooks, MCP, and LSP** together and cascades to subdirectories.
/hooks-untrust revokes it. Global hooks in ~/.matcha/hooks/ never need an entry.
Claude-style matcher names such as Bash still match Matcha CLI's run_terminal_command. Cursor camelCase event names map to the same events (preToolUse → PreToolUse).
Events you will actually use
| Event | When | Can block? |
|---|---|---|
SessionStart | Session starts | No |
UserPromptSubmit | You send a prompt | No |
PreToolUse | A tool is about to run | Yes — deny |
PostToolUse | Tool succeeded | No |
PostToolUseFailure | Tool failed | No |
Stop | A turn would finish | Yes — keep working |
SessionEnd | Session ends | No |
PreToolUse can deny. Stop / SubagentStop can block the stop and feed a reason back to the assistant (up to 8 continuations per turn). Other events are observe-only. Hook failures **fail open**: a crash or timeout does not block the tool. Only an explicit {"decision":"deny","reason":"…"} (or exit code 2 on PreToolUse) blocks.
matcher is a regex on the real tool name. MCP tools appear as server__tool (for example linear__save_issue), not as the internal use_tool dispatcher.
A blocking shell guard
~/.matcha/hooks/safe-shell.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "bin/safe-shell.sh", "timeout": 5 }
]
}
]
}
}bin/safe-shell.sh (next to the JSON, executable):
#!/bin/sh
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.toolInput.command // empty')
if echo "$CMD" | grep -qE '(rm -rf /|mkfs)'; then
echo '{"decision": "deny", "reason": "Blocked potentially destructive command"}'
exit 2
fi
echo '{"decision": "allow"}'The event arrives as JSON on stdin. Common fields include hookEventName, sessionId, cwd, workspaceRoot, toolName, and toolInput.
Manage in the TUI
/hooksOpens the extensions modal on the **Hooks** tab. The pager does **not** list /hooks-list, /hooks-add, /hooks-remove, /hooks-trust, or /hooks-untrust in the slash menu; those shell commands still work, and the modal covers the same jobs.
| Key | Action |
|---|---|
r | Reload from disk |
a | Add a hook file or directory |
x then y | Remove a custom source |
Space | Enable or disable one hook |
f | Filter All / Enabled / Disabled |
From a shell session (or ACP) you can also run:
/hooks-list
/hooks-add ~/.matcha/hooks
/hooks-remove ~/.matcha/hooks
/hooks-trust
/hooks-untrustWhat you should see
| You ran | Result |
|---|---|
File in ~/.matcha/hooks/ + /hooks | Hook listed under Global |
/hooks-trust in a repo with .matcha/hooks/ | Project hooks start running |
PreToolUse deny JSON | Tool blocked; reason in scrollback |
| An HTTP hook URL | Not a taught procedure |