Skip to content

A hook runs a command when Raw reaches a named event, such as before a tool call or when a session starts. A hook can deny a prompt or a tool call. Other events only notify.

Hooks are opt-in. An agent with no hooks.use entry loads no hooks.

An agent lists its hooks in hooks.use. Hooks run in the order listed.

Prefix Where Raw loads it from
agent/<name> hooks/<name>/ beside the selected config file.
local/<name> ~/.config/raw/hooks/<name>/, or the $XDG_CONFIG_HOME equivalent.
pkg/<alias>/hooks/<export> A hook from an installed package.
{
"agents": {
"coder": {
"model": "local",
"tools": { "use": ["builtin/read_file", "builtin/bash"] },
"hooks": { "use": ["agent/guard"] }
}
}
}

Each hook is a folder with a hook.json manifest and the script it runs.

  • Directoryhooks/
    • Directoryguard/
      • hook.json
      • index.mjs
Field Required Rules
name Yes Name of the hook.
events Yes A nonempty array of subscriptions. See below.
command Yes An executable on PATH, or a ./ path inside the hook folder. No shell expands it.
args No Literal arguments. An argument that begins with ./ resolves inside the hook folder.
timeout_ms No Default 5000. Maximum 30000.
protocol_version Yes Must be 2.

Each entry in events has a name. Tool events can also have:

Field Meaning
match A glob over the canonical tool ID, such as builtin/bash or mcp/search/*.
when An argument filter with the same form as tools.rules, such as { "source": "arguments", "any": "commands[*].command", "regex": "..." }.

match and when are valid only on tool events.

Event When it fires
SessionStart A session attaches to the runtime, including on resume.
UserPromptSubmit A user prompt is submitted. Can deny.
PreToolUse Before a tool call runs. Can deny.
PostToolUse After a tool handler ran successfully.
PostToolUseFailure After a tool handler ran and failed.
Stop A turn finishes.
SessionEnd The runtime attachment closes. Stored history is not deleted.

Tool hooks see built-in, local, MCP, and ACP tools alike. Calls that policy denies, hides, rejects as invalid, or that the user denies at approval do not fire tool hooks.

Raw sends one UTF-8 JSON object on the hook’s stdin. It contains protocol_version: 2, the event name, the session cwd, optional agent, session, and turn IDs, and an event-specific payload.

The hook responds in one of these ways:

  • Print nothing and exit 0. Raw continues.
  • Print one JSON object. For UserPromptSubmit and PreToolUse, the object is { "decision": "continue" } or { "decision": "deny", "reason": "..." }. A message field, if present, is shown to the user.
  • Exit with code 2. Raw denies. This wins over any JSON the hook printed.

For other events, the hook can only send a message, which is shown as a notification. It cannot change a result that has already happened.

Failures are handled by event type:

  • On a gate event (UserPromptSubmit or PreToolUse), malformed output, a nonzero exit other than 2, a spawn failure, a timeout, or oversized output blocks the action.
  • On a notification event, the same failures only produce a warning.

A continue response never overrides an ask or deny rule in tools.rules.

Raw does not log hook input or raw stdout by default. Every hook run produces a short receipt in the live output and in saved history, with the hook ID, event, outcome, elapsed time, and any message or error code.

This is the guard example. It denies Bash commands that run rm, and it reports Bash failures.

{
"name": "guard",
"events": [
{
"name": "PreToolUse",
"match": "builtin/bash",
"when": {
"source": "arguments",
"any": "commands[*].command",
"regex": "(^|[;&|()\\n])\\s*rm(\\s|$)"
}
},
{
"name": "PostToolUseFailure",
"match": "builtin/bash"
}
],
"command": "node",
"args": ["./index.mjs"],
"timeout_ms": 5000,
"protocol_version": 2
}
let input = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", chunk => input += chunk);
process.stdin.on("end", () => {
const request = JSON.parse(input);
if (request.event === "PreToolUse") {
process.stdout.write(JSON.stringify({ decision: "deny", reason: "Review removal commands first" }));
} else {
process.stdout.write(JSON.stringify({ message: "Bash command failed" }));
}
});

The regex is a text match, not a shell parser. Pair a hook like this with a tools.rules entry if you need stronger protection.

This hook only reports. It cannot deny the Stop event.

{
"protocol_version": 2,
"name": "notify",
"events": [{ "name": "Stop" }],
"command": "python3",
"args": ["./notify.py"],
"timeout_ms": 3000
}
import json
import sys
event = json.load(sys.stdin)
print(json.dumps({"message": f"Turn {event.get('turn_id', 'unknown')} finished"}))
  1. Copy the guard folder to hooks/guard/ beside your config file.

  2. Add "hooks": { "use": ["agent/guard"] } to the agent you want to guard.

  3. Check the configuration. This validates the file without running the script.

    Terminal window
    raw --config /path/to/raw.json config list
  4. Ask the agent to run a harmless command, such as printf. No guard receipt should appear.

  5. Ask the agent to run a command that includes rm. The call should be denied, and a PreToolUse receipt should appear.

  • A selected hook folder can hold at most 256 regular files and 16 MiB in total. Links are rejected.
  • Hook changes apply on the next attachment or turn, including resume. A running runtime keeps the version it loaded.
  • The hook’s stdin, stdout, and stderr are size-bounded. Raw ends the process tree when the timeout or the cleanup window expires.