{"record":{"id":"e833ef5a9e1100a9","repo":"langchain-ai/deepagents","slug":"permission-interrupted-by-hook","errorCode":null,"errorMessage":"Permission interrupted by hook","messagePattern":"Permission interrupted by hook","errorType":"exception","errorClass":"ClientHookStopError","httpStatus":null,"severity":"error","filePath":"libs/code/deepagents_code/client/non_interactive.py","lineNumber":1860,"sourceCode":"            [\n                ToolCallData(\n                    id=f\"{interrupt_id}:{index}\",\n                    name=action_request.get(\"name\", \"\"),\n                    args=action_request.get(\"args\", {}),\n                )\n                for index, action_request in enumerate(action_requests)\n            ]\n        )\n        if plan.interrupted:\n            interrupting = next(\n                outcome for outcome in plan.outcomes if outcome.interrupt\n            )\n            reason = (\n                interrupting.decision.get(\"message\")\n                if interrupting.decision is not None\n                else None\n            )\n            raise ClientHookStopError(reason or \"Permission interrupted by hook\")\n\n        if not plan.fully_resolved:\n            await state.hooks.notify(\n                DcodeNotificationKind.PERMISSION_REQUIRED,\n                \"Permission required\",\n            )\n        resolved: list[dict[str, str]] = []\n        for outcome, action_request in zip(\n            plan.outcomes,\n            action_requests,\n            strict=True,\n        ):\n            decision = outcome.decision\n            resolved.append(\n                cast(\"dict[str, str]\", dict(decision))\n                if decision is not None\n                else _make_hitl_decision(action_request, console)\n            )","sourceCodeStart":1842,"sourceCodeEnd":1878,"githubUrl":"https://github.com/langchain-ai/deepagents/blob/a1af029e6e73cb17c36bff823d227747b28e91e1/libs/code/deepagents_code/client/non_interactive.py#L1842-L1878","documentation":"During HITL interrupt processing, if a hook's decision interrupts a permission request, `_process_hitl_interrupts` raises `ClientHookStopError` with the hook-supplied message, defaulting to \"Permission interrupted by hook\". It signals that a client lifecycle hook, not the user, denied/interrupted the pending permission prompt, ending the headless run.","triggerScenarios":"A permission request is intercepted by a registered hook whose decision interrupts the tool call while running `_run_agent_loop` headless; the interrupting hook's decision contains no `message`, so the default text is used.","commonSituations":"Policy hooks blocking shell commands not on the allow list; a hook returning an interrupt decision without a custom message; security tooling (e.g. secret-scanning hooks) vetoing file edits in headless CI runs.","solutions":["Read the hook's `message` if present; if you see the default text, find which registered permission hook interrupted and inspect its logs.","Update the hook to allow the operation or to emit a descriptive `message` in its decision.","Allow-list the needed command/paths via `shell.allow_list` so the permission never reaches the hook.","Catch `ClientHookStopError` in the harness and record it as a policy denial rather than a crash."],"exampleFix":"// before: interrupt with no message\nreturn {\"decision\": \"interrupt\"}\n// after: explain the denial\nreturn {\"decision\": \"interrupt\", \"message\": \"command not on approved list: \" + cmd}","handlingStrategy":"try-catch","validationCode":"# ensure hooks won't interrupt needed commands before the run\nfor hook in active_hooks:\n    if not hook.allows(command): disable(hook)","typeGuard":null,"tryCatchPattern":"from deepagents_code.hooks.client_lifecycle import ClientHookStopError\ntry:\n    await run_non_interactive(task)\nexcept ClientHookStopError as exc:\n    record_policy_denial(exc)  # treat as deliberate denial, default message = hook sent none","preventionTips":["Require hooks to include a `message` in interrupt decisions","Maintain `shell.allow_list` so routine commands skip the permission/hook path","Log hook decisions in headless runs for auditability"],"tags":["hooks","permissions","hitl","interrupt"],"backgroundTag":"permission-denied-by-hook","analyzedSha":"a1af029e6e73cb17c36bff823d227747b28e91e1","analyzedAt":"2026-08-29T11:43:24.718Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}