Hooks
Hooks are shell commands OrbCode runs at fixed points in the agent loop. Use them to block dangerous actions, auto-approve trusted ones, rewrite tool inputs, inject context into the model, format code after edits, notify yourself, or keep the agent working until a condition is met. OrbCode’s hooks follow the same contract as Claude Code’s hooks, so scripts written for Claude Code work here with two tweaks: use$MATTERAI_PROJECT_DIR (not $CLAUDE_PROJECT_DIR) and use OrbCode’s tool names (execute_command, file_edit, …) in your matchers. See Differences from Claude Code.
Two-minute example
Make OrbCode blockrm -rf and print the git branch on every prompt. Two steps.
1. Create a hook script at ~/.orbcode/hooks/guard.sh and make it executable:
~/.orbcode/settings.json:
rm -rf / the model tries is blocked with feedback, and every prompt gets the current branch appended as context. The recipes below use jq to parse JSON; install it with brew install jq / apt install jq, or parse with Python/Node — see Debugging hooks.
Where hooks live
Hooks are configured under thehooks key of settings.json. OrbCode reads two files and merges their hooks (it does not overwrite):
Project-level
.orb/ files (AGENTS.md, links.json) are read by both
the CLI and the Orbital IDE extension.
Hooks themselves stay in .orbcode/settings.json — .orb/ does not store
hooks.MATTERAI_CONFIG_DIR.
Hooks are not written to config.json (the app’s own state file) — they are configuration you own.
Configuration shape
- Event name — one of the events. Unknown names are ignored.
matcher— a JavaScript regex tested against one field of the event (the tool name forPreToolUse/PostToolUse,sourceforSessionStart, etc.; see the per-event tables). The regex is auto-anchored (^…$), so"execute_command"matches exactly that tool name, not"execute_command_extra"; use"a|b"for alternation. Omit it, or use"*", to match everything. An invalid regex falls back to an exact-string comparison.hooks— the commands to run when the matcher matches. You can list several; they all run.command— run through your shell ($SHELL, falling back to/bin/sh;cmd.exeon Windows). May be an inline command or a path to a script.timeout— per-command, in seconds. A hook that exceeds it is killed and reported as a non-blocking message. Default: 10s.
How a hook receives input (stdin)
Every hook command gets a single-line JSON object on stdin. All events include these base fields:
Plus event-specific fields (see Events reference).
The one-liner pattern every recipe uses — read stdin once, then pull fields out with
jq:
$MATTERAI_PROJECT_DIR).
How a hook controls OrbCode
A hook influences OrbCode in two ways: its exit code (simple) and/or a JSON object printed on stdout (precise). You can use either or both.Exit codes (the simple way)
What exit 2 does per event:
JSON on stdout (the precise way)
If a hook prints a JSON object (output starting with{), OrbCode parses it for fine-grained control. All fields are optional:
- Skip the approval prompt (auto-allow a tool):
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}} - Deny a tool with a reason:
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"…"}} - Force the approval prompt even for an auto-approved tool:
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask"}} - Rewrite the tool input:
{"hookSpecificOutput":{"hookEventName":"PreToolUse","updatedInput":{…}}} - Inject context (any of
PreToolUse/PostToolUse/UserPromptSubmit/SessionStart):{"hookSpecificOutput":{"hookEventName":"…","additionalContext":"…"}} - Block a prompt or force the agent to continue:
{"decision":"block","reason":"…"}
additionalContext for UserPromptSubmit and SessionStart, and ignored elsewhere — so echo "some context" is the shortcut for injecting context on those two events.
Events reference
OrbCode fires these events. The Matcher column is the field the matcher regex is tested against (for events with no match field, only an empty/"*" matcher applies).
SessionStart
Fires once, before the first turn of a session (or the first turn after --resume).
- Matcher:
source—"startup"or"resume". - Extra input:
source. - Can: inject session context (stdout on exit 0, or
additionalContext). - Cannot: block.
UserPromptSubmit
Fires before each prompt is sent to the model.
- Matcher: none.
- Extra input:
prompt(the text you typed). - Can: inject context (stdout/
additionalContext), block the prompt (exit 2or{"decision":"block"}), or stop the turn ({"continue":false}).
PreToolUse
Fires before a tool runs — before its approval prompt.
- Matcher:
tool_name. - Extra input:
tool_name,tool_input(the tool’s arguments). - Can: deny the tool, allow it (skip approval), ask (force approval), rewrite
tool_inputviaupdatedInput, or add context.
PostToolUse
Fires right after a tool returns.
- Matcher:
tool_name. - Extra input:
tool_name,tool_input,tool_response(the tool’s output text). - Can: add context / feedback to the model (
additionalContext, or{"decision":"block","reason":…}). The tool has already run — it can’t be undone.
Notification
Fires when OrbCode pauses for you — when it needs permission to run a tool, or when it asks a follow-up question.
- Matcher: none.
- Extra input:
message(what OrbCode is asking). - Can: anything with side effects (notify, log). Cannot block.
Stop
Fires when the model is about to finish the turn.
- Matcher: none.
- Extra input:
stop_hook_active—trueif this turn is already continuing because a Stop hook blocked it (check this to avoid loops). - Can: force the agent to keep going (
exit 2or{"decision":"block","reason":…}). OrbCode forces at most one continuation per turn as a safety net.
PreCompact
Fires before /compact summarizes the conversation.
- Matcher:
trigger— currently always"manual". - Extra input:
trigger,custom_instructions. - Can: run side effects (e.g. snapshot the transcript). Cannot cancel compaction.
SessionEnd
Fires when the session ends: quitting (Ctrl+C / /exit), /logout, or the end of a -p (headless) run.
- Matcher:
reason—"prompt_input_exit","logout", or"other". - Extra input:
reason. - Can: run cleanup/logging. Capped at 3s on quit so it never wedges exit.
SubagentStop (reserved)
Defined for Claude Code compatibility, but OrbCode has no subagents yet, so this never fires. It’s safe to configure; it’s a no-op until subagents land.
When multiple hooks match
All matching hooks for an event run in parallel, and their results are merged:- Permission decisions combine most-restrictive-first:
deny>ask>allow. If any hook denies, the action is denied. - Context (
additionalContext/ plain stdout) from every hook is concatenated. updatedInput: if more than one hook rewrites the input, the last matching hook wins.continue:falsefrom any hook stops the turn.- Block reasons from multiple hooks are joined together.
Execution model
- Hooks run through your shell; each gets the JSON payload on stdin.
- Each command has its own timeout (default 10s) and is force-killed if it overruns — a slow or hung hook never wedges the agent. If a hook ignores SIGTERM, OrbCode escalates to SIGKILL after a 2s grace.
- A hook that fails to start, errors, or times out is surfaced as a non-blocking message; it never crashes OrbCode.
- If you interrupt the turn (
Esc), in-flight hooks are signalled to abort. - Hooks are opt-in: with no
hooksblock, there is zero added work and zero behavior change.
Environment variables
Hooks inherit a redacted copy of your environment:
$PATH, $HOME, $LANG, $SHELL, etc. are available, but credential-like variables are stripped so a hook can never exfiltrate your API token. Specifically redacted:
MATTERAI_TOKEN,MATTERAI_API_KEY,MATTERAI_CONFIG_DIR,MATTERAI_BACKEND_URL,MATTERAI_APP_URL(OrbCode’s own vars).- Any variable whose name matches
/(?:^|_)(TOKEN|KEY|SECRET|PASSWORD|PASSWD|CREDENTIAL|PRIVATE_KEY)(?:$|_)/i(soGITHUB_TOKEN,AWS_SECRET_ACCESS_KEY,DATABASE_PASSWORD, … are redacted too).
command (e.g. read it from a file the hook can access, or set a dedicated non-matching env var).
Cookbook
Each recipe is asettings.json snippet plus (where useful) a script. Put scripts under ~/.orbcode/hooks/ and chmod +x them.
Auto-format files after edits
~/.orbcode/hooks/format.sh:
Block edits to protected files
~/.orbcode/hooks/protect.sh:
Block dangerous shell commands
~/.orbcode/hooks/guard.sh:
Auto-approve a safe, read-only tool
Skip the approval prompt forread_file and list_files:
Always require approval for one tool
Force the prompt forweb_fetch even in auto-approve mode:
Rewrite / normalize tool input
Forcels to always be ls -la:
~/.orbcode/hooks/rewrite.sh:
Inject dynamic context on every prompt
Block prompts that look like leaked secrets
~/.orbcode/hooks/no-secrets.sh:
Load team conventions at session start
Desktop notification when OrbCode needs you (macOS)
~/.orbcode/hooks/notify.sh:
Keep working until tests pass
~/.orbcode/hooks/tests-must-pass.sh:
Snapshot the transcript before compaction
~/.orbcode/hooks/snapshot.sh:
Log every session end
Debugging hooks
Run a hook by hand — pipe it a fake payload and inspect the exit code:jq? Parse with Python or Node:
- Forgetting
exit 0at the end of a script — a leftover non-zero exit from the last command will be read as an error (or a block, if it’s 2). - Reading stdin twice —
catdrains it. Read once into a variable (input=$(cat)), then reuse. - Output that accidentally starts with
{— it’ll be parsed as a JSON control object. Prefix plain text (e.g.echo "note: …") if that’s not what you want. - Relative script paths — use an absolute path or
~; OrbCode runs the hook with the workspace as the working directory. matcheron a no-match-field event (e.g. amatcheronStop) — it will never match. Omit the matcher for those events.- Wrong tool names — OrbCode uses
execute_command,file_edit,file_write,multi_file_edit,read_file,list_files,search_files,web_fetch,web_search,update_todo_list(not Claude Code’sBash,Edit,Write, …).
systemMessage or a non-blocking error, so you can see what went wrong.
Differences from Claude Code
The contract is identical (stdin JSON, exit-code protocol, JSON output schema, matcher regexes, parallel execution, per-command timeout). Differences:
Events present in newer Claude Code builds but not in OrbCode yet:
PostToolUseFailure, PermissionRequest, PermissionDenied, Setup, SubagentStart, PostCompact, and the worktree/file-watch/elicitation events.
Security
Hooks execute arbitrary shell commands as your user. OrbCode treats the two sources of hooks differently:- User hooks (
~/.orbcode/settings.json) are written by you and always run. - Project hooks (
<repo>/.orbcode/settings.json) ship inside a repository and could come from anyone, so they are disabled until you trust them.
The trust prompt
The first time OrbCode starts in a project that defines hooks, it lists the shell commands those hooks would run and asks whether to trust them:- Trust & enable → the project’s hooks run for this workspace, and the decision is saved to
~/.orbcode/hook-trust.json. - Keep disabled → the project’s hooks never run; only your user hooks do.
-p) mode there is no prompt, so untrusted project hooks are skipped with a warning on stderr. Set MATTERAI_TRUST_PROJECT_HOOKS=1 to trust project hooks in CI/automation where you control the repository. This env var is only honored when stdin is not a TTY — a stray export in a shell rc file cannot silently disable the trust gate for interactive sessions.
Other notes
- Hooks see a redacted environment — they inherit
$PATH,$HOME, etc. but never your API token or other credential-like variables (see Environment variables). Treat hook scripts like any other code you run locally, but OrbCode ensures they can’t silently exfiltrate your OrbCode credentials. - Injected context is sandboxed — text a hook injects via
additionalContext(or plain stdout onUserPromptSubmit/SessionStart) is wrapped in<hook_context>tags and capped at ~8 KB. The system prompt tells the model to treat the contents as untrusted, so a hook can’t prompt-inject the model into running arbitrary commands. - Tool-input rewrites are logged — when a
PreToolUsehook rewrites a tool’s input viaupdatedInput, OrbCode emits a visible system message so you can see that a hook changed what the model asked for. - Keep hook commands small and auditable; prefer checked-in scripts over long inline commands.