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:

ScopePathTrusted?
Global~/.matcha/hooks/*.jsonAlways
Project<project>/.matcha/hooks/*.jsonFolder trust required
Config~/.matcha/config.toml ([[hooks.<Event>]])Always
PluginInside an installed, **trusted** pluginPer plugin
Compat~/.claude/settings.json, ~/.cursor/hooks.json, and project twinsSame 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 (preToolUsePreToolUse).

Events you will actually use

EventWhenCan block?
SessionStartSession startsNo
UserPromptSubmitYou send a promptNo
PreToolUseA tool is about to runYes — deny
PostToolUseTool succeededNo
PostToolUseFailureTool failedNo
StopA turn would finishYes — keep working
SessionEndSession endsNo

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

/hooks

Opens 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.

KeyAction
rReload from disk
aAdd a hook file or directory
x then yRemove a custom source
SpaceEnable or disable one hook
fFilter 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-untrust

What you should see

You ranResult
File in ~/.matcha/hooks/ + /hooksHook listed under Global
/hooks-trust in a repo with .matcha/hooks/Project hooks start running
PreToolUse deny JSONTool blocked; reason in scrollback
An HTTP hook URLNot a taught procedure