Skip to main content
Hooks let you run custom logic at key points during a workflow — before a stage starts, after a run completes, when a sandbox is ready, and more. Use them for validation, notifications, guardrails, and orchestration without modifying the workflow graph itself.

Hook types

Fabro supports four hook types, from simple shell commands to full agent sessions:

Command

Run a shell command via sh -c. The simplest and most common hook type.
run.toml

HTTP

POST the event context as JSON to an HTTP endpoint. Useful for webhooks, external APIs, and notification services.
run.toml

Prompt

A single-turn LLM call that evaluates the event context and returns an ok/block decision. The model responds with structured JSON.
run.toml

Agent

A multi-turn agent session with full tool access (shell, file read/write, grep, glob). The agent can investigate the workspace before making a decision.
run.toml

Lifecycle events

Each hook fires on a specific lifecycle event:

Configuration

Hooks are defined as [[hooks]] entries in any of these TOML config files:
  • .fabro/project.toml — project-level hooks, apply to all workflows in the project
  • workflow.toml — per-workflow hooks
  • ~/.fabro/settings.toml — global defaults for all runs
See Merging hook configs for how these layers combine.
run.toml

Blocking vs. non-blocking

Blocking hooks can affect workflow execution. Non-blocking hooks run for side effects only — their decisions are ignored. Blocking by default: run_start, stage_start, edge_selected, pre_tool_use, sandbox_ready. These events represent decision points where a hook can prevent or redirect execution. Non-blocking by default: All other events. Override with blocking = true if needed. When multiple blocking hooks match the same event, they run sequentially. If any hook returns a block decision, execution short-circuits — remaining hooks are skipped.

Hook decisions

Blocking hooks return a decision that controls what happens next: When multiple blocking hooks run, decisions are merged with this precedence: Block > Skip/Override > Proceed.

Command hook decisions

Command hooks communicate decisions via exit code and stdout: To return an explicit decision from a command hook, print JSON to stdout:

Prompt and agent hook decisions

Prompt and agent hooks return a JSON response:
If the LLM fails to produce valid JSON, the hook fails open (proceeds). This fail-open behavior also applies to timeouts and LLM errors.

Matchers

The matcher field is a regex that filters when a hook fires. Omit matcher to match all occurrences of the event. Each event type matches against different context fields: The matcher is a regex, so write_file|edit_file matches either tool and ^agent$ matches exactly the handler type agent. When an event has multiple matchable fields (e.g., tool events match against both tool_name and node_id), the hook fires if any field matches the regex. For tool events, the matcher is tested against the tool’s internal name (e.g., shell, write_file, edit_file). See Tools for the full list. To match all file-writing tools across providers, use write_file|edit_file|apply_patch.

Examples

This example auto-formats Rust code whenever the agent writes or edits a file:
More examples:

Execution environment

Sandbox vs. host

By default, command hooks run inside the sandbox (sandbox = true). This means they execute in the same environment as the agent’s tools — same filesystem, same installed packages. Set sandbox = false to run on the host machine. This is useful for hooks that need access to host-only resources (CI systems, local credentials, notification tools). HTTP, prompt, and agent hooks ignore this setting — HTTP calls are always made from the host, and prompt/agent hooks use the LLM API directly.

Environment variables

Command hooks receive these environment variables:

Hook context

The full event context is available as a JSON payload. For command hooks running in the sandbox, it is written to a temp file (path in FABRO_HOOK_CONTEXT). For command hooks running on the host (sandbox = false), it is piped to stdin. For HTTP hooks, it is the POST body.
Fields vary by event — edge_from/edge_to/edge_label are only set for edge_selected, failure_reason for failure events, attempt/max_attempts for stage_retrying, etc. Null fields are omitted from the serialized JSON. For tool-level events (pre_tool_use, post_tool_use, post_tool_use_failure), the context includes additional fields:

Timeouts

Each hook type has a default timeout: Override with timeout_ms on any hook definition. Prompt and agent hooks fail open on timeout — execution proceeds as if the hook returned ok: true.

Fail-open behavior

Hooks are designed to be safe by default. Several failure modes result in the hook proceeding rather than blocking:
  • Prompt/agent LLM call fails — proceeds
  • Prompt/agent hook times out — proceeds
  • Prompt hook returns unparseable JSON — proceeds
  • HTTP hook returns non-2xx — proceeds
  • HTTP hook connection fails — proceeds
Command hooks do not fail open. A non-zero exit code (other than 0 or 2) produces a block decision.

Merging hook configs

Hooks from multiple config files are merged in this order (later layers win on name collisions):
  1. ~/.fabro/settings.toml — global defaults
  2. .fabro/project.toml — project-level overrides
  3. workflow.toml — per-workflow overrides
This lets you define global hooks at the server level, project-wide hooks in .fabro/project.toml, and override or extend them per workflow.

Full example

run.toml