A hook is a shell command that the CLI runs when a run reaches a lifecycle event. Hooks let you observe a run or notify another system from outside it, for example, send an HTTP webhook, append a line to a log file, or trigger a CI job. Hooks are not part of the language.
Hooks run on the host CLI. 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 run_start before the runner spawns and run_end after the runner exits. Each hook command receives a JSON payload on its stdin.
The same four events (run_start, step_start, step_end, run_end), with the same payload shapes, fire in the three ways you can run a def:
jaiph run or jaiph <file.jh>.jaiph serve, dispatched by the server process.jaiph mcp tool call, dispatched by the server process. The hook’s stderr goes to the server’s stderr, and stdout stays clean for the protocol.Some run modes dispatch no hooks. jaiph run --raw dispatches no hooks, because it is meant for embedding a run inside another program and keeps its output clean. jaiph test runs defs 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.
.jh file you can run with jaiph run / jaiph <file.jh>, or serve with jaiph serve / jaiph mcp (hooks do not fire for jaiph test, jaiph compile, jaiph format, jaiph init, jaiph install, jaiph use, or jaiph run --raw).sh, plus whatever tool the hook command needs (jq, curl, etc.).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, so a project-local <workspace>/.jaiph/hooks.json that arrives with a cloned or untrusted repository is gated behind a per-workspace trust decision. Without that trust, the CLI ignores the project file and writes a one-line notice to stderr. The global file still runs. 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 via --env or requested via a script use clause (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
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 run_start, run_end, step_start, and step_end. The following file maps three of them.
{
"run_start": ["echo 'run started'"],
"step_end": ["jq -c . >> /tmp/jaiph-steps.jsonl"],
"run_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 run exit code. When a hook fails, the CLI writes a jaiph hooks: … line and continues.
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 run_start and run_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 run_start, then the step_* events, then run_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.
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":"def","step_name":"main","status":0,"elapsed_ms":1500}
The jq filter above keeps only a few fields. A full step_end payload also includes run_id, step_id, timestamp, run_path, and workspace. It also carries out_file and err_file, the paths to the files that captured the step’s stdout and stderr. Each file exists, but it can be empty when the step wrote nothing to that stream. step_kind is def for a def call, script for a script step, and prompt for a prompt step. step_name is the def or script name, except for a prompt step, where it is the name of the backend that ran the prompt.
The other events carry different fields:
step_start carries event, run_id, step_id, step_kind, step_name, timestamp, run_path, and workspace. It does not carry status or elapsed_ms, because the step has not finished yet.run_start carries event, run_id (the run id, present in every invocation mode), timestamp, run_path, and workspace.run_end carries event, run_id, status (the resolved run exit status), elapsed_ms (the total run time), timestamp, run_path, and workspace. A direct jaiph run also adds run_dir (the run directory under .jaiph/runs) and summary_file (the path to run_summary.jsonl) when the runner reports them. HTTP and MCP runs add run_dir in the same way but do not add summary_file. Use run_dir and summary_file to point a webhook at the run’s artifacts.Every hook command inherits the CLI’s environment, which is why $HOME resolves in the examples above.
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:
{ "run_end": ["true"] }
jaiph serve and jaiph mcp re-read hooks.json when they reload the program 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.