zeroclaw-labs/zeroclaw · error
LLM call cancelled by hook: {reason}
Error message
LLM call cancelled by hook: {reason} What it means
Before each LLM dispatch, run_tool_call_loop consults registered hooks (the shown site is model selection / provider request preparation). A hook returning HookResult::Cancel(reason) aborts the whole turn immediately, embedding the hook-supplied reason. This is a deliberate policy veto, not an infrastructure failure — the hook decided this LLM call must not happen.
Source
Thrown at crates/zeroclaw-runtime/src/agent/turn/mod.rs:775
multimodal_config,
degrade_strip_images,
image_cache.as_deref_mut(),
)
.await?;
let mut provider_request_messages = prepared_messages.messages;
let mut hook_selected_model = None;
if let Some(hooks) = ctx.hooks.filter(|hooks| !hooks.is_empty()) {
let mut candidate_model = active_model.to_string();
match hooks
.run_before_llm_call(&mut provider_request_messages, &mut candidate_model)
.await
{
crate::hooks::HookResult::Continue(()) => {
hook_selected_model = Some(candidate_model);
}
crate::hooks::HookResult::Cancel(reason) => {
anyhow::bail!("LLM call cancelled by hook: {reason}");
}
}
}
let provider_request_model = hook_selected_model.as_deref().unwrap_or(active_model);
// Only direct Agent turns scope the complete prompt variants. Preserve
// the channel loop's existing hook/protocol behavior rather than
// silently widening this delegation-focused repair into channel prompt
// reconciliation.
let uses_scoped_tool_protocol = TOOL_PROTOCOL_PROMPTS.try_with(|_| ()).is_ok();
let protocol_model = if uses_scoped_tool_protocol {
provider_request_model
} else {
active_model
};
iteration_tool_specs.refresh_native_tool_mode(active_model_provider, protocol_model);
let IterationToolSpecs {
ref tool_specs,
use_native_tools,View on GitHub (pinned to 88bb9c8533)
Solutions
- Read the {reason} embedded in the message — it is the hook's own justification; find the matching rule in the hooks configuration
- Check hook invocation logs to identify which hook fired and on what input
- If the veto is unintended, narrow the hook's match condition or fix its logic to return Continue
- If intended, treat the turn as policy-stopped: notify the user; do not retry — a retry re-runs the same hook
Example fix
// before: hook vetoes broadly
if !allowed_models.contains(&requested_model) {
return HookResult::Cancel(format!("model {requested_model} not allowed"));
}
// after: veto only the requests the policy actually targets
if policy_scope.matches(request) && !allowed_models.contains(&requested_model) {
return HookResult::Cancel(format!("model {requested_model} not allowed for {policy_scope}"));
} Defensive patterns
Strategy: try-catch
Try / catch
match agent.run_turn(req).await {
Err(ref e) if e.to_string().starts_with("LLM call cancelled by hook") => {
// policy stop: report the hook reason to the user; do NOT retry blindly
}
other => other,
} Prevention
- Audit hook configurations on every deploy; hooks are policy, treat changes like config changes
- Dry-run new hooks against representative requests before enabling Cancel paths
- Keep hook allowlists/rules in sync with model and provider renames
- Log hook decisions separately so vetoes are attributable
When it happens
Trigger: A configured hook (content policy, model allowlist, spend guard, operator rule) evaluates the upcoming call and returns Cancel; e.g. a model-selection hook rejecting the candidate model for this channel or request shape.
Common situations: Hook scripts added or changed during rollout matching more requests than intended; hook logic returning non-Continue on edge cases; restricted/CI environments with policy hooks enabled; stale hook rules after a model rename.
Related errors
- Agent loop aborted: repeated prompt-required tool call '{too
- Agent exceeded maximum tool iterations ({max_iterations})
- Final summary LLM call timed out after {step_secs}s (step_ti
- Budget exceeded: ${:.4} of ${:.2} {:?} limit. Cannot make fu
- Agent loop aborted by loop detector: {msg}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/e661d3b9af46ca89.
Report an issue: GitHub.