Skip to content

feat(security): Add HTTP API security hooks for plugin scanning#6405

Open
masterfung wants to merge 10 commits intoopenclaw:mainfrom
TryMightyAI:feat/http-api-security-hooks
Open

feat(security): Add HTTP API security hooks for plugin scanning#6405
masterfung wants to merge 10 commits intoopenclaw:mainfrom
TryMightyAI:feat/http-api-security-hooks

Conversation

@masterfung
Copy link

@masterfung masterfung commented Feb 1, 2026

Summary

This PR adds 4 new plugin hooks to protect HTTP API endpoints from prompt injection and data exfiltration attacks.

Related Discussion: #6098
Previous PR: #6099 (closed for rebase)

Problem

OpenClaw's HTTP API endpoints currently bypass ALL plugin security hooks:

Endpoint Hook Coverage Risk
/v1/chat/completions ❌ None Critical
/v1/responses ❌ None Critical
/tools/invoke ❌ None Critical

Any application using the API directly (SDKs, curl) has zero protection against:

  • Prompt injection attacks
  • Data exfiltration via LLM responses
  • Tool argument injection
  • Indirect injection via tool results

Solution

Added 4 new plugin hooks:

Hook Purpose Execution
http_request_received Scan/block incoming requests Sequential (can block)
http_response_sending Scan/block responses for leaks Sequential (can block)
http_tool_invoke Scan/block tool arguments Sequential (can block)
http_tool_result Scan/block tool results Sequential (can block)

Security Features

  • FAIL-CLOSED: Hook errors/timeouts block requests (not bypass)
  • Header redaction: Authorization/Cookie/X-API-Key redacted
  • Request ID: For correlation/audit logging

Changes

File Lines Description
src/plugins/types.ts +157 Hook type definitions
src/plugins/hooks.ts +126 Hook runners
src/gateway/openai-http.ts +166 Hooks for /v1/chat/completions
src/gateway/openresponses-http.ts +103 Hooks for /v1/responses
src/gateway/tools-invoke-http.ts +108 Hooks for /tools/invoke
src/plugins/http-hooks.test.ts +387 13 unit tests

Total: 651 lines added, 9 lines removed

Test Plan

  • 13 new unit tests covering all hooks
  • Tests for error handling (fail-closed behavior)
  • Tests for hook priority ordering
  • TypeScript compilation passes
  • All existing tests unaffected
  • Rebased on latest main (26 new commits)
npx vitest run src/plugins/http-hooks.test.ts
# ✓ 13 tests pass

Usage Example

Security plugins can now register for HTTP hooks:

api.on("http_request_received", async (event, ctx) => {
  const result = await scanForInjection(event.content);
  if (result.isInjection) {
    return { 
      block: true, 
      blockReason: "Prompt injection detected",
      blockStatusCode: 400 
    };
  }
});

api.on("http_response_sending", async (event, ctx) => {
  const result = await scanForLeaks(event.content);
  if (result.hasLeak) {
    return { 
      block: true, 
      blockReason: "Credential leak detected" 
    };
  }
});

Real-World Plugin

This PR enables Citadel Guard, a security plugin that:

  • Scans for prompt injection using ML models
  • Detects credential/PII leaks in responses
  • Provides multi-turn attack detection
  • Works with both OSS and Pro Citadel backends

The plugin already has conditional hook registration to work with both current OpenClaw (messaging hooks) and this PR (HTTP hooks).

Known Limitations

  1. Streaming responses: Output scanning only works for non-streaming responses. Streaming would require buffering which breaks the streaming UX.

Breaking Changes

None. New hooks are additive and don't affect existing behavior.

Greptile Overview

Greptile Summary

