feat(plugins): run pre_tool/post_tool hooks on the Direct-API runtime#417
feat(plugins): run pre_tool/post_tool hooks on the Direct-API runtime#417OBenner wants to merge 1 commit into
Conversation
Plugin tool hooks (pre_tool/post_tool) previously ran only on the Claude SDK backend, because the plugin runtime was built around the SDK's HookMatcher type and wired solely into create_client(). On the Direct-API in-process runtime (generic_edit / full_agent_runtime), tool calls were dispatched by LocalActionExecutor with no hook invocation — so a GENERIC_EDIT plugin's guard (e.g. skill-pack-runtime's skill-script permission guard) was silently bypassed on those backends. Wire the hooks into the Direct-API runtime: - plugin_tool_bridge: a pure, one-directional normalization layer mapping the native local-action vocabulary (run_command, write_file, ...) to the canonical Claude SDK tool vocabulary (Bash, Write, Edit, ...) so hooks written against SDK names match on both backends. Actions with no SDK equivalent get stable extension names (Delete/Move/ApplyPatch). Read-only actions are mapped but gated off by default. - generic_edit: load hook-capable plugins once per session, build the AgentContext per run, and invoke pre_tool (block before execution) and post_tool (observational) at the single _execute_action choke point, covering the executor and MCP-bridge paths. Documented limitations (by design): pre_tool is block-only (no input rewrite), post_tool is observational (no rollback), read-only hooking is opt-in, and Codex/CLI backends remain out of reach (opaque tool loop). augment_prompt cross-backend is intentionally not part of this change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 42 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (5)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
| from pathlib import Path | ||
| from types import SimpleNamespace | ||
|
|
||
| import pytest |



Problem
Plugin tool hooks (
pre_tool/post_tool) ran only on the Claude SDK backend. The plugin runtime was built around the SDK'sHookMatchertype and wired solely intocreate_client(). On the Direct-API in-process runtime (generic_edit/full_agent_runtime), tool calls were dispatched byLocalActionExecutorwith no hook invocation.Consequence: a
GENERIC_EDITplugin's guard was silently bypassed on Direct-API. Concretely,skill-pack-runtime's skill-script permission guard matchestool_name.casefold() in {"bash","shell"}— the nativerun_commandname never matched, so skill scripts ran unguarded exactly in thegeneric_editmode where they execute.Change
Wire the hooks into the Direct-API runtime, mirroring the SDK path:
plugin_tool_bridge.py(new) — a pure, one-directional normalization layer mapping the native local-action vocabulary (run_command,write_file,replace_text, ...) to the canonical Claude SDK tool vocabulary (Bash,Write,Edit, ...), so hooks written against SDK names match on both backends. Actions with no SDK equivalent get stable extension names (Delete/Move/ApplyPatch). Read-only actions are mapped but gated off by default.generic_edit.py— load hook-capable plugins once per session, build theAgentContextper run, and invokepre_tool(block before execution) +post_tool(observational) at the single_execute_actionchoke point, covering both the executor and MCP-bridge paths.plugin-development.md— documents the backend-coverage matrix, the normalization contract, and the limitations below.Backend coverage after this change
pre_tool(block)post_tool(observe)before_session…)Limitations (by design, documented)
pre_toolis block-only on Direct-API — normalization is one-directional, so input rewrite is not honored.post_toolis observational — the action already ran; a block is logged + annotated (plugin_post_tool_block), not rolled back.augment_promptcross-backend is intentionally not part of this PR (separate follow-up).Tests
39 new tests + full regression pass — 310 passing (
test_plugin_tool_bridge,test_generic_edit_plugin_hooks,test_agent_runtime,test_plugin_runtime,test_skill_pack_runtime,test_plugin_integration,test_plugin_control_plane). The anchor test proves both the bug and the fix: the nativerun_commandname bypasses the guard, and the normalizedBashname blocks.🤖 Generated with Claude Code