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

  1. Read the {reason} embedded in the message — it is the hook's own justification; find the matching rule in the hooks configuration
  2. Check hook invocation logs to identify which hook fired and on what input
  3. If the veto is unintended, narrow the hook's match condition or fix its logic to return Continue
  4. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/e661d3b9af46ca89. Report an issue: GitHub.