Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Reactive Hooks

Zeph can run shell commands automatically in response to environment changes and tool execution events. Four hook events are supported: working directory changes, file system changes, tool execution before/after.

Hook Types

pre_tool_use and post_tool_use

Fires before and after a tool is executed. Useful for logging, monitoring, security auditing, or modifying the environment before/after tool runs.

Pre-execution (before tool runs):

[[hooks.pre_tool_use]]
tools = "shell|bash|sh"              # Pipe-separated tool name patterns (glob matching)
command = "echo"
args = ["About to run: $ZEPH_TOOL_NAME with args: $ZEPH_TOOL_ARGS_JSON"]

Post-execution (after tool runs):

[[hooks.post_tool_use]]
tools = "write_file|edit_file"       # File write tools
command = "git"
args = ["add", "$ZEPH_TOOL_NAME"]
fail_closed = false                  # If true, hook failure aborts the tool chain (default: false)

Environment variables available to hook processes:

VariableAvailable inDescription
ZEPH_TOOL_NAMEpre + postTool name (e.g., shell, web_scrape)
ZEPH_TOOL_ARGS_JSONpre + postTool arguments as JSON (truncated to 64 KiB via UTF-8 boundary)
ZEPH_TOOL_DURATION_MSpost onlyTime taken to execute the tool (milliseconds)
ZEPH_SESSION_IDpre + post (main agent only)Session ID; omitted in subagent hooks

Hook firing order:

Pre-hooks fire before utility gate and permission checks. This means observers can see all tool invocations, including those that would be blocked by policies. Post-hooks fire after successful execution.

cwd_changed

Fires when the agent’s working directory changes — either via the set_working_directory tool or an explicit directory change detected after tool execution.

[[hooks.cwd_changed]]
command = "echo"
args = ["Changed to $ZEPH_NEW_CWD"]

[[hooks.cwd_changed]]
command = "git"
args = ["status", "--short"]

Environment variables available to the hook process:

VariableDescription
ZEPH_OLD_CWDPrevious working directory
ZEPH_NEW_CWDNew working directory

file_changed

Fires when a file under watch_paths is modified. Changes are detected via notify-debouncer-mini with a 500 ms debounce window — rapid successive modifications produce a single event.

[hooks.file_changed]
watch_paths = ["src/", "config.toml"]

[[hooks.file_changed.handlers]]
command = "cargo"
args = ["check", "--quiet"]

[[hooks.file_changed.handlers]]
command = "echo"
args = ["File changed: $ZEPH_CHANGED_PATH"]

Environment variable available to the hook process:

VariableDescription
ZEPH_CHANGED_PATHAbsolute path of the changed file

The set_working_directory Tool

The set_working_directory tool gives the LLM an explicit, persistent way to change the agent’s working directory. Unlike cd in a bash tool call (which is ephemeral and scoped to one subprocess), set_working_directory updates the agent’s global cwd and triggers any cwd_changed hooks.

Use set_working_directory to switch into /path/to/project

After the tool executes, subsequent bash and file tool calls run relative to the new directory.

TUI Indicator

When a hook fires, the TUI status bar shows a short spinner message:

  • cwd_changedWorking directory changed…
  • file_changedFile changed: <path>…

The indicator disappears once all hook commands for that event have completed.

Configuration Reference

# Pre-tool-use hooks — run before any tool execution
[[hooks.pre_tool_use]]
tools = "shell|bash|sh"           # Tool name pattern (pipe-separated, glob matching)
command = "echo"
args = ["Running: $ZEPH_TOOL_NAME"]
fail_closed = false               # If true, hook failure aborts the tool (default: false)

# Post-tool-use hooks — run after tool execution completes
[[hooks.post_tool_use]]
tools = "write_file"
command = "git"
args = ["add", "$ZEPH_TOOL_NAME"]
fail_closed = false               # If true, hook failure blocks subsequent tools

# cwd_changed hooks — run in order when the working directory changes
[[hooks.cwd_changed]]
command = "echo"
args = ["cwd is now $ZEPH_NEW_CWD"]

# file_changed hooks — watch_paths + handler list
[hooks.file_changed]
watch_paths = ["src/", "tests/"]   # relative or absolute paths to watch
debounce_ms = 500                  # debounce window in milliseconds (default: 500)