This PR introduces four new plugin hooks to apply security scanning/blocking to direct HTTP API consumers that previously bypassed plugin security coverage: request/response scanning for /v1/chat/completions and /v1/responses, plus tool param/result scanning for /tools/invoke. The gateway handlers now build a redacted HTTP context (headers/IP/requestId), invoke the appropriate hook runners in fail-closed mode (errors/blocks return 4xx/5xx), and support request-body replacement and response content modification in non-streaming paths. For streaming responses, it accumulates content and runs an end-of-stream audit hook (cannot retroactively block already-sent data). Hook runner support and unit tests were added in the plugins layer to execute these hooks sequentially with priority ordering and merged results.

Confidence Score: 4/5

  • This PR is generally safe to merge, with one notable behavioral ambiguity around how multiple security hook handlers’ decisions are merged.
  • Core changes are additive, covered by new unit tests, and the gateway integrations appear consistent with the intended fail-closed semantics. The main concern is merge semantics for the new HTTP hook results: the current next.field ?? acc?.field pattern allows later (lower-priority) handlers to explicitly undo earlier decisions (e.g., unblock), which may be undesirable for security and could surprise plugin authors.
  • src/plugins/hooks.ts (HTTP hook merge semantics)

@openclaw-barnacle openclaw-barnacle bot added the gateway Gateway runtime label Feb 1, 2026
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 1, 2026

Additional Comments (2)

src/gateway/openresponses-http.ts
[P0] Streaming /v1/responses never runs http_response_sending, so streaming responses bypass the new “response leak/exfil” hook entirely.

The PR description calls out streaming limitations, but the code currently doesn’t invoke runHttpResponseSending at all in the streaming path (only http_request_received is enforced). That means plugins can’t block/redact streaming output even at end-of-stream. If the intended behavior is “non-streaming only”, this is a security gap vs the stated goal of protecting the endpoint.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/openresponses-http.ts
Line: 703:710

Comment:
[P0] Streaming `/v1/responses` never runs `http_response_sending`, so streaming responses bypass the new “response leak/exfil” hook entirely.

The PR description calls out streaming limitations, but the code currently doesn’t invoke `runHttpResponseSending` at all in the streaming path (only `http_request_received` is enforced). That means plugins can’t block/redact streaming output even at end-of-stream. If the intended behavior is “non-streaming only”, this is a security gap vs the stated goal of protecting the endpoint.

How can I resolve this? If you propose a fix, please make it concise.

src/gateway/openai-http.ts
[P0] Streaming /v1/chat/completions bypasses http_response_sending (and can’t be blocked/redacted).

http_response_sending is only invoked in the non-streaming branch. In the SSE streaming branch, chunks are written directly from onAgentEvent/fallback without any hook call, so plugins cannot detect leaks or block/modify content for streamed responses. Given the PR’s goal (“protect HTTP API endpoints”), this is a meaningful coverage hole unless explicitly accepted and documented as such.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/openai-http.ts
Line: 430:476

Comment:
[P0] Streaming `/v1/chat/completions` bypasses `http_response_sending` (and can’t be blocked/redacted).

`http_response_sending` is only invoked in the non-streaming branch. In the SSE streaming branch, chunks are written directly from `onAgentEvent`/fallback without any hook call, so plugins cannot detect leaks or block/modify content for streamed responses. Given the PR’s goal (“protect HTTP API endpoints”), this is a meaningful coverage hole unless explicitly accepted and documented as such.

How can I resolve this? If you propose a fix, please make it concise.

@openclaw-barnacle openclaw-barnacle bot added docs Improvements or additions to documentation channel: telegram Channel integration: telegram labels Feb 1, 2026
@masterfung masterfung force-pushed the feat/http-api-security-hooks branch from feb59f5 to 063c7a0 Compare February 1, 2026 17:39
@openclaw-barnacle openclaw-barnacle bot added the docker Docker and sandbox tooling label Feb 1, 2026
@masterfung masterfung force-pushed the feat/http-api-security-hooks branch 2 times, most recently from bee9ce5 to e2be359 Compare February 2, 2026 05:51
@openclaw-barnacle openclaw-barnacle bot removed docs Improvements or additions to documentation channel: telegram Channel integration: telegram docker Docker and sandbox tooling labels Feb 2, 2026
@masterfung masterfung force-pushed the feat/http-api-security-hooks branch from e2be359 to 8ad63df Compare February 2, 2026 16:45
@masterfung
Copy link
Author

