openclaw - 💡(How to fix) Fix [Feature]: Add before_tool_result_emit hook for live tool-result mutation before same-turn model consumption [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#52849Fetched 2026-04-08 01:18:32
View on GitHub
Comments
0
Participants
1
Timeline
1
Reactions
0
Participants
Timeline (top)
labeled ×1

OpenClaw currently exposes strong hook surfaces around tool execution, but there appears to be a missing seam for security/compliance use cases that need to inspect or transform a tool result before it is consumed by the model in the same turn.

Today, plugins/runtime seem to support:

  • before_tool_call — mutate params or block before execution
  • after_tool_call — observe after execution
  • tool_result_persist — mutate persisted toolResult transcript messages
  • before_message_write — mutate/block persisted messages

What seems to be missing is a sanctioned hook for:

tool executes -> result inspected/transformed -> transformed result enters live model loop

This matters for tools that return externally sourced untrusted content, such as:

  • web_fetch
  • webhook-derived content
  • email-derived content
  • OCR / extraction tools
  • other tools returning arbitrary external text

Root Cause

  • OpenClaw plugin authors and advanced self-hosters
  • Security-focused deployments
  • Any workflow where tool output may contain untrusted external text
  • Most relevant for web_fetch, but also applicable to webhook/email/OCR/extraction-style tool outputs
  • Impacts all channels indirectly because the problem is in the tool execution -> model consumption path, not a single messaging surface

Fix Action

Fix / Workaround

  • avoid patching installed dist/runtime code

  • avoid hardcoding behavior only for web_fetch

  • provide a general-purpose security/compliance seam

  • fit the existing hook-oriented architecture

  • benefit future tools returning untrusted external content

  • Raw tool output may reach the model before security/policy layers can sanitize it

  • Persist-time mutation is possible, but that is too late for same-turn protection

  • Integrators are forced into bad options:

  • patching installed dist/runtime code

  • special-casing core internals

  • settling for weaker transcript-only protections

  • Causes implementation delays, extra manual work, and fragile maintenance burden

  • Prevents clean upstream/plugin-based implementation of ingress-security for web_fetch

  • We intentionally want to avoid patching installed dist/runtime artifacts

  • We also want to avoid hardcoding one-off logic only for web_fetch

  • The goal is to use a supported plugin/core extension seam that fits OpenClaw’s existing hook architecture

Code Example

export type PluginHookBeforeToolResultEmitEv ent = {
   toolName: string;
   toolCallId?: string;
   runId?: string;
   sessionKey?: string;
   sessionId?: string;
   agentId?: string;

   params: unknown;
   result: unknown;

   isSynthetic?: boolean;
   inputProvenance?: InputProvenance;
   };

---

export type PluginHookBeforeToolResultEmitRe sult =
   | {
   action?: "allow";
   result?: unknown; // optional replacement
   metadata?: Record<string, unknown>;
   }
   | {
   action: "block";
   reason: string;
   metadata?: Record<string, unknown>;
   };

---

export type PluginHookName =
   | "before_tool_call"
   | "after_tool_call"
   | "tool_result_persist"
   | "before_message_write"
   | "before_tool_result_emit";

---

api.registerTypedHook("before_to ol_result_emit", (event) => {
   if (event.toolName !== "web_fetch") return { action: "allow" };

   const inspected = inspectInboundWebFetchResult(eve nt.result);

   if (inspected.decision === "block") {
   return {
   action: "block",
   reason: inspected.humanSummary,
   metadata: {
   ingressDecision: inspected.decision,
   reasonCodes: inspected.reasonCodes,
   },
   };
   }

   return {
   action: "allow",
   result: inspected.sanitizedPayload,
   metadata: {
   ingressDecision: inspected.decision,
   reasonCodes: inspected.reasonCodes,
   provenance: inspected.provenance,
   },
   };
   });
RAW_BUFFERClick to expand / collapse

Summary

OpenClaw currently exposes strong hook surfaces around tool execution, but there appears to be a missing seam for security/compliance use cases that need to inspect or transform a tool result before it is consumed by the model in the same turn.

Today, plugins/runtime seem to support:

  • before_tool_call — mutate params or block before execution
  • after_tool_call — observe after execution
  • tool_result_persist — mutate persisted toolResult transcript messages
  • before_message_write — mutate/block persisted messages

What seems to be missing is a sanctioned hook for:

tool executes -> result inspected/transformed -> transformed result enters live model loop

This matters for tools that return externally sourced untrusted content, such as:

  • web_fetch
  • webhook-derived content
  • email-derived content
  • OCR / extraction tools
  • other tools returning arbitrary external text

Problem to solve

For prompt-defense / ingress-security use cases, we need to support flows like:

web_fetch result -> ingress inspect/sanitize -> rewritten result -> same-turn model consumption

The existing hooks are close, but not enough:

  • before_tool_call is too early
  • after_tool_call appears observational only
  • tool_result_persist / before_message_write are too late for same-turn protection

So while transcript persistence can be sanitized, the active model loop may still see the raw result first, which defeats same-turn ingress protection.

Proposed solution

Add a new typed hook, preferably:

before_tool_result_emit

This hook would run:

  • after tool execution
  • before the tool result is emitted back into the active model/agent loop

It should allow plugins/core logic to:

  1. allow unchanged result
  2. replace result with a sanitized/annotated version
  3. block result entirely with a structured reason
  4. optionally attach metadata/provenance

Proposed contract

Event

  export type PluginHookBeforeToolResultEmitEv ent = {
  toolName: string;
  toolCallId?: string;
  runId?: string;
  sessionKey?: string;
  sessionId?: string;
  agentId?: string;

  params: unknown;
  result: unknown;

  isSynthetic?: boolean;
  inputProvenance?: InputProvenance;
  };

Result

  export type PluginHookBeforeToolResultEmitRe sult =
  | {
  action?: "allow";
  result?: unknown; // optional replacement
  metadata?: Record<string, unknown>;
  }
  | {
  action: "block";
  reason: string;
  metadata?: Record<string, unknown>;
  };

Hook name addition

  export type PluginHookName =
  | "before_tool_call"
  | "after_tool_call"
  | "tool_result_persist"
  | "before_message_write"
  | "before_tool_result_emit";

Suggested pipeline placement

Recommended order:

  1. tool selected
  2. before_tool_call
  3. tool executes
  4. before_tool_result_emit
  5. live tool result passed into model loop
  6. tool_result_persist
  7. before_message_write
  8. transcript write

This preserves existing semantics while adding the missing enforcement point.

Expected behavior

Allow unchanged

If hook returns allow with no replacement:

  • current behavior continues

Replace

If hook returns allow with result:

  • the replacement becomes the canonical live result used for:
  • same-turn model consumption
  • tool result message construction
  • later persistence, unless persistence hooks mutate again

Block

If hook returns block:

  • raw result should not enter the live model loop
  • a structured blocked tool-result payload/message should be emitted instead
  • block reason should be surfaced safely

Throw/failure behavior

Would be useful to define fail-open / fail-closed behavior, potentially configurable by tool or plugin.

Concrete use case

For web_fetch, this would enable:

  1. tool fetches raw external web content
  2. before_tool_result_emit runs for toolName === "web_fetch"
  3. plugin/core policy:
  • extracts text
  • sanitizes/normalizes
  • optionally scans
  • evaluates policy
  1. returns either:
  • sanitized payload replacement with provenance metadata, or
  • block with reason
  1. active model sees only sanitized/annotated content

That solves the missing same-turn seam for ingress-security.

Example handler

  api.registerTypedHook("before_to ol_result_emit", (event) => {
  if (event.toolName !== "web_fetch") return { action: "allow" };

  const inspected = inspectInboundWebFetchResult(eve nt.result);

  if (inspected.decision === "block") {
  return {
  action: "block",
  reason: inspected.humanSummary,
  metadata: {
  ingressDecision: inspected.decision,
  reasonCodes: inspected.reasonCodes,
  },
  };
  }

  return {
  action: "allow",
  result: inspected.sanitizedPayload,
  metadata: {
  ingressDecision: inspected.decision,
  reasonCodes: inspected.reasonCodes,
  provenance: inspected.provenance,
  },
  };
  });

Why this is better than direct core special-casing

This would:

  • avoid patching installed dist/runtime code
  • avoid hardcoding behavior only for web_fetch
  • provide a general-purpose security/compliance seam
  • fit the existing hook-oriented architecture
  • benefit future tools returning untrusted external content

Why persist-time mutation is not enough

Persist-time hooks are great for:

  • transcript hygiene
  • audits
  • later-turn context safety

But they are too late for same-turn protection if the model already consumed the raw result.

Backward compatibility

This is additive:

  • no existing plugin breakage
  • no existing hook contract breakage
  • no behavior change unless enabled by a plugin/core policy

Optional alternatives

If before_tool_result_emit naming doesn’t fit, alternatives could be:

  • tool_result_transform
  • after_tool_call_mutate
  • before_tool_result_consume

But before_tool_result_emit feels clearest.

Alternatives considered

No response

Impact

This affects anyone trying to build security, compliance, or policy controls around tool outputs, especially for tools that return externally sourced untrusted content such as web_fetch and similar content-ingestion paths.

Affected users / systems / channels

  • OpenClaw plugin authors and advanced self-hosters
  • Security-focused deployments
  • Any workflow where tool output may contain untrusted external text
  • Most relevant for web_fetch, but also applicable to webhook/email/OCR/extraction-style tool outputs
  • Impacts all channels indirectly because the problem is in the tool execution -> model consumption path, not a single messaging surface

Severity

Workflow-blocking for same-turn ingress/prompt-defense use cases.

It is not a general outage, but it blocks a whole class of security features that need to sanitize or block tool results before the model sees them in the same turn.

Frequency

Always, for this use case.

Any time a plugin or extension wants to inspect and rewrite a live tool result before same-turn model consumption, the missing hook becomes the blocker. This is not an intermittent bug; it is a missing extension seam.

Consequences

  • Raw tool output may reach the model before security/policy layers can sanitize it
  • Persist-time mutation is possible, but that is too late for same-turn protection
  • Integrators are forced into bad options:
  • patching installed dist/runtime code
  • special-casing core internals
  • settling for weaker transcript-only protections
  • Causes implementation delays, extra manual work, and fragile maintenance burden
  • Prevents clean upstream/plugin-based implementation of ingress-security for web_fetch

In practice, this blocks a secure pattern like:

web_fetch result -> inspect/sanitize -> rewritten safe result -> same-turn model consumption

which is a necessary foundation for stronger prompt-injection and untrusted-content defenses.

Evidence/examples

Checked against the published [email protected] package and release notes.

Relevant evidence

  • The published SDK surface appears to include hooks such as:
  • before_tool_call
  • after_tool_call
  • tool_result_persist
  • persistence-time message mutation via session/tool-result guard APIs
  • The published SDK surface did not show an obvious hook for live tool-result mutation before same-turn model consumption, such as:
  • before_tool_result_emit
  • tool_result_transform
  • mutating return support for after_tool_call

Specific files inspected in the published 2026.3.22 package

  • dist/plugin-sdk/src/plugins/types.d.ts
  • dist/plugin-sdk/src/agents/pi-tools.before-tool-call.d.ts
  • dist/plugin-sdk/src/agents/session-tool-result-guard.d.ts
  • dist/plugin-sdk/src/agents/session-tool-result-guard-wrapper.d.ts
  • dist/plugin-sdk/src/agents/tools/web-fetch.d.ts
  • dist/plugin-sdk/src/auto-reply/reply/untrusted-context.d.ts

Example of the gap in practice

For security / ingress-defense work, the desired flow is:

web_fetch result -> inspect/sanitize -> rewritten result -> same-turn model consumption

Current hooks seem to support:

  • blocking or mutating tool params before execution
  • mutating persisted transcript messages after execution

But they do not appear to support:

  • mutating the live tool result before it is passed back into the active agent/model loop

Why this matters

This means an integrator can sanitize the stored transcript, but may still be unable to guarantee that the model did not already consume the raw external content in the same turn.

Prior art / analogous pattern

A dedicated post-execution, pre-consumption hook such as before_tool_result_emit would fit naturally alongside existing lifecycle hooks and would mirror the role that before_tool_call already plays on the input side.

Additional information

This request comes from implementing an ingress-security / prompt-defense feature that treats external content as untrusted by default and aims to inspect/sanitize it before sensitive downstream use.

Constraints / context

  • We intentionally want to avoid patching installed dist/runtime artifacts
  • We also want to avoid hardcoding one-off logic only for web_fetch
  • The goal is to use a supported plugin/core extension seam that fits OpenClaw’s existing hook architecture

Why existing hooks are close but not enough

The current surfaces are already strong for:

  • pre-execution policy (before_tool_call)
  • telemetry / post-call observation (after_tool_call)
  • transcript hygiene (tool_result_persist, before_message_write)

So this request is not asking for a brand-new plugin model — just the missing post-execution, pre-consumption seam.

Why web_fetch is only the first use case

web_fetch is the clearest motivating example, but the same need applies to any tool that can return externally sourced text or other untrusted content, including:

  • webhook-driven content
  • email-derived content
  • extraction / OCR / parsing results
  • future tools that ingest external sources

Desired outcome

The ideal addition would be a small, additive hook such as:

  • before_tool_result_emit

that allows:

  • allow unchanged result
  • replace result with sanitized result
  • block result with reason
  • optionally attach metadata/provenance

Backward-compatibility expectation

This should be fully additive:

  • no breaking change to current hooks
  • no behavior change unless a plugin opts in

Optional note

If maintainers already have an internal seam that supports this use case and is intended for plugins/extensions, clarification on the recommended path would also solve the problem.

extent analysis

Fix Plan

To address the missing seam for security/compliance use cases, we will add a new typed hook called before_tool_result_emit. This hook will run after tool execution and before the tool result is emitted back into the active model/agent loop.

Step-by-Step Solution

  1. Add the new hook: Introduce the before_tool_result_emit hook to the existing hook architecture.
  2. Define the hook contract: Establish the contract for the before_tool_result_emit hook, including the event and result types.
  3. Implement the hook logic: Create a function that will handle the before_tool_result_emit hook, allowing plugins/core logic to:
    • Allow unchanged result
    • Replace result with a sanitized/annotated version
    • Block result entirely with a structured reason
    • Optionally attach metadata/provenance
  4. Register the hook: Register the before_tool_result_emit hook with the plugin SDK.

Example Code

// Define the before_tool_result_emit hook contract
export type PluginHookBeforeToolResultEmitEvent = {
  toolName: string;
  toolCallId?: string;
  runId?: string;
  sessionKey?: string;
  sessionId?: string;
  agentId?: string;

  params: unknown;
  result: unknown;

  isSynthetic?: boolean;
  inputProvenance?: InputProvenance;
};

export type PluginHookBeforeToolResultEmitResult =
  | {
      action?: "allow";
      result?: unknown; // optional replacement
      metadata?: Record<string, unknown>;
    }
  | {
      action: "block";
      reason: string;
      metadata?: Record<string, unknown>;
    };

// Implement the before_tool_result_emit hook logic
api.registerTypedHook("before_tool_result_emit", (event) => {
  if (event.toolName !== "web_fetch") return { action: "allow" };

  const inspected = inspectInboundWebFetchResult(event.result);

  if (inspected.decision === "block") {
    return {
      action: "block",
      reason: inspected.humanSummary,
      metadata: {
        ingressDecision: inspected.decision,
        reasonCodes: inspected.reasonCodes,
      },
    };
  }

  return {
    action: "allow",
    result: inspected.sanitizedPayload,
    metadata: {
      ingressDecision: inspected.decision,
      reasonCodes: inspected.reasonCodes,
      provenance: inspected.provenance,
    },
  };
});

Verification

To verify that the fix worked, test the before_tool_result_emit hook with different tool names and results, ensuring that the hook is called correctly and the result is processed as expected.

Extra Tips

  • Ensure that the before_tool_result_emit hook is fully additive and does not break existing hooks or behavior.
  • Consider adding fail-open/fail-closed behavior

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix [Feature]: Add before_tool_result_emit hook for live tool-result mutation before same-turn model consumption [1 participants]