{"record":{"id":"e661d3b9af46ca89","repo":"zeroclaw-labs/zeroclaw","slug":"llm-call-cancelled-by-hook-reason","errorCode":null,"errorMessage":"LLM call cancelled by hook: {reason}","messagePattern":"LLM call cancelled by hook: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/agent/turn/mod.rs","lineNumber":775,"sourceCode":"            multimodal_config,\n            degrade_strip_images,\n            image_cache.as_deref_mut(),\n        )\n        .await?;\n        let mut provider_request_messages = prepared_messages.messages;\n        let mut hook_selected_model = None;\n\n        if let Some(hooks) = ctx.hooks.filter(|hooks| !hooks.is_empty()) {\n            let mut candidate_model = active_model.to_string();\n            match hooks\n                .run_before_llm_call(&mut provider_request_messages, &mut candidate_model)\n                .await\n            {\n                crate::hooks::HookResult::Continue(()) => {\n                    hook_selected_model = Some(candidate_model);\n                }\n                crate::hooks::HookResult::Cancel(reason) => {\n                    anyhow::bail!(\"LLM call cancelled by hook: {reason}\");\n                }\n            }\n        }\n        let provider_request_model = hook_selected_model.as_deref().unwrap_or(active_model);\n        // Only direct Agent turns scope the complete prompt variants. Preserve\n        // the channel loop's existing hook/protocol behavior rather than\n        // silently widening this delegation-focused repair into channel prompt\n        // reconciliation.\n        let uses_scoped_tool_protocol = TOOL_PROTOCOL_PROMPTS.try_with(|_| ()).is_ok();\n        let protocol_model = if uses_scoped_tool_protocol {\n            provider_request_model\n        } else {\n            active_model\n        };\n        iteration_tool_specs.refresh_native_tool_mode(active_model_provider, protocol_model);\n        let IterationToolSpecs {\n            ref tool_specs,\n            use_native_tools,","sourceCodeStart":757,"sourceCodeEnd":793,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/agent/turn/mod.rs#L757-L793","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: hook vetoes broadly\nif !allowed_models.contains(&requested_model) {\n    return HookResult::Cancel(format!(\"model {requested_model} not allowed\"));\n}\n\n// after: veto only the requests the policy actually targets\nif policy_scope.matches(request) && !allowed_models.contains(&requested_model) {\n    return HookResult::Cancel(format!(\"model {requested_model} not allowed for {policy_scope}\"));\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"match agent.run_turn(req).await {\n    Err(ref e) if e.to_string().starts_with(\"LLM call cancelled by hook\") => {\n        // policy stop: report the hook reason to the user; do NOT retry blindly\n    }\n    other => other,\n}","preventionTips":["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"],"tags":["hooks","policy-cancellation","llm-dispatch","agent-loop"],"backgroundTag":"hook-request-cancelled","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}