Addressed Streaming Security Gap ✅

Thanks for the thorough review @greptile-apps! The P0 concerns about streaming responses are valid and have been addressed:

What we added:

End-of-stream scanning (commit 8ad63df):

  • Accumulates streamed content during SSE transmission
  • Runs http_response_sending hook at end-of-stream with full response
  • Works in audit mode - logs/alerts but doesn't break stream mid-flight
// At end of stream:
if (hookRunner && httpCtx && accumulatedContent) {
  hookRunner.runHttpResponseSending({
    content: accumulatedContent,
    isStreaming: true,
    isFinalChunk: true,
  }, httpCtx);
}

Why audit-only for streaming?

Blocking mid-stream would corrupt the SSE connection. Instead, plugins can:

  1. Log the leak for investigation
  2. Alert security team
  3. Terminate the session for future requests
  4. Flag the response in audit trail

This matches how other security gateways handle streaming (can't un-send bytes already transmitted).

Documentation

Added inline comments explaining this tradeoff in both openai-http.ts and openresponses-http.ts.


Ready for review! Happy to address any other feedback.

@masterfung
Copy link
Author

cc @steipete - would love your feedback on this security hooks feature when you have a moment! It closes a gap where HTTP API consumers bypass plugin hooks that protect messaging platforms.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 2, 2026

Perfect! I can see the implementation now. This is a solid approach to the streaming security challenge.

✅ What Works Well

1. Proper content accumulation:

// Content accumulated during streaming in both files
accumulatedText += content; // openresponses-http.ts  
accumulatedContent += content; // openai-http.ts

2. End-of-stream hook execution:
Both files properly invoke the http_response_sending hook after streaming completes with clear audit logging when content would have been blocked.

3. Clear documentation:
The header comments clearly explain the tradeoff and alternatives for security teams.

Summary

This is the right solution for streaming security - audit-mode scanning that:

  • ✅ Accumulates content during streaming
  • ✅ Runs security hooks at end-of-stream
  • ✅ Logs when content would have been blocked (audit trail)
  • ✅ Doesn't break streaming UX by mid-stream blocking
  • ✅ Clearly documents the architectural tradeoff

The implementation properly balances security visibility with streaming performance. Security plugins can still detect and log issues for investigation, alert security teams, and take action on future requests.

This addresses the P0 streaming gap concern effectively! 🎯

@masterfung
Copy link
Author

Why We Built This: Citadel Guard for OpenClaw 🛡️

We're building Citadel Guard - an open-source security plugin that protects OpenClaw agents from prompt injection, jailbreaks, and data exfiltration.

The Problem We Hit

Our plugin works great for messaging platforms (Telegram, Discord, Slack) because all messages flow through plugin hooks:

User → message_received hook → Citadel scans → AI → message_sending hook → Citadel scans → User
        ✅ Protected                                    ✅ Protected

But when users hit the HTTP API directly (/v1/chat/completions, /v1/responses), there are no hooks:

User → HTTP API → AI → HTTP Response → User
        ❌ No scanning    ❌ No scanning

Same OpenClaw instance, completely different security posture depending on how you access it.

What This PR Enables

With these 4 hooks, Citadel can protect ALL entry points:

Hook What Citadel Does
http_request_received Blocks prompt injection before it hits the model
http_response_sending Catches credential leaks, PII exposure
http_tool_invoke Stops dangerous tool args (rm -rf /, SQL injection)
http_tool_result Detects indirect injection from external data

Real-World Use Case

We have the plugin ready and tested: citadel-guard-openclaw

  • 258 tests passing
  • Handles both Citadel Pro (hosted) and OSS (self-hosted)
  • Already protects messaging + tools
  • Just waiting on this PR to protect HTTP API

Why This Matters for OpenClaw

Security plugins are a huge value-add for enterprise adoption. This PR makes OpenClaw a platform where the community can build security layers - not just Citadel, but rate limiters, PII redactors, compliance filters, etc.

Happy to jump on a call to demo if that helps! 🙏

@masterfung
Copy link
Author

@greptileai can you re-review the pr?

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 3, 2026

Additional Comments (2)

src/gateway/openai-http.ts
[P1] writeSse uses JSON.stringify directly; this can throw and break streaming responses.

If an SSE payload includes a bigint or a circular structure (even via nested event data), JSON.stringify will throw synchronously and can terminate the streaming response unexpectedly. Consider using the same kind of “safe stringify” approach used in /tools/invoke or otherwise ensuring SSE payloads are always JSON-serializable.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/openai-http.ts
Line: 54:56

Comment:
[P1] `writeSse` uses `JSON.stringify` directly; this can throw and break streaming responses.

If an SSE payload includes a `bigint` or a circular structure (even via nested event data), `JSON.stringify` will throw synchronously and can terminate the streaming response unexpectedly. Consider using the same kind of “safe stringify” approach used in `/tools/invoke` or otherwise ensuring SSE payloads are always JSON-serializable.

How can I resolve this? If you propose a fix, please make it concise.

src/gateway/openai-http.ts
[P1] Likely bug in extractTextContent: type === "input_text" branch checks text instead of input_text.

For content parts shaped like { type: "input_text", input_text: "..." }, this currently returns empty because it does typeof text === "string". That means request scanning and prompt building may miss user content for some clients.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/openai-http.ts
Line: 78:83

Comment:
[P1] Likely bug in `extractTextContent`: `type === "input_text"` branch checks `text` instead of `input_text`.

For content parts shaped like `{ type: "input_text", input_text: "..." }`, this currently returns empty because it does `typeof text === "string"`. That means request scanning and prompt building may miss user content for some clients.

How can I resolve this? If you propose a fix, please make it concise.

@masterfung
Copy link
Author

cc @cpojer @shakkernerd @justinhuangcode love your takes on this PR. Simple, powerful, and secure.

@openclaw-barnacle openclaw-barnacle bot added docs Improvements or additions to documentation app: macos App: macos app: web-ui App: web-ui commands Command implementations agents Agent runtime and tooling labels Feb 3, 2026
@masterfung
Copy link
Author

@greptileai review

@masterfung masterfung force-pushed the feat/http-api-security-hooks branch from a4e5ef4 to 5f29e10 Compare February 3, 2026 04:47
@openclaw-barnacle openclaw-barnacle bot removed the docs Improvements or additions to documentation label Feb 3, 2026
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 3, 2026

Additional Comments (4)

src/gateway/openai-http.ts
[P2] buildHttpContext silently drops multi-value headers (string[]) which can matter for auditing (e.g. multiple set-cookie, repeated x-forwarded-for). Right now only typeof value === "string" is kept; arrays are ignored.

If you want to preserve these safely, consider normalizing arrays (e.g. join with , or keep first) rather than dropping them entirely.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/openai-http.ts
Line: 104:122

Comment:
[P2] `buildHttpContext` silently drops multi-value headers (string[]) which can matter for auditing (e.g. multiple `set-cookie`, repeated `x-forwarded-for`). Right now only `typeof value === "string"` is kept; arrays are ignored.

If you want to preserve these safely, consider normalizing arrays (e.g. join with `,` or keep first) rather than dropping them entirely.

How can I resolve this? If you propose a fix, please make it concise.

src/gateway/tools-invoke-http.ts
[P1] toolArgs is computed once before hooks and then mutated via Object.assign(toolArgs, preHook.modifiedParams). If a plugin returns a replacement params object (not just partial overrides), Object.assign will merge keys but won’t remove unsafe keys from the original args. This can undermine sanitization (e.g. plugin tries to drop unsafe but it remains).

If the intended contract is “replacement”, you likely want toolArgs = preHook.modifiedParams (and ensure it stays in sync with the object passed into execute).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/tools-invoke-http.ts
Line: 666:673

Comment:
[P1] `toolArgs` is computed once before hooks and then mutated via `Object.assign(toolArgs, preHook.modifiedParams)`. If a plugin returns a *replacement* params object (not just partial overrides), `Object.assign` will merge keys but won’t remove unsafe keys from the original args. This can undermine sanitization (e.g. plugin tries to drop `unsafe` but it remains).

If the intended contract is “replacement”, you likely want `toolArgs = preHook.modifiedParams` (and ensure it stays in sync with the object passed into `execute`).

How can I resolve this? If you propose a fix, please make it concise.

src/gateway/openai-http.ts
[P1] modifiedRequestBody is applied via Object.assign(payload, ...), which can’t remove fields and may leave the original unsafe data present (same issue as tool params). If a plugin intends to replace the request body or delete fields, merging won’t achieve that.

If the contract is “sanitized replacement”, consider replacing payload entirely (or explicitly applying deletions) so plugins can actually drop unsafe keys.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/openai-http.ts
Line: 182:185

Comment:
[P1] `modifiedRequestBody` is applied via `Object.assign(payload, ...)`, which can’t remove fields and may leave the original unsafe data present (same issue as tool params). If a plugin intends to *replace* the request body or delete fields, merging won’t achieve that.

If the contract is “sanitized replacement”, consider replacing `payload` entirely (or explicitly applying deletions) so plugins can actually drop unsafe keys.

How can I resolve this? If you propose a fix, please make it concise.

src/gateway/openai-http.ts
[P3] End-of-stream http_response_sending hook is invoked asynchronously and its promise isn’t awaited before res.end(). That’s fine for “audit-only”, but it means the process could exit early (or tests could complete) before the hook finishes/logs, and hook errors won’t affect request handling.

If you want deterministic auditing, consider awaiting the hook (with a short timeout) before ending, or explicitly documenting that this is best-effort only.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/openai-http.ts
Line: 320:345

Comment:
[P3] End-of-stream `http_response_sending` hook is invoked asynchronously and its promise isn’t awaited before `res.end()`. That’s fine for “audit-only”, but it means the process could exit early (or tests could complete) before the hook finishes/logs, and hook errors won’t affect request handling.

If you want deterministic auditing, consider awaiting the hook (with a short timeout) before ending, or explicitly documenting that this is best-effort only.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

@masterfung masterfung force-pushed the feat/http-api-security-hooks branch from 8299021 to c641a54 Compare February 3, 2026 05:46
@masterfung
Copy link
Author

All Code Review Issues Addressed ✅

Round 1 Fixes:

Issue Fix
P1: writeSse can throw Wrapped JSON.stringify in try-catch with fallback
P1: extractTextContent bug Fixed input_text type to use inputText variable
P2: toolArgs computed twice Removed duplicate, reuse single declaration
P1: Hook modifications ignored Now uses modifiedParams, modifiedResult, modifiedRequestBody

Round 2 Fixes:

Issue Fix
P0: Missing response hook Added http_response_sending for non-streaming /v1/responses
P2: URL parse crash Wrapped in try-catch in openai-http.ts

Additional Self-Review Fixes (preemptive):

Issue Fix
URL parse in openresponses-http.ts Same try-catch pattern applied
URL parse in tools-invoke-http.ts Same try-catch pattern applied
writeSseEvent can throw Wrapped JSON.stringify in try-catch (matches writeSse)

All checks passing ✅ (lint, format, typecheck)
All tests passing ✅ (20/20)

@greptileai review

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

@masterfung
Copy link
Author

Comprehensive Code Review Fixes - All Issues Addressed ✅

P0 Fixes:

Issue Fix
Missing http_response_sending for non-streaming /v1/responses ✅ Added with block/modify support

P1 Fixes:

Issue Fix
modifiedRequestBody merge can't delete keys ✅ Full replacement (clear all keys + assign) in both files
modifiedParams merge can't delete keys ✅ Full replacement for tool args sanitization
Response hook ignores blockStatusCode ✅ Added to type + respected in both handlers

P2 Fixes:

Issue Fix
URL parse can throw ✅ try-catch in all 3 HTTP handlers
writeSseEvent can throw ✅ JSON.stringify try-catch added
buildHttpContext drops multi-value headers ✅ Join arrays with ", " in all 3 handlers
Test assertion too specific ✅ Regex to match direct/wrapped errors

P3 (Documented):

Issue Status
End-of-stream hook not awaited Already documented as "audit-only" in file header - cannot block already-streamed content

Files changed: openai-http.ts, openresponses-http.ts, tools-invoke-http.ts, types.ts, http-hooks.test.ts

All checks ✅ | All tests ✅ (20/20)

@greptileai review

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 3, 2026

Additional Comments (1)

src/gateway/tools-invoke-http.ts
[P1] /tools/invoke ignores plugin-provided status codes when blocking tool invoke/result.

PluginHookHttpToolInvokeResult and PluginHookHttpToolResultResult don’t currently carry a blockStatusCode, and the gateway hard-codes sendJson(res, 400, ...) on blocks (here and again around postHook?.block). This makes it impossible for security plugins to distinguish policy blocks (e.g. 403/422) from fail-closed outages (e.g. 503) in a consistent way with http_request_received/http_response_sending, which both support blockStatusCode.

Also appears at src/gateway/tools-invoke-http.ts:828-835.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/tools-invoke-http.ts
Line: 771:774

Comment:
[P1] `/tools/invoke` ignores plugin-provided status codes when blocking tool invoke/result.

`PluginHookHttpToolInvokeResult` and `PluginHookHttpToolResultResult` don’t currently carry a `blockStatusCode`, and the gateway hard-codes `sendJson(res, 400, ...)` on blocks (here and again around `postHook?.block`). This makes it impossible for security plugins to distinguish policy blocks (e.g. 403/422) from fail-closed outages (e.g. 503) in a consistent way with `http_request_received`/`http_response_sending`, which both support `blockStatusCode`.

Also appears at src/gateway/tools-invoke-http.ts:828-835.

How can I resolve this? If you propose a fix, please make it concise.

@masterfung
Copy link
Author

Final Fixes - blockStatusCode Consistency ✅

Issue Fix
P2: http_response_sending doesn't merge blockStatusCode ✅ Added to merge function
P1: Tool hooks don't support blockStatusCode ✅ Added to both types + merge functions + gateway

Now all 4 HTTP hooks consistently support blockStatusCode:

  • http_request_received
  • http_response_sending
  • http_tool_invoke
  • http_tool_result

Plugins can now specify 400/403/422/500/503 etc. consistently across all hooks.

All checks ✅ | All tests ✅

@greptileai review

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 3, 2026

Additional Comments (1)

src/gateway/tools-invoke-http.ts
PluginHookHttpContext (and the header redaction/normalization loop) is built unconditionally here, before checking whether getGlobalHookRunner() returns anything. When HTTP hooks are disabled/no plugins register them, this adds avoidable per-request overhead (and differs from the other HTTP handlers in this PR, which only build ctx when a hook runner exists). Consider moving the safeHeaders/httpCtx construction inside if (hookRunner) { ... } so the /tools/invoke path stays as cheap as before when hooks aren’t in use.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/tools-invoke-http.ts
Line: 727:755

Comment:
`PluginHookHttpContext` (and the header redaction/normalization loop) is built unconditionally here, before checking whether `getGlobalHookRunner()` returns anything. When HTTP hooks are disabled/no plugins register them, this adds avoidable per-request overhead (and differs from the other HTTP handlers in this PR, which only build ctx when a hook runner exists). Consider moving the `safeHeaders`/`httpCtx` construction inside `if (hookRunner) { ... }` so the `/tools/invoke` path stays as cheap as before when hooks aren’t in use.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

@masterfung
Copy link
Author

These issues are already fixed ✅

@greptileai - The issues you flagged have already been addressed in commit 2b04f6967:

P1: /tools/invoke ignores blockStatusCode

  • Fixed at lines 376 and 427 (not 771-774/828-835 - line numbers shifted)
  • sendJson(res, preHook.blockStatusCode ?? 400, ...)
  • sendJson(res, postHook.blockStatusCode ?? 400, ...)

P2: runHttpResponseSending doesn't merge blockStatusCode

  • Fixed at hooks.ts line 494
  • Added blockStatusCode: next.blockStatusCode ?? acc?.blockStatusCode

All 4 HTTP hooks now have blockStatusCode in:

  1. Types ✅
  2. Merge functions ✅
  3. Gateway handlers ✅

Please re-review with latest code.

@greptileai review

masterfung and others added 9 commits February 3, 2026 14:45
SECURITY FIX: HTTP API endpoints were bypassing all plugin security hooks,
creating a critical vulnerability where direct API consumers (SDKs, curl)
had zero protection against prompt injection and data exfiltration attacks.

Added 4 new plugin hooks:
- http_request_received: Scan/block incoming requests before LLM processing
- http_response_sending: Scan/block outgoing responses for data leaks
- http_tool_invoke: Scan/block tool invocations via /tools/invoke
- http_tool_result: Scan/block tool results for indirect injection

Protected endpoints:
- /v1/chat/completions (OpenAI-compatible API)
- /v1/responses (OpenResponses API)
- /tools/invoke (Direct tool invocation)

Security features:
- FAIL-CLOSED by default (hook errors/timeouts = block request)
- Sensitive headers redacted from hook context
- Request ID for correlation/audit logging
- Content modification support for sanitization/redaction

Tests: 13 new unit tests covering all hook types, error handling, and priority

This enables security plugins like Citadel Guard to protect ALL traffic,
not just messaging platform traffic (Telegram, Discord, Slack).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove unused imports (beforeEach, HookRunner) from http-hooks.test.ts
- Replace `void` with `undefined` in return types to fix redundant-type-constituent warnings
- Replace `string | unknown` with `unknown` (unknown already covers string)
- Format docs and skills files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
P0 Fixes:
- Add safeStringify() to handle BigInt/circular refs in tools-invoke-http.ts
  Prevents JSON.stringify from crashing before hooks run
- Document streaming response limitation in openai-http.ts
  Streaming bypasses http_response_sending hook (by design - buffering breaks UX)

P1 Fixes:
- Exclude function_call_output from user content scanning in openresponses-http.ts
  Tool output should be scanned via http_tool_result hook, not request hook
  Prevents false positives when tool results contain benign text

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Document that streaming responses bypass http_response_sending hook in
openresponses-http.ts (matching the existing documentation in openai-http.ts).

This is a known limitation - output scanning requires buffering which
defeats the streaming UX. Plugins should use message_sending hook at
the messaging layer for output scanning, or accept that streaming
responses cannot be scanned.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements audit-only scanning for streaming HTTP responses:

- Accumulate streamed content during SSE delivery
- Run http_response_sending hook at stream completion
- Log blocked responses for audit (cannot unsend already-streamed content)
- Update security documentation to clarify streaming behavior

This addresses the streaming bypass concern raised in code review.
For real-time blocking, users should disable streaming (stream: false)
or use message_sending hooks at the messaging layer.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
P1 fixes:
- writeSse: Wrap JSON.stringify in try-catch to handle bigint/circular refs
- extractTextContent: Fix input_text type to use inputText variable
- Hook modifications: Use modifiedParams/modifiedResult/modifiedRequestBody
  when returned by plugins (enables sanitization use cases)

P2 fixes:
- Remove duplicate toolArgs computation in tools-invoke-http.ts
- Add lint disable comment for required `any` cast

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
P0 fix:
- Add http_response_sending hook for non-streaming /v1/responses
  (was missing, defeating response-side leak scanning)

P2 fixes:
- Wrap URL parsing in try-catch in openai-http.ts, openresponses-http.ts,
  and tools-invoke-http.ts to handle malformed request URLs
- Wrap JSON.stringify in try-catch in writeSseEvent (openresponses-http.ts)
  to match writeSse pattern and prevent streaming crashes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
P0:
- Add http_response_sending hook for non-streaming /v1/responses

P1 fixes:
- modifiedRequestBody: Use full replacement (clear + assign) instead of merge
  to allow plugins to delete/redact keys (openai-http, openresponses-http)
- modifiedParams: Same full replacement pattern for tool args sanitization
- blockStatusCode: Add to PluginHookHttpResponseSendingResult type and
  respect it in both response handlers (consistent with request-side)

P2 fixes:
- URL parsing: Wrap in try-catch across all 3 HTTP handlers
- writeSseEvent: Add JSON.stringify try-catch (matches writeSse pattern)
- buildHttpContext: Handle multi-value headers (join arrays with ", ")
  across all 3 HTTP handlers for proper auditing
- Test assertion: Use regex to match both direct and wrapped error messages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add blockStatusCode to http_response_sending merge function
- Add blockStatusCode to PluginHookHttpToolInvokeResult type
- Add blockStatusCode to PluginHookHttpToolResultResult type
- Add blockStatusCode to tool hook merge functions
- Use blockStatusCode in tools-invoke-http.ts for both preHook and postHook

This ensures plugins can specify custom status codes (403/422/503)
consistently across all 4 HTTP hooks.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +533 to +536
if (hookRunner && httpCtx && accumulatedContent) {
hookRunner
.runHttpResponseSending(
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] In the streaming path, the end-of-stream http_response_sending invocation only uses the result for block logging, and ignores modifiedContent / modifiedResponseBody. That makes the hook’s “modifying” fields effectively no-ops for streaming (even in audit-only mode), which can surprise plugins that expect redaction/sanitization to be applied consistently.

If streaming can never apply modifications, it may be better to omit/ignore modification fields for streaming explicitly (or document it in the event/result) so plugin authors don’t assume they’ll take effect.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/openai-http.ts
Line: 533:536

Comment:
[P1] In the streaming path, the end-of-stream `http_response_sending` invocation only uses the result for `block` logging, and ignores `modifiedContent` / `modifiedResponseBody`. That makes the hook’s “modifying” fields effectively no-ops for streaming (even in audit-only mode), which can surprise plugins that expect redaction/sanitization to be applied consistently.

If streaming can never apply modifications, it may be better to omit/ignore modification fields for streaming explicitly (or document it in the event/result) so plugin authors don’t assume they’ll take effect.

How can I resolve this? If you propose a fix, please make it concise.

Move HTTP context construction inside the `if (hookRunner)` block to avoid
per-request overhead when no HTTP hooks are registered. This matches the
pattern used in the other HTTP handlers in this PR.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@masterfung masterfung force-pushed the feat/http-api-security-hooks branch from 2b04f69 to 39d473c Compare February 3, 2026 22:51
@openclaw-barnacle openclaw-barnacle bot removed the docs Improvements or additions to documentation label Feb 3, 2026
@masterfung
Copy link
Author

Performance fix applied ✅

Issue: httpCtx built unconditionally before checking hookRunner

Fix: Moved safeHeaders/httpCtx construction inside if (hookRunner) block to avoid per-request overhead when hooks aren't registered.

Also rebased on latest upstream/main.

@greptileai review

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@mudrii

This comment was marked as spam.

@openclaw-barnacle
Copy link

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added the stale Marked as stale due to inactivity label Mar 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime stale Marked as stale due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants