Skip to content

Hooks

Most of the time a workflow node is just a prompt and an output. Sometimes you need to tweak what happens around the node: gate which tools the model can call, inject extra context just before the prompt fires, modify the response before the next node sees it. Hooks are the slot for that work.

This page is Advanced. You can write very useful workflows without ever touching hooks.

What a hook is

A hook is a small piece of code that runs at a specific moment around a node's execution. The two most common moments:

  • before-tool-use. Runs right before the model is about to call a tool. You can allow it, block it, or modify the tool call.
  • post-tool-use. Runs right after a tool finished. You can inspect the result, modify it, or trigger something on the side.

The hook itself is either a YAML block of conditions or a shell command Z.E.N. runs with the tool call as JSON on stdin.

A minimal example

Gate a node so it can only read files (no writes, no shell):

yaml
nodes:
  - id: review-doc
    type: prompt
    hooks:
      - on: before-tool-use
        allow: [Read]
        deny: [Write, Edit, Bash]
    prompt: |
      Review the document at $DOCS_DIR/spec.md and return your notes.

Z.E.N. sees the model try to call Bash, the hook says deny, the call is blocked. The model continues without that capability.

Inject context just-in-time

Sometimes the right context depends on the moment, not the workflow:

yaml
  - id: respond-to-mention
    type: prompt
    hooks:
      - on: before-tool-use
        when: tool == "WriteSlackMessage"
        inject_context: |
          Current time: $NOW_ISO
          Current channel topic: $channel_topic.output
    prompt: |
      Draft a reply to the latest message in $CHANNEL.

The injected text is appended to the model's working context at the moment the tool is about to fire.

Run a script as the hook

For more complex logic, point the hook at a script:

yaml
  - id: classify-email
    type: prompt
    hooks:
      - on: post-tool-use
        when: tool == "ReadEmail"
        run: scripts/scrub-pii.sh

The script gets the tool result on stdin as JSON. Whatever it writes to stdout becomes the new tool result.

Hook scope

Hooks declared on a single node apply only to that node. Hooks declared at the workflow level apply to every node:

yaml
name: review-pipeline
hooks:
  - on: before-tool-use
    deny: [Bash]

nodes:
  - id: a
    type: prompt
    # inherits the deny-bash hook

  - id: b
    type: prompt
    hooks:
      - on: before-tool-use
        deny: []  # node-level overrides workflow-level

Why hooks exist (the why-not-just-the-prompt question)

You can ask the model not to use a tool. You can write it in the prompt. The model will usually obey, but "usually" isn't a safety property. A hook is enforcement, not request. If a hook says deny, the tool call is blocked at the runtime, regardless of what the model decides to try.

Use hooks when you need a guarantee. Use prompt instructions for everything else.

Where to learn the schema

The full hook event schema (every event type, every field, every available filter) is documented in the Claude Agent SDK reference. Z.E.N. forwards hook events through to whichever provider the node is using; the schema is provider-defined.

AI that follows a recipe, not a conversation.