[[hooks.file_changed.handlers]]
command = "cargo"
args = ["check", "--quiet"]
FieldTypeDefaultDescription
hooks.pre_tool_use[].toolsstringPipe-separated tool name patterns to match
hooks.pre_tool_use[].commandstringExecutable to run
hooks.pre_tool_use[].argsVec<String>[]Arguments (env vars expanded)
hooks.pre_tool_use[].fail_closedboolfalseIf true, hook failure aborts the tool chain
hooks.post_tool_use[].toolsstringPipe-separated tool name patterns to match
hooks.post_tool_use[].commandstringExecutable to run
hooks.post_tool_use[].argsVec<String>[]Arguments (env vars expanded)
hooks.post_tool_use[].fail_closedboolfalseIf true, hook failure aborts the tool chain
hooks.cwd_changed[].commandstringExecutable to run
hooks.cwd_changed[].argsVec<String>[]Arguments (env vars expanded)
hooks.file_changed.watch_pathsVec<String>[]Paths to monitor
hooks.file_changed.debounce_msu64500Debounce window in milliseconds
hooks.file_changed.handlers[].commandstringExecutable to run
hooks.file_changed.handlers[].argsVec<String>[]Arguments (env vars expanded)

Tool Pattern Matching

Tool name patterns support pipe-separated patterns and glob matching:

# Match exact tool names
tools = "shell"                     # Only the shell tool

# Match multiple tools
tools = "shell|bash|sh"             # Any shell variant

# Glob patterns (glob syntax)
tools = "write_*"                   # write_file, write_dir, etc.

# Combine exact and globs
tools = "shell|*_file"              # shell tool or any *_file tool

Patterns are matched case-sensitively. An empty pattern matches no tools.

Hook Tracing and Instrumentation

All hook execution is instrumented with distributed tracing. Each hook invocation generates:

  • zeph.hooks.cwd_changed span — execution of a cwd_changed hook
  • zeph.hooks.file_changed span — execution of a file_changed hook

Spans include:

AttributeValue
hook.commandExecutable name (e.g., cargo, git)
hook.argsFull argument list
hook.duration_msExecution wall-clock time
hook.exit_codeProcess exit code (if available)

Traces are exported to your configured telemetry backend (local Chrome JSON or Jaeger OTLP) and are visible in profiling tools like Perfetto. This allows you to identify slow hooks and optimize them.

Hook Propagation on Config Reload

When zeph reload-config is called (or config changes are hot-reloaded), hooks are immediately re-parsed and re-registered. The TUI and scheduler receive hook update notifications so they can reconfigure watchers without restarting.

For file_changed hooks:

  1. Old watchers are stopped
  2. New watch paths are parsed from the updated config
  3. Handlers are registered with the new watcher
  4. The next file modification triggers the updated hooks

For cwd_changed hooks:

  1. The hook list is updated in memory
  2. The next working directory change fires the new hooks

This enables configuration updates without restarting the agent process.

Reactive Events

Zeph fires reactive events when the environment changes beneath the agent. Events are processed synchronously before the next agent turn, ensuring hooks complete before the LLM sees the updated context.

CwdChanged

Fires after every tool execution turn when std::env::current_dir() differs from the directory recorded at the start of the turn. This covers both explicit set_working_directory calls and any side effects from shell commands that change the process cwd.

Hook commands receive the old and new paths via environment variables:

VariableDescription
ZEPH_OLD_CWDWorking directory before the change
ZEPH_NEW_CWDWorking directory after the change

Use cases:

  • Auto-run git status when switching into a different repo
  • Reload environment variables (e.g., .envrc) when entering a project directory
  • Notify external tools (e.g., tmux pane title, status bar) of the active project
[[hooks.cwd_changed]]
type         = "command"
command      = "git"
args         = ["status", "--short"]
timeout_secs = 10
fail_closed  = false

[[hooks.cwd_changed]]
type         = "command"
command      = "echo"
args         = ["Entered $ZEPH_NEW_CWD"]
timeout_secs = 5
fail_closed  = false

FileChanged

Fires when a file under one of the configured watch_paths is modified. The watcher uses notify-debouncer-mini with a configurable debounce window (default: 500 ms), so rapid successive writes produce a single event.

The changed file path is passed to hook commands via:

VariableDescription
ZEPH_CHANGED_PATHAbsolute path of the modified file

Use cases:

  • Run cargo check on every save during a coding session
  • Regenerate documentation when a source file changes
  • Invalidate a cache or restart a development server

Configure glob patterns for watch_paths and add one or more handler commands:

[hooks.file_changed]
watch_paths  = ["src/", "tests/", "Cargo.toml"]
debounce_ms  = 300

[[hooks.file_changed.hooks]]
type         = "command"
command      = "cargo"
args         = ["check", "--quiet"]
timeout_secs = 30
fail_closed  = false

[[hooks.file_changed.hooks]]
type         = "command"
command      = "echo"
args         = ["Changed: $ZEPH_CHANGED_PATH"]
timeout_secs = 5
fail_closed  = false

watch_paths accepts relative paths (resolved from the agent’s working directory at startup) or absolute paths. Directories are watched recursively.

Hook Execution Model

Each hook definition (HookDef) carries:

FieldTypeDefaultDescription
typestringAlways "command"
commandstringExecutable to run (must be on PATH or an absolute path)
argsVec<String>[]Arguments; $VAR references in args are expanded from the hook environment
timeout_secsu6410Maximum time to wait for the command to complete
fail_closedboolfalseWhen true, a hook failure blocks the agent turn; when false, failures are logged as warnings

Multiple hooks for the same event are executed in declaration order. If fail_closed = true on any hook, a failure in that hook stops execution of subsequent hooks for that event.

TurnComplete

Fires after each agent turn completes. This hook does not block the turn — it runs fire-and-forget in the background and allows notification integrations, logging, or external system updates to happen after the agent responds.

Hook commands receive environment variables describing the turn outcome:

VariableDescription
ZEPH_TURN_DURATION_MSTurn latency in milliseconds
ZEPH_TURN_STATUSsuccess, error, or cancelled
ZEPH_TURN_PREVIEWFirst 150 chars of redacted agent response
ZEPH_TURN_LLM_REQUESTSNumber of LLM API calls made this turn

Use cases:

  • Send a custom notification via a webhook
  • Log turn metrics to an external service
  • Sync agent state to an external system after each turn
[[hooks.turn_complete]]
type         = "command"
command      = "curl"
args         = ["-X", "POST", "http://localhost:9999/webhook", "-d", "status=$ZEPH_TURN_STATUS"]
timeout_secs = 5
fail_closed  = false

When a [notifications] block is configured, turn_complete hooks share the same should_fire gate — the hook only runs if notifications are also configured to fire. When [notifications] is absent or enabled = false, turn_complete hooks fire on every turn.

PermissionDenied

Fires when a tool execution is blocked by any gate check: policy gates, sandbox restrictions, permission layers, rate limiters, quota limits, utility action restrictions, or dependency failures. This comprehensive hook allows you to log or audit all blocked tool calls before they reach the user or external systems.

Hook commands receive:

VariableDescription
ZEPH_DENIED_TOOLName of the blocked tool
ZEPH_DENY_REASONReason the tool was denied (e.g., "quota exceeded", "policy gate: untrusted_model", "utility action: ModelSwitch")

Denial reasons include:

  • quota exceeded — tool execution quota exhausted
  • policy gate: <name> — blocked by a named policy gate
  • sandbox violation: <type> — sandbox restriction violated
  • rate limit exceeded — API rate limit hit
  • dependency failed — dependent tool or resource unavailable
  • utility action: <action> — blocked by a utility gate (e.g., ModelSwitch, ConfigReload)
  • blocked by before_tool layer — pre-execution permission check

Use cases:

  • Log security audit events to a central system
  • Alert on suspicious tool invocation patterns
  • Track which policies are enforcing restrictions
  • Monitor quota exhaustion
[[hooks.permission_denied]]
type         = "command"
command      = "logger"
args         = ["-t", "zeph-security", "Denied tool: $ZEPH_DENIED_TOOL - $ZEPH_DENY_REASON"]
timeout_secs = 5
fail_closed  = false

MCP Tool Hooks

Hooks support direct MCP tool invocation via type = "mcp_tool". When type = "mcp_tool", the hook invokes a tool on a connected MCP server instead of spawning a subprocess.

[[hooks.cwd_changed]]
type     = "mcp_tool"
server   = "filesystem"        # MCP server id
tool     = "write_file"        # MCP tool name
args     = {"path": "/tmp/log", "contents": "Changed to $ZEPH_NEW_CWD"}
fail_closed = false            # ignored if server unavailable

MCP tool hooks require the MCP manager to be active. If the server is unavailable, the hook result depends on fail_closed:

  • fail_closed = false (default): error is logged and the turn continues
  • fail_closed = true: turn is blocked until the tool succeeds or timeout expires