feat(hooks): add before_llm_call, after_llm_call, before_response_emit tool/content mutating hooks [claude, human developer oversight]#33916
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 68a376e318
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Greptile SummaryThis PR adds three sequential/modifying plugin hooks — What changed and why it fits:
Previously raised issues that have been addressed:
Remaining concerns:
Confidence Score: 4/5
|
…tion tracking - after_llm_call is now void (parallel, observational) — modifying tool calls requires agent loop interception points that don't exist yet. Documented as a future upgrade path. - before_response_emit block now returns empty string instead of undefined, so callers can distinguish blocked from unmodified. - hookIterationRef declared before event subscription (fixes temporal dead zone warning). Subscription now activates for before_llm_call too, so iteration counter updates even without after_llm_call hooks. Addresses greptile + codex-connector review on PR openclaw#33916.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e07ec3d846
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d71f865cff
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…re_response_emit - before_response_emit: use explicit undefined check instead of truthiness for content, so plugins can return empty string as valid modified content - before_llm_call: check systemPrompt !== undefined instead of truthiness, so plugins can clear the system prompt with empty string Addresses codex P2 reviews on PR openclaw#33916.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 70ea643c8e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
To use Codex here, create an environment for this repo. |
|
You're correct on points 1-3. I was wrong about However, the
The code comment says interception "doesn't exist yet" — that's acknowledging a limitation, not describing intentional design. If Good catch on the history clearing — I see you added Action items:
The mismatch between documentation and implementation will confuse plugin authors who need the blocking capability you described. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c59c9a7b4b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6edb9d8d5f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e7d6095cca
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 40e86563be
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c01752f27c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ffb3f95226
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b68cdb19f6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e35a8b1884
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Accidentally committed .openclaw-tank/memory/2026-03-05.md. Removed from tracking and added .openclaw-*/ to .gitignore to prevent future leaks.
…ength mismatch 1. before_response_emit merge semantics: content and allContent now use first-writer-wins (acc ?? next) instead of last-writer-wins (next ?? acc). Once a higher-priority security plugin sets redacted content, later utility plugins cannot override it. Matches the one-way latch on block. 2. rewriteAllAssistantContent logs a warning when allContent length differs from the assistant message count, making silent data loss visible in logs.
…_call Consistent with first-writer-wins on content/allContent in before_response_emit, the intersection latch on tools, and the one-way latch on block. Once a higher-priority security plugin sanitizes inputs, later utility plugins cannot override them.
…response_emit merge Once a higher-priority plugin sets either content or allContent, both fields are locked for later plugins. Prevents a lower-priority plugin from bypassing single-message redaction (content) by supplying allContent (which takes precedence in application), or vice versa.
…ted in after_llm_call
…afe) Removes index-based slicing via preRunMessageCount which can become stale when compaction replaces/merges transcript entries without changing the array length. Tail scan finds the last N assistant messages from the end, which is always correct regardless of compaction state.
…ction warning - Removed preRunMessageCount from ApplyBeforeResponseEmitParams, getRunScopedMessages signature, and all callers. The parameter was unused since switching to tail-based scanning. - Added warn-level log when block fires but getRunScopedMessages returns [] (compaction edge case where run boundaries are unidentifiable). Response delivery is still suppressed but blocked content may persist in session history.
…dMessages Tool-call-only assistant messages have content (array with tool_use blocks) but no text. assistantTextCount is derived from streamed text entries, so the tail scan must match that filter. Using extractAssistantText().length > 0 instead of 'content' in msg.
before_llm_call block now uses BeforeLlmCallBlockError sentinel class instead of generic Error. The agent loop catches this specifically and completes the run gracefully (no error surfaced) — consistent with before_response_emit block behavior. Previously, blocking threw a generic error that surfaced as a run failure.
Consistent with block (one-way latch) and content/messages (first-writer-wins): the higher-priority plugin that originally blocked also owns the reason. Prevents a lower-priority plugin from silently overwriting the block reason.
…s only Tool-call-only assistant messages (content arrays with only tool_use blocks, no text) are now skipped in rewriteAllAssistantContent, matching getRunScopedMessages and assistantTexts counting. Prevents allContent index misalignment when tool-call-only turns are interspersed with text-bearing turns.
…ges block - allContent splice now bounded to original assistantTexts.length — plugins cannot expand the response beyond what was produced. - Block with empty runMessages (compaction edge case) now fails closed by clearing ALL assistant content in the session, preventing blocked text from leaking into future turns or persistence.
clearAllAssistantContent now sets array content to [] instead of only clearing text parts. Prevents sensitive data in tool_use input arguments from persisting in session history when a response is blocked by before_response_emit.
…ors, guard gate clear
1. Test comment: 'h2 overrides' → clarifies h1 never blocked (not a latch scenario)
2. Cross-lock: added NOTE for plugin authors about silent drop behavior
3. Gate clear: guarded by hasHooks('after_llm_call') to avoid no-op on before_llm_call-only
… to PLUGIN_HOOK_NAMES Required for isPluginHookName validation and registerTypedHook. Same pattern as the context_assembled/loop_iteration fix.
a520ab2 to
68a78a0
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 68a78a0397
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| export function rewriteLastAssistantContent(messages: AgentMessage[], newContent: string): void { | ||
| const sessionMsg = [...messages] | ||
| .toReversed() | ||
| .find((m) => m.role === "assistant" && "content" in m); |
There was a problem hiding this comment.
Rewrite last text-bearing assistant message
rewriteLastAssistantContent picks the last assistant message regardless of whether it contains text, so when a run ends with an assistant tool-call-only message the subsequent rewriteSingleAssistantMessage call is a no-op. In that case a before_response_emit handler that returns modified content appears to succeed (and updates assistantTexts), but the original text in activeSession.messages remains unredacted and is still available to later turns/session persistence.
Useful? React with 👍 / 👎.
| log.debug( | ||
| `applying allContent modification (${emitResult.allContent.length} entries, original ${assistantTexts.length})`, | ||
| ); | ||
| rewriteAllAssistantContent(runMessages, emitResult.allContent); |
There was a problem hiding this comment.
Fail closed when run-scoped rewrite target is empty
The allContent modification path rewrites runMessages and returns success without checking whether getRunScopedMessages actually found any current-run messages. Under the compaction fallback case (already acknowledged by the block branch), this means the emitted response is redacted but session history is left unchanged, so sensitive pre-redaction text can still be fed into subsequent prompts.
Useful? React with 👍 / 👎.
|
Very interested in this! We run a multi-agent setup with 24 specialized agents and have been building our own content sanitization pipeline to scan all external data before it reaches any LLM. These hooks would let us enforce that at the gateway level instead of retrofitting every individual tool — exactly the right abstraction layer for security policy enforcement. The |
Split into 3 focused PRsThis PR has been superseded by 3 smaller, stacked PRs: PR 1:
|
Updated split: 2 PRsSuperseded by:
|
|
@bmendonca3: NB, the split has taken place; more notes now in #36671 |
TL;DR
Adds three sequential / mutating plugin hooks that allow policy plugins to intercept and gate:
before_llm_call)after_llm_call)before_response_emit)Includes an
after_llm_call"gate bridge" so tool-call blocking/filtering works without restructuring the pi-agent-core loop. No behavior changes unless a plugin registers these hooks.New hooks (contracts)
before_llm_callafter_llm_callbefore_response_emitThese follow the same sequential "merge results across handlers" pattern as
before_tool_call/message_sending.Architecture: after_llm_call gate bridge
after_llm_callruns from an event subscription callback, but tool execution happens elsewhere. To avoid invasive loop refactors:after_llm_callwrites its decision into a per-session gate (after-llm-call-gate.ts)before_tool_callchecks the gate before each tool executes (block or allowlist)turn_startand cleanup to prevent stale decisionsmessageEndSeqcounter prevents intra-turn race conditions between slow async handlersReview guide (start here)
src/plugins/types.ts(new hook names + event/result types)src/plugins/hooks.ts(sequential/FIFO merge + error handling)src/agents/pi-embedded-runner/run/hook-stream-wrapper.ts(+ tests)src/agents/pi-embedded-runner/run/attempt.ts(emits hook events)src/agents/pi-embedded-runner/run/after-llm-call-gate.ts(+ tests)src/agents/pi-tools.before-tool-call.ts(enforcement)src/agents/pi-embedded-runner/run/hook-response-emit.ts(+ tests)content) and multi-turn (allContent) modificationpreRunMessageCountwith compaction-safe fallbacksrc/hooks/internal-hooks.ts,src/gateway/server.impl.ts,src/gateway/server-startup.tsSecurity / correctness notes
before_response_emitblocking clears/redacts content from all current-run assistant messages (not just the last) so it does not persist into session historyallContentenables full multi-turn PII redaction across tool-loop iterationsTesting
Adds/extends unit & integration tests across:
before_llm_call)getRunScopedMessages(normal, compaction, defensive fallback)AI-Assisted Development 🤖
This PR was developed with AI assistance (Claude/OpenClaw agent "Tank").
pnpm checkclean.