Skip to content

feat(plugins): run pre_tool/post_tool hooks on the Direct-API runtime#417

Open
OBenner wants to merge 1 commit into
developfrom
claude/hopeful-ishizaka-d23950
Open

feat(plugins): run pre_tool/post_tool hooks on the Direct-API runtime#417
OBenner wants to merge 1 commit into
developfrom
claude/hopeful-ishizaka-d23950

Conversation

@OBenner

@OBenner OBenner commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Problem

Plugin tool hooks (pre_tool/post_tool) ran only on the Claude SDK backend. 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.

Consequence: a GENERIC_EDIT plugin's guard was silently bypassed on Direct-API. Concretely, skill-pack-runtime's skill-script permission guard matches tool_name.casefold() in {"bash","shell"} — the native run_command name never matched, so skill scripts ran unguarded exactly in the generic_edit mode 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 the AgentContext per run, and invoke pre_tool (block before execution) + post_tool (observational) at the single _execute_action choke 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

Hook Claude SDK Direct-API in-process Codex / CLI
pre_tool (block) ❌ (opaque loop)
post_tool (observe)
lifecycle (before_session …)

Limitations (by design, documented)

  • pre_tool is block-only on Direct-API — normalization is one-directional, so input rewrite is not honored.
  • post_tool is observational — the action already ran; a block is logged + annotated (plugin_post_tool_block), not rolled back.
  • Read-only hooking is opt-in (off by default) so plugins don't run on every file read.
  • Codex/CLI backends remain out of reach (they own their tool loop).
  • augment_prompt cross-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 native run_command name bypasses the guard, and the normalized Bash name blocks.

🤖 Generated with Claude Code

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>
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@OBenner, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 42 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: de2e3732-7334-4c12-97ae-edbe1b88c8e3

📥 Commits

Reviewing files that changed from the base of the PR and between 4aa2449 and a80f0c0.

📒 Files selected for processing (5)
  • apps/backend/agents/runtime/adapters/generic_edit.py
  • apps/backend/agents/runtime/plugin_tool_bridge.py
  • docs/guides/plugin-development.md
  • tests/test_generic_edit_plugin_hooks.py
  • tests/test_plugin_tool_bridge.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/hopeful-ishizaka-d23950

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

Copy link
Copy Markdown

from pathlib import Path
from types import SimpleNamespace

import pytest
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants