fix(hooks): harden before_tool_call hook runner to fail-closed on error [AI]#59822
fix(hooks): harden before_tool_call hook runner to fail-closed on error [AI]#59822drobison00 merged 4 commits intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR hardens the Confidence Score: 5/5Safe to merge — the security fix is correct, non-security hooks are unaffected, and all previous review concerns have been addressed. All previously raised issues are resolved. The core logic is sound and three layers of tests lock in the fail-closed contract. No P0 or P1 findings remain. No files require special attention. Reviews (2): Last reviewed commit: "fix: address PR review feedback" | Re-trigger Greptile |
|
@greptile review |
…or [AI] (openclaw#59822) * fix: address issue * fix: address PR review feedback * docs: add changelog entry for PR merge * docs: normalize changelog entry placement --------- Co-authored-by: Devin Robison <drobison@nvidia.com>
…or [AI] (openclaw#59822) * fix: address issue * fix: address PR review feedback * docs: add changelog entry for PR merge * docs: normalize changelog entry placement --------- Co-authored-by: Devin Robison <drobison@nvidia.com>
…or [AI] (openclaw#59822) * fix: address issue * fix: address PR review feedback * docs: add changelog entry for PR merge * docs: normalize changelog entry placement --------- Co-authored-by: Devin Robison <drobison@nvidia.com>
…or [AI] (openclaw#59822) * fix: address issue * fix: address PR review feedback * docs: add changelog entry for PR merge * docs: normalize changelog entry placement --------- Co-authored-by: Devin Robison <drobison@nvidia.com>
Summary
catchErrors: trueunconditionally, causing any exception thrown by abefore_tool_callhook to be silently swallowed. An outer catch inrunBeforeToolCallHookcompounded this by returning{ blocked: false }on any error, guaranteeing fail-open behavior regardless of hook intent.before_tool_call) must fail closed — if a hook throws, the tool call must be blocked, not permitted. Silent fail-open allows a crashing security hook to be fully bypassed with only a log-level error emitted.failurePolicyByHookoption ("fail-open"|"fail-closed") onHookRunnerOptions; configured the global runner withbefore_tool_call: "fail-closed"; updatedrunBeforeToolCallHook's catch block to return{ blocked: true }instead of falling through to{ blocked: false }; elevated log level fromwarntoerror.fail-open, preserving existing behavior for non-security hooks (telemetry, logging, etc.).Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Root Cause / Regression History (if applicable)
catchErrors: truewas hardcoded ininitializeGlobalHookRunnerand applied uniformly to all hooks including security-critical ones. A second independent catch inrunBeforeToolCallHookreturned{ blocked: false }on any error, creating a two-layer fail-open guarantee.before_tool_callhook resulted in a blocked tool call. The integration test for this scenario was explicitly titled "continues execution when hook throws" — asserting the wrong (fail-open) behavior.catchErrorsoption was designed for resilience of non-critical hooks; its unconditional application tobefore_tool_callwas an unintended side effect.Regression Test Plan (if applicable)
src/plugins/hooks.security.test.ts,src/agents/pi-tools.before-tool-call.integration.e2e.test.ts,src/agents/pi-tools.before-tool-call.e2e.test.tsbefore_tool_callhook that throws must result in a blocked outcome; the wrapped tool must not be executed.wrapToolWithBeforeToolCallHookpath; the security test exercises the hook runner'sfail-closedpolicy in isolation.User-visible / Behavior Changes
If a
before_tool_callplugin hook throws an unhandled exception, the tool call will now be blocked instead of proceeding silently. Operators will see anerror-level log entry. This only affects hooks that crash; correctly-implemented hooks are unaffected.Diagram (if applicable)
Security Impact (required)
Yes, explain risk + mitigation: This change reduces the execution surface; a crashing hook now blocks rather than permits. The only risk is a legitimate hook that throws due to a bug causing unexpected tool blocking — operators should ensure their security hooks handle errors internally if partial-block semantics are needed.Repro + Verification
Environment
Steps
before_tool_callhook that throws unconditionally.warnlevel.errorlevel.Expected
"Tool call blocked because before_tool_call hook failed".Actual (after fix)
executeis never called.Evidence
Updated integration test (
pi-tools.before-tool-call.integration.e2e.test.ts) previously assertedexecutewas called when the hook threw; it now asserts the opposite. New test inhooks.security.test.tsandpi-tools.before-tool-call.e2e.test.tslock in fail-closed semantics.Human Verification (required)
before_tool_callhooks (policy defaults tofail-open, no behavior change), approval-required path (separate try/catch, unaffected).Review Conversations
Compatibility / Migration
Risks and Mitigations
before_tool_callhook that throws due to a non-security bug will now block tool calls it previously allowed through.