Add a hook

A hook is a shell command that the CLI runs when a workflow reaches a lifecycle event. Hooks let you observe a run or notify another system from outside the workflow, for example send an HTTP webhook, append a line to a log file, or trigger a CI job. Hooks are not part of the workflow language.

Hooks run on the host CLI, even when the workflow itself runs inside Docker. The CLI dispatches them at four lifecycle events. It reads the step events from __JAIPH_EVENT__ lines on the runner’s stderr, and it emits workflow_start before the runner spawns and workflow_end after the runner exits. Each hook command receives a JSON payload on its stdin.

The same four events (workflow_start, step_start, step_end, workflow_end), with the same payload shapes, fire in the three ways you can run a workflow:

Some run modes dispatch no hooks. jaiph run --raw dispatches no hooks, because it is meant for transparent embedding and for the inner run of a Docker sandbox. The host side of a Docker run still dispatches hooks. jaiph test runs workflows in-process and dispatches no hooks. When you run jaiph serve or jaiph mcp, the server loads hooks.json at startup and reads it again on each source reload.

Prerequisites

1. Create the hooks file

Hooks come from one of two files. Project hooks override global hooks for each event, and the lists are not merged. If the project file defines commands for an event, only those commands run for that event. Omit an event from the project file to keep the global commands for that event.

Scope Path Trust
Global ~/.jaiph/hooks.json Always runs (the operator’s own file).
Project <workspace>/.jaiph/hooks.json Runs only when the workspace is trusted (see below).

Both files are optional. If a file contains invalid JSON, the CLI writes a jaiph hooks: … line to stderr and skips that file.

Hook commands run on the host, before and outside any Docker sandbox, so a project-local <workspace>/.jaiph/hooks.json that arrives with a cloned or untrusted repository is gated behind a per-workspace trust decision. Absent trust, the CLI ignores the project file and writes a one-line notice to stderr; the global file is unaffected. Trust the current workspace by exporting the opt-in before you run:

export JAIPH_TRUST_PROJECT_HOOKS=1

The variable is read from the host environment on jaiph run, jaiph serve, and jaiph mcp; it cannot be set from a .jh file via --env or trusted_envs (a file must not be able to trust itself). See JAIPH_TRUST_PROJECT_HOOKS.

Create the one you want:

mkdir -p .jaiph
cat > .jaiph/hooks.json <<'EOF'
{
  "step_end": [
    "jq -c '{event,step_kind,step_name,status,elapsed_ms}' >> \"$HOME/.jaiph/step-events.jsonl\""
  ]
}
EOF

2. Map events to commands

Each hooks file is a JSON object. The keys are event names, and each value is an array of shell commands. The four supported events are workflow_start, workflow_end, step_start, and step_end. The following file maps three of them.

{
  "workflow_start": ["echo 'run started'"],
  "step_end":       ["jq -c . >> /tmp/jaiph-steps.jsonl"],
  "workflow_end":   ["curl -s -X POST https://example.com/jaiph/end -d @-"]
}

Each command runs under sh -c, with the JSON payload written to its stdin. A process can read its stdin only once, so if you need the payload more than once, read it into a variable first.

p=$(cat); echo "$p" | jq -r .status; echo "$p" | jq -r .run_path

The CLI discards each hook’s stdout and copies its stderr to the CLI’s stderr. A hook failure never changes the workflow exit code. When a hook fails, the CLI writes a jaiph hooks: … line and continues.

3. Run the workflow

Trust the workspace first if the hooks live in the project file (<workspace>/.jaiph/hooks.json); global hooks need no opt-in.

export JAIPH_TRUST_PROJECT_HOOKS=1
jaiph run ./flow.jh

Each registered hook fires when the CLI dispatches its event. Step hooks follow a matching __JAIPH_EVENT__ line on the runner’s stderr, and the CLI emits workflow_start and workflow_end itself. For every hook the CLI writes the JSON payload to stdin and does not wait for the command to finish, so hook commands can overlap in time. The lifecycle order is always workflow_start, then the step_* events, then workflow_end. Within a single event, the CLI starts the commands in the order they appear in the file, but they can finish in any order.

Verification

Tail your hook’s output target after a run:

tail -n 5 "$HOME/.jaiph/step-events.jsonl"

A successful step_end record looks like this:

{"event":"step_end","step_kind":"workflow","step_name":"default","status":0,"elapsed_ms":1500}

The jq filter above keeps only a few fields. A full step_end payload also includes workflow_id, step_id, timestamp, run_path, and workspace. It adds out_file and err_file when the step captured stdout or stderr log files.

The other events carry different fields:

Every hook command inherits the CLI’s environment, which is why $HOME resolves in the examples above.

Disable a global hook for one project

There is no flag that disables a hook. An empty array does not override the global hooks, so you cannot turn an event off by setting it to []. Instead, override the event in the project file with a command that does nothing:

{ "workflow_end": ["true"] }

Reload behavior on a running server

jaiph serve and jaiph mcp re-read hooks.json when they reload the workflow source, not the moment you save the file. A server reloads when a watched module source file changes, and it reads the hooks again as part of that reload. So an edit to hooks.json alone takes effect on the next source change, not on its own.