Skip to content

Customize

Hooks

Run your own commands at fixed points in Bwat's lifecycle: veto tools before they run, notify your team, enforce house rules.

What hooks are

Hooks are your own shell commands, run automatically at fixed points in Bwat's lifecycle. They're the automation layer for the things Bwat shouldn't have to remember: lint after every edit, veto the one command you never want run, post to your team channel when a task finishes, block prompts that match a policy.

You configure them in two plain JSON files, with no code inside Bwat and no plugin system to learn:

ScopeFileApplies to
User~/.bwat/hooks.jsonEvery project on this machine
Project.bwat/hooks.jsonThis workspace only

Both locations are inside auto-gitignored directories, so a repository can never carry executable hooks to a teammate. Hooks are always something the machine's owner opted into. Edits are picked up immediately; BWAT_SKIP_HOOKS=1 disables all hooks if you ever need a kill switch.

A first hook

This project-scope hook vetoes any git push --force Bwat tries to run in every permission mode, including Auto, where there would otherwise be no prompt to stop it:

.bwat/hooks.json
{
  "version": 1,
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "grep -q 'push --force' <<< \"$TOOL_INPUT\" && { echo 'no force pushes' >&2; exit 2; } || exit 0"
          }
        ]
      }
    ]
  }
}

If Bwat later tries git push --force, the hook exits with code 2 and Bwat receives the error no force pushes instead of running the command. It sees the reason and adapts, exactly as if you had denied the action yourself.

The events

EventFiresWhat a block (exit 2) does
PreToolUseImmediately before any tool runs: Bwat's own tools, sub-agents, everythingVetoes the call; the tool body never runs and Bwat gets your stderr as the reason
PostToolUseAfter a tool result is producedAppends your feedback to the result Bwat sees
UserPromptSubmitAt the start of a run, before the first model callBlocks the whole prompt; you see the reason in chat
StopWhen a run finishes naturally (not on cancels)Nothing yet: notification only
SessionStartOnce per session, at the first runNothing
SessionEndCLI exit, or closing the VS Code panelNothing

Placement, precisely: PreToolUse runs after your approval in Manual or Edit-automatically modes, so a hook can veto but never silently approve something you haven't confirmed. In Auto mode (where there are no prompts), it is the pre-execution guardrail.

The contract

Each hook command runs through your shell with the working directory set to the workspace, and receives one JSON object on stdin describing what's happening:

stdin for PreToolUse
{
  "event": "PreToolUse",
  "sessionId": "agent_…",
  "cwd": "/path/to/workspace",
  "tool_name": "BashTool",
  "tool_input": { "command": "git push --force origin main" }
}

PostToolUse adds tool_result; UserPromptSubmit includes the prompt text. Exit codes are the whole decision:

Exit codeMeaning
0Continue: nothing happens
2Block: stderr becomes the feedback Bwat (or you) sees
anything else / crash / timeoutNon-blocking: logged as a note, the run carries on

Hooks are deliberately fail-open: a broken hook can log noise, but it can never take a run down. Timeouts default to 60 seconds (set timeoutMs per hook, capped at 10 minutes) and count as non-blocking. A timed-out hook's whole process tree is killed.

Matchers

The optional matcher is a regular expression tested against both the tool's wire name (BashTool) and short form (Bash), so "matcher": "Bash" covers the shell tool. Omit it and the hook fires for every tool. Only the two tool events use matchers; the lifecycle events always fire.

Ideas

  • Veto list: block rm -rf in a monorepo, kubectl delete against production, or edits to infrastructure/ via a PreToolUse matcher on Write|Edit.
  • Lint on edit: a PostToolUse hook on Edit|Write that runs your formatter; a non-zero exit surfaces as a note.
  • Team notifications: a Stop hook that posts “Bwat finished in <project>” to Slack.
  • Prompt policy: a UserPromptSubmit hook that rejects prompts containing secrets you don't want sent anywhere.

Hooks pair naturally with your own MCP servers: allow the tools you've opted into, veto everything else.