Hooks
Harn exposes three concentric hook surfaces. Each surface fires
synchronously on the agent-loop thread; closure handlers run inside the
same VM context as the surrounding turn, so they see the live transcript
and can call any builtin. Hook invocations are recorded on the active
session transcript under hook_call, hook_returned, and hook_vetoed
event kinds, so replay tooling reproduces the same control flow byte for
byte.
Tool lifecycle hooks (register_tool_hook)
PreToolUse and PostToolUse fire around each tool dispatch.
PreToolUse returns {deny: reason} to refuse the call or
{args: replacement} to rewrite the arguments; PostToolUse returns a
string or {result: replacement} to rewrite the result. For custom
logic, pass pre and/or post closures in the config table.
register_tool_hook({pattern: "exec_*", deny: "exec is gated"})
register_tool_hook({pattern: "*", max_output: 4000})
register_tool_hook(
{
pattern: "fetch_*",
pre: { event ->
return {reminder: {body: "Network fetch is about to run", tags: ["tool"]}}
},
},
)
Persona / step lifecycle hooks (register_persona_hook, register_step_hook)
Persona-scoped hooks observe PreStep, PostStep,
OnApprovalRequested, OnHandoffEmitted, OnPersonaPaused,
OnPersonaResumed, and OnBudgetThreshold(pct). Step hooks pin a
matching pair (persona_pattern, step_name) and accept the same
events; both can deny via {deny: reason} or rewrite via {args} /
{output}.
Hook closures may also return a reminder effect for the active session
transcript. The reminder spec uses the same keys as
transcript.inject_reminder: body, tags, dedupe_key,
ttl_turns, preserve_on_compact, propagate, and role_hint.
register_step_hook("merge_*", "audit", "PreStep", { ctx ->
return {
reminder: {body: "Audit step is running", tags: ["audit"]},
then: {args: ctx.step.args},
}
})
Session lifecycle hooks (register_session_hook)
Session-level hooks fire from the whole-session turn loop. They are the primary plugin surface for hosts and package authors who want to observe or veto the surrounding session rather than each individual tool call.
| Event | When it fires | Veto |
|---|---|---|
session_start | After the session record is open, before the loop starts | Advisory |
session_end | After the loop exits, before native session-end hooks fire | Advisory |
user_prompt_submit | Before the agent sees the user's prompt | {block: true, reason} returns a blocked result with stop_reason: user_prompt_submit_blocked |
pre_compact | Before transcript autocompaction runs | {block: true} skips this compaction pass |
post_compact | After autocompaction completes and replaces messages | Advisory |
post_turn | After a model/tool turn is recorded, before post-turn control logic decides whether to continue | Advisory |
permission_asked | When the dynamic permission policy needs to escalate | {decision: "allow"|"deny", reason} short-circuits the policy |
permission_replied | After the dynamic permission policy decides | Advisory |
file_edited | After write_file / append_file / write_file_bytes / notify_file_edited queues an edit. Drained at each agent-loop turn boundary | Advisory |
session_error | Before session_end when the loop ended with an error status or terminal error | Advisory |
session_idle | Each time the daemon-mode agent loop enters its wake_interval_ms wait between turns | Advisory |
pre_finish | Just before the pipeline's on_finish callback runs (or before pipeline return when no callback is registered) | Advisory |
on_unsettled_detected | Between pre_finish and on_finish, but only when harness.unsettled_state() is non-empty | Advisory |
post_finish | After the pipeline's on_finish callback returns, just before the pipeline value is yielded to the host | Advisory |
Return-value protocol
| Return | Meaning |
|---|---|
nil or true | Allow / advisory acknowledgment |
false | Veto (same as {block: true}) |
{block: true, reason: ...} | Veto |
{decision: "allow"|"deny"|"ask", reason?: ...} | Permission short-circuit (only honoured for permission_asked) |
{reminder: {...}, then?: ...} | Inject a system_reminder, then apply the optional inner control/action |
{body: "...", tags?: [...], dedupe_key?: ...} | Inject a reminder and otherwise allow/pass |
[{reminder: {...}}, ...] | Session hook effect list; reminders are injected in order and deduped by dedupe_key |
Any other return shape raises a runtime error so misuse fails loudly.
Example: veto secrets
register_session_hook("user_prompt_submit", { event ->
let prompt = to_string(event?.prompt ?? "")
if prompt.contains("secret") {
return {block: true, reason: "policy violation: secret in prompt"}
}
return nil
})
Example: short-circuit a permission decision
register_session_hook("permission_asked", { event ->
if to_string(event?.tool?.name ?? "") == "exec_root" {
return {decision: "deny", reason: "exec_root never runs unattended"}
}
return nil
})
Example: re-run a linter after each file edit
register_session_hook("file_edited", { event ->
let path = to_string(event?.path ?? "")
if path.ends_with(".rs") {
log("re-running clippy after edit to " + path)
}
return nil
})
For non-blocking context refresh, librarian, and crystallization jobs, use the receipt contract in Context maintenance hooks instead of doing slow work inside the hook closure.
[[hooks]] package manifest entries
Declarative entries in harn.toml register VM hooks at package load
time. The event name maps 1:1 to the runtime enum (PreToolUse,
SessionStart, UserPromptSubmit, etc.):
[[hooks]]
event = "UserPromptSubmit"
pattern = "*"
handler = "policy::reject_secrets"
The handler must resolve to an exported pub fn in the package's
namespaced exports. The harness wires the closure via
register_vm_hook and the resulting hook participates in the same
veto / tape capture semantics as a programmatic registration.
Tape capture and replay
Every register_session_hook invocation writes three transcript
events:
hook_call— payload + handler name, before the closure runs.hook_returned— handler name + parsed control flow, after it runs.hook_vetoed— emitted only when control flow isblockor a permissiondecision.
Because the events are first-class transcript entries, replay tools that re-execute a recorded session see the same hooks fire in the same order with the same payloads, even if the closures are no longer registered. This is the basis for the replay-fidelity guarantee used by transcript, trigger, and orchestrator replay tooling.