← Back to PRs

#20802: feat(hooks): upgrade llm_input, llm_output, and after_tool_call to modifying hooks

by eilon-onyx open 2026-02-19 09:40 View on GitHub →
agents size: M
## Summary - Upgrade `llm_input` hook from void (audit-only) to modifying: plugins can now **block** (abort the LLM call) or **mask** (modify prompt/systemPrompt) before content reaches the LLM - Upgrade `llm_output` hook from void to modifying: plugins can now **mask** (modify assistantTexts) after the LLM responds — enables rehydrating masked tokens from `llm_input` - Upgrade `after_tool_call` hook from void to modifying in the adapter path: plugins can now **mask** (modify/redact tool results) before the LLM sees them - Subscribe handler path for `after_tool_call` remains audit-only since results are already committed to the session by that point ## Motivation Three critical hooks — `llm_input`, `llm_output`, and `after_tool_call` — fire at exactly the right points for PII/data leakage prevention but were previously audit-only. This enables plugins to actively prevent sensitive data from reaching LLMs, and to rehydrate masked tokens in the response. The mask/rehydrate round-trip: `llm_input` masks sensitive data (e.g. PII → `«PERSON_001»`) before it reaches the LLM, and `llm_output` rehydrates those tokens in the assistant response before it reaches the user. ## Changes ### Types (`src/plugins/types.ts`) - Add `PluginHookLlmInputResult` (prompt, systemPrompt, block, blockReason) - Add `PluginHookLlmOutputResult` (assistantTexts) - Add `PluginHookAfterToolCallResult` (result) - Update `PluginHookHandlerMap` return types for all three hooks ### Hook runner (`src/plugins/hooks.ts`) - `runLlmInput`: switch from `runVoidHook` to `runModifyingHook` with merge semantics - `runLlmOutput`: switch from `runVoidHook` to `runModifyingHook` - `runAfterToolCall`: switch from `runVoidHook` to `runModifyingHook` - Add new type imports and re-exports ### Consumers - `attempt.ts`: await `llm_input` result, handle block and prompt/systemPrompt modification - `attempt.ts`: await `llm_output` result, apply modified assistantTexts - `pi-tool-definition-adapter.ts`: apply modified tool results from `after_tool_call` (both success and error paths) ### Tests - 4 new `runLlmInput` runner tests (modified prompt, block, no hooks, multi-handler merge) - 3 new `runLlmOutput` runner tests (modified assistantTexts, void handler, no hooks) - 4 new `runAfterToolCall` runner tests (modified result, no hooks, void handler, last-wins) - 3 new adapter e2e tests (modified result success/error path, undefined passthrough) - All existing tests continue to pass ## Backward Compatibility Fully backward compatible: - Plugins returning `void` continue to work — `runModifyingHook` treats undefined/null returns as no-ops - Handler signature change from `=> Promise<void>` to `=> Promise<Result | void>` is strictly additive ## Test plan - [x] `pnpm test -- --run src/plugins/wired-hooks-llm.test.ts` (14 tests pass) - [x] `pnpm vitest run --config vitest.e2e.config.ts src/agents/pi-tool-definition-adapter.after-tool-call.e2e.test.ts` (7 tests pass) - [x] `pnpm tsc --noEmit` (clean, only pre-existing tui.ts error) - [x] `pnpm lint` (0 warnings, 0 errors) 🤖 Generated with [Claude Code](https://claude.com/claude-code)

Most Similar PRs