Add tool_result_received hook for output interception#10328
Add tool_result_received hook for output interception#10328ThomasLWang wants to merge 6 commits intoopenclaw:mainfrom
Conversation
This PR adds a new plugin hook 'tool_result_received' that allows plugins to intercept, modify, or block tool results before they reach the agent. Why not enhance existing after_tool_call hook? - after_tool_call runs as fire-and-forget (parallel execution) - Returns void - cannot modify or block results - Breaking existing behavior would impact plugins relying on it - Better to add a new hook with clear semantics The new hook: - Runs sequentially after tool execution, before result reaches agent - Can modify tool results (e.g., sanitize, redact, transform) - Can block results with custom error messages - Supports async operations (e.g., API calls for security checks) - Symmetric with before_tool_call for input/output control Use cases: - Security guardrails: detect prompt injection in external content - Content filtering: remove sensitive data from tool outputs - Data validation: ensure tool results meet safety requirements - Audit logging: record all tool outputs for compliance Files changed: - src/plugins/types.ts: Add hook type definitions - src/plugins/hooks.ts: Implement hook runner - src/agents/pi-tools.before-tool-call.ts: Call hook in tool wrapper - docs/concepts/agent-loop.md: Document the new hook Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
| const toolName = normalizeToolName(args.toolName || "tool"); | ||
| try { | ||
| const normalizedParams = isPlainObject(args.params) ? args.params : {}; | ||
| const hookResult = await hookRunner.runToolResultReceived( |
There was a problem hiding this comment.
Params passed to hook
runToolResultReceivedHook normalizes args.params to {} when it isn’t a plain object (src/agents/pi-tools.before-tool-call.ts:82-83), and the wrapper passes params: beforeOutcome.params (line ~148). If a tool legitimately takes non-object params (string/array), plugins will always see {} and cannot make decisions based on the real params. This is a behavior change vs the wrapper’s ability to pass through non-object params to the tool itself. If non-object tool params are supported in this codebase, the event type/normalization should preserve them (e.g., params: unknown in the event) so security filters can reason over actual inputs.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-tools.before-tool-call.ts
Line: 80:83
Comment:
**Params passed to hook**
`runToolResultReceivedHook` normalizes `args.params` to `{}` when it isn’t a plain object (`src/agents/pi-tools.before-tool-call.ts:82-83`), and the wrapper passes `params: beforeOutcome.params` (line ~148). If a tool legitimately takes non-object params (string/array), plugins will *always* see `{}` and cannot make decisions based on the real params. This is a behavior change vs the wrapper’s ability to pass through non-object params to the tool itself. If non-object tool params are supported in this codebase, the event type/normalization should preserve them (e.g., `params: unknown` in the event) so security filters can reason over actual inputs.
How can I resolve this? If you propose a fix, please make it concise.| execute: async (toolCallId, params, signal, onUpdate) => { | ||
| const outcome = await runBeforeToolCallHook({ | ||
| const startTime = Date.now(); | ||
|
|
||
| // Before hook - can modify params or block the call | ||
| const beforeOutcome = await runBeforeToolCallHook({ |
There was a problem hiding this comment.
Duration includes hook time
durationMs is computed as Date.now() - startTime immediately after tool execution, but before running the new tool_result_received hook (src/agents/pi-tools.before-tool-call.ts:127-153). That means the duration reported to plugins is only tool runtime, not “tool+guardrail” runtime, which is likely fine—but the docstring/intent in the PR description reads like it’s the overall post-tool phase. If this field is meant to represent tool execution time only, consider renaming (or documenting) it; otherwise compute duration after the interception hook so metrics match what the agent experiences.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-tools.before-tool-call.ts
Line: 127:131
Comment:
**Duration includes hook time**
`durationMs` is computed as `Date.now() - startTime` immediately after tool execution, but before running the new `tool_result_received` hook (`src/agents/pi-tools.before-tool-call.ts:127-153`). That means the duration reported to plugins is only tool runtime, not “tool+guardrail” runtime, which is likely fine—but the docstring/intent in the PR description reads like it’s the overall post-tool phase. If this field is meant to represent tool execution time only, consider renaming (or documenting) it; otherwise compute duration after the interception hook so metrics match what the agent experiences.
How can I resolve this? If you propose a fix, please make it concise.
Additional Comments (1)
Prompt To Fix With AIThis is a comment left during a code review.
Path: src/plugins/hooks.ts
Line: 154:156
Comment:
**Modifying hooks return type**
`runModifyingHook` force-casts every handler to `(...)=>(Promise<TResult>)` (`src/plugins/hooks.ts:154-156`). For hooks whose handler type is `Promise<TResult | void> | TResult | void` (including the new `tool_result_received`), a handler that returns `void` will be cast to `Promise<TResult>` and still awaited. This works at runtime, but it breaks the type contract and can hide real mismatches (e.g., returning `null` vs `undefined`) from being caught. Consider changing the cast to `Promise<TResult | void>` (or `unknown`) and narrowing before merge so the runner matches the declared handler signatures.
How can I resolve this? If you propose a fix, please make it concise. |
- Add type assertion to handle execute return type correctly - Fixes CI check: tsgo type mismatch error The execute function returns AgentToolResult<unknown>, but afterOutcome.result was typed as plain unknown. Added type assertion to preserve the correct type.
10c3bb0 to
c7bbf3d
Compare
Break long line to comply with formatting standards.
Apply prettier formatting to comply with oxfmt standards: - Break long log statements into multiple lines - Format union types consistently - Multi-line function call formatting
Apply prettier formatting to hooks.ts and types.ts to fix remaining format check issues.
Run oxfmt --write to apply correct formatting standards. oxfmt prefers more compact single-line formatting compared to prettier.
|
Landed on Changes applied on top of the PR to address review feedback:
Also added changelog entry and @ThomasLWang to the clawtributors list. Thanks for the contribution! |
|
Landed on openguardrails/openclaw main (e9f1951cc) with review fixes applied. Opening a new PR to merge upstream. |
…law#10328) Add a new plugin hook that runs after tool execution but before results reach the agent, enabling plugins to modify or block tool outputs for security guardrails and content filtering. Fixes from review: - Pass original params (unknown) instead of normalizing to {} - Fix runModifyingHook type cast to match handler signatures - Clarify durationMs measures pure tool execution time Co-Authored-By: Thomas <ThomasLWang@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add
tool_result_receivedHook for Output InterceptionOverview
This PR adds a new plugin hook
tool_result_receivedthat enables plugins to intercept, modify, or block tool results before they reach the agent. This is critical for implementing security guardrails against indirect prompt injection attacks in personal AI assistants.Motivation
The Personal AI Assistant Security Paradigm Shift
Traditional chatbot security focuses on input validation - preventing users from injecting malicious prompts. However, personal AI assistants face a fundamentally different threat model:
Why Existing Hooks Are Insufficient
Current state:
before_tool_call- Can intercept tool inputs (parameters)after_tool_call- Can see tool outputs, but:void- cannot modify or block resultstool_result_persist- Synchronous, runs during persistence (too late, wrong phase)What we need:
Why Not Enhance
after_tool_call?We considered enhancing the existing
after_tool_callhook instead of adding a new one. We chose Option B (new hook) for these reasons:after_tool_callfrom fire-and-forget to sequential would break existing plugins that rely on its current behaviorafter_tool_call) vs. interception (tool_result_received) makes intent explicitbefore_tool_call/tool_result_receivedfor clean input/output controlImplementation
1. Type Definitions (
src/plugins/types.ts)Added new hook types:
2. Hook Runner (
src/plugins/hooks.ts)Implemented sequential hook execution with result merging:
3. Tool Wrapper Integration (
src/agents/pi-tools.before-tool-call.ts)Modified tool wrapper to call the hook after execution:
4. Documentation (
docs/concepts/agent-loop.md)Updated plugin hooks section with clear distinctions:
before_tool_call- Intercept inputs before executionafter_tool_call- Observe outputs (fire-and-forget)tool_result_received- Intercept outputs before agent sees them (sequential, blocking)tool_result_persist- Transform results during persistence (synchronous)Use Cases
1. Indirect Prompt Injection Detection
Problem: Email contains hidden prompt injection:
Solution with
tool_result_received:2. Sensitive Data Redaction
3. Content Validation
OpenGuardrails Integration
This PR directly enables OpenGuardrails.com to build a security plugin for OpenClaw:
og-openclawguard- OpenGuardrails OpenClaw PluginBefore this PR: Plugin could only log detections using
tool_result_persist(sync, no blocking)After this PR: Plugin can block malicious content using
tool_result_received(async, blocking)Testing
The hook implementation follows the same patterns as existing hooks:
Recommended testing:
Migration Guide
For plugin authors:
Breaking Changes
None. This is a purely additive change:
after_tool_callsemantics are preservedFuture Work
Potential follow-up enhancements:
message_result_receivedfor agent output interception (complete the symmetry)Acknowledgments
This PR was developed in collaboration with OpenGuardrails.com, a professional AI security company specializing in SOTA content safety and prompt injection detection.
Authors:
Related:
Greptile Overview
Greptile Summary
tool_result_received, intended to run after tool execution but before the agent consumes the result, allowing plugins to modify/block tool outputs.src/plugins/hooks.tsand wires it into the global hook runner API.src/agents/pi-tools.before-tool-call.tsto invoke the new interception hook and optionally throw when blocked.Confidence Score: 3/5
(2/5) Greptile learns from your feedback when you react with thumbs up/down!