Hooks
Declarative condition → action pairs that fire on runtime events. Declare them
under runtime.hooks[] (whole app) or agents[].hooks[] (one agent).
Always quote YAML "on" — YAML 1.1 reads bare on: as the boolean true.
Mental model
Every hook is: on an event, if a condition holds, do an action.
runtime:
hooks:
- id: block-rm-rf
"on": tool_start # the event
condition:
type: content_contains # the test…
keyword: "rm -rf" # …and its params, INLINE (see below)
action:
type: gate # what to do…
allow: false # …and its params, INLINE
reason: "Remove specific paths, not the tree."
Params are inline. A condition's and an action's parameters are siblings of
type, NOT nested under a params: key. {type: gate, allow: false, reason: …}
is correct; {type: gate, params: {allow: false}} is not. The compiler validates
every required param and its type, so a missing one fails the build with a clear
message (hook action "inject_message" has no "content") rather than silently
misbehaving at runtime.
Hook fields
| Field | Type | Notes |
|---|---|---|
id | string | Required, unique within the app. |
"on" (or event) | event | The event to fire on. Both spellings work. |
condition | object | {type, …params}. Omit ⇒ always. |
action | object | {type, …params}. |
cooldown | number | Min seconds between fires. |
max_fires | integer | Cap total fires per session. |
priority | integer | Higher runs first when several match. |
enabled | boolean | false keeps the hook in the file but off. |
timeout | number | Seconds budget for a detached (async) action (default 30). |
tags | string[] | Free labels. |
Events
"on" accepts either the canonical name or an alias — they compile to the same event.
| Canonical | Alias | Fires when |
|---|---|---|
turn_start | user_prompt | A user turn begins. |
turn_end | — | A turn finishes (model done for this round). |
stop | pre_finish | The agent is about to end the turn — the place to gate completion. |
tool_start | pre_tool_use | Just before a tool runs — the place to gate or rewrite a call. |
tool_end | post_tool_use | Just after a tool returns. |
session_start | — | A session is created. |
session_end | — | A session ends. |
pre_compact | — | Just before context compaction. |
error | — | A runtime error is raised. |
approval_request | — | An approval is being requested — can auto-allow/deny. |
Declared but NOT fired in this build — a hook on one of these compiles with a
warning (DGT-W0006 … the hook will not fire) and does nothing. Do not build on them:
| Event | Why |
|---|---|
agent_spawn, agent_complete | Multi-agent lifecycle hooks are not implemented in this build. |
activation | Declared-only; not routed at the hook layer. |
Conditions
Every condition and its parameters (required ones marked ·):
type | Params | True when |
|---|---|---|
always / (omitted) | — | Always. |
never | — | Never. |
context_pressure | ·threshold (0–1) | tokens_used / max_tokens exceeds threshold. |
turn_count | ·threshold (int), every (int) | turn == threshold; with every, turn ≥ threshold && turn % every == 0. |
tool_calls | ·threshold (int) | Tool calls so far ≥ threshold. |
message_count | ·threshold (int) | Messages so far ≥ threshold. |
tool_name | ·match (string or list) | The invoked tool matches. Glob: *, ?, and a|b|c alternation. |
tool_failed | — | The tool that just ran errored. |
content_contains | ·keyword (string) | keyword is a substring of the model text, the user message, the tool's command arg, or the tool error. |
error_type | ·match (regex) | The error type matches the regex. |
expression | ·expr (string) | A numeric compare (>, >=, <, <=, ==) on one of: tokens_used, max_tokens, messages, tool_calls, turn_count, open_tasks; or the literals true / false / tool_failed. |
all_of | ·conditions (list) | Every nested condition holds (AND). |
any_of | ·conditions (list) | Any nested condition holds (OR). |
not | ·condition (object) | The nested condition is false. |
Actions
type and its params. The Effect at column is the events where the action's
effect is actually applied — an action fired at any other event runs but its
turn-changing effect is dropped (e.g. a gate on turn_end never blocks).
type | Params | Does | Effect at |
|---|---|---|---|
inject_message | ·content, role(=user), placeholder | Adds a message to the conversation. | any event |
gate | allow(=false), reason | Blocks (allow:false) or allows the gated step; reason is shown to the agent. | tool_start, stop, approval_request |
transform_params | ·transformation (object) | Merges rendered keys into the tool's arguments. | tool_start |
transform_result | ·transformation (object) | Merges rendered keys into the tool's result. | tool_end |
lsp_diagnose | path_field, content_field | Runs the compiler on the edited file and appends its diagnostics to the tool result. | tool_end |
module_action_inject | module, ·action, params, role(=user) | Calls a tool and injects its output back as a message. | any event |
compact_context | strategy (truncate|summarize), keep_last (int) | Compacts the session context. | any event |
module_action | module, ·action, params | Calls a tool; result discarded (side effect only). | any (detached) |
log | ·message, level | Writes a server log line. | any (detached) |
notify | title, message, level, tag | Emits a user-visible notification event. | any (detached) |
shell | ·command, cwd, timeout, on_error (log|ignore|raise) | Runs bash.run. | any (detached) |
pipe | ·to, map (object), extra (object), on_error | Calls the to tool with map+extra as args. | any (detached) |
chain | ·actions (list) | Runs several actions in order; effects merge. | per sub-action |
noop | — | Does nothing. | — |
compile_yaml and auto_test_deploy are reserved action names: they pass
validation but are not implemented yet and do nothing at runtime. Use
lsp_diagnose for compile feedback.
Templating
These action fields are rendered through {{…}} at fire time (everything else
is a literal): log.message; notify.title / notify.message;
inject_message.content; gate.reason; shell.command / shell.cwd;
transform_params.transformation.*; transform_result.transformation.*;
pipe.map.*; module_action(.inject).params.*.
Available variables: {{tool.name}}, {{tool.error}}, {{tool.result}} /
{{tool.result.KEY}}, {{tool.params}} / {{tool.params.KEY}} (dotted paths
and [i] indices walk into the object), {{tasks.summary}}, {{tasks.open}}.
A variable that does not resolve renders as empty.
Automatic built-in hooks
Two hooks are attached by the runtime — you do not declare them:
digitorn.builtin.lsp_diagnose— any app that grants thelspmodule gets compiler diagnostics appended to the result of everyfilesystem.write/edit/multi_editautomatically. (This is why the Builder Assistant sees errors after eachapp.yamledit without asking.)digitorn.builtin.task_completion_guard— gatesstopwhile the agent has open tasks, nudging it to finish or pause viaask_user.
Verified examples
Each of these compiles clean (digitorn lint).
runtime:
hooks:
# Keep a long session alive: summarise old turns past 80% context.
- id: auto-compact-at-80pct
"on": turn_end
condition: { type: context_pressure, threshold: 0.80 }
action: { type: compact_context, strategy: summarize, keep_last: 6 }
cooldown: 60
# Safety gate: refuse a destructive shell command BEFORE it runs.
- id: block-rm-rf
"on": tool_start
condition:
type: all_of
conditions:
- { type: tool_name, match: "bash.run" }
- { type: content_contains, keyword: "rm -rf" }
action:
type: gate
allow: false
reason: "Refusing `rm -rf`. Remove specific paths explicitly instead."
# Push recovery guidance into the turn when any tool errors.
- id: guide-on-tool-error
"on": tool_end
condition: { type: tool_failed }
action:
type: inject_message
content: "Tool `{{tool.name}}` failed: {{tool.error}}. Diagnose before retrying; don't repeat the same call."
# Escalate repeated infra errors to a visible notification.
- id: notify-on-timeout
"on": error
condition: { type: error_type, match: "timeout|rate_limit" }
action:
type: notify
title: "Agent degraded"
message: "Hit a {{tool.error}} — retrying with backoff."
level: warning
# Enrich the turn: after a web.fetch, call another tool with params drawn
# from the call, and inject its output back for the model to use.
- id: enrich-after-fetch
"on": tool_end
condition: { type: tool_name, match: "web.fetch" }
action:
type: module_action_inject
module: web
action: search
params: { query: "background on {{tool.params.url}}" }
# Two effects on one trigger, every 10th turn past 30.
- id: remind-on-long-sessions
"on": turn_end
condition: { type: turn_count, threshold: 30, every: 10 }
action:
type: chain
actions:
- { type: log, message: "Long session: {{tasks.open}} open task(s)." }
- type: inject_message
content: "You're 30+ turns in. Re-read the goal and close open tasks before adding new work."