#20580: feat(hooks): bridge after_tool_call to internal hook handler system
agents
size: S
Cluster:
Plugin Hook Enhancements
## Summary
Bridges the plugin hook system's `after_tool_call` event to the internal hook handler system, so plugins in `~/.openclaw/hooks/<plugin>/handler.ts` can observe tool executions.
Discussed in #20575. Related to #20570.
---
## Problem
OpenClaw has two hook systems that don't talk to each other:
**Plugin Hook System** (internal): fires `after_tool_call` inside the agent pipeline with full tool context.
**Internal Hook Handler System** (external, `~/.openclaw/hooks/`): never receives tool events — blind to everything happening inside a session.
The result: memory, observability, and audit plugins can't record what tools ran, what the inputs were, or what came back.
## Changes
### `src/hooks/internal-hooks.ts`
- Add `"tool"` to `InternalHookEventType`
- Add `ToolAfterCallHookContext` + `ToolAfterCallHookEvent` types
- Add `isToolAfterCallEvent()` type guard
### `src/agents/pi-embedded-subscribe.handlers.tools.ts`
- After existing plugin hook fires → bridge to `triggerInternalHook("tool", "after_call", ...)` (fire-and-forget, never blocks execution)
### `src/agents/pi-tool-definition-adapter.ts`
- Same bridge in both success path and error path
### `src/hooks/internal-hooks.test.ts`
- 6 new tests: `isToolAfterCallEvent` guard (true/false cases) + handler triggering
## Behaviour
The bridge fires **unconditionally** — it does not require any plugin hooks to be registered. It is always fire-and-forget and can never block or throw into the agent pipeline.
Payload delivered to external handlers:
```ts
event.type // "tool"
event.action // "after_call"
event.context // { toolName, params, result?, error?, durationMs? }
```
## Usage Example
```ts
// ~/.openclaw/hooks/my-memory/handler.ts
import type { HookHandler } from '../../src/hooks/hooks.js';
import { isToolAfterCallEvent } from '../../src/hooks/internal-hooks.js';
const handler: HookHandler = async (event) => {
if (isToolAfterCallEvent(event)) {
const { toolName, params, result, error, durationMs } = event.context;
await recordObservation({ toolName, params, result, error, durationMs });
}
};
export default handler;
```
HOOK.md metadata to subscribe:
```yaml
metadata: { "openclaw": { "events": ["tool:after_call"] } }
```
## Test Results
```
Test Files 1 passed (1)
Tests 36 passed (36) ← 6 new, 30 existing
```
Build: `pnpm build` — clean, zero errors.
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Bridges the plugin hook system's `after_tool_call` event to the internal hook handler system. This enables external plugins in `~/.openclaw/hooks/<plugin>/handler.ts` to observe tool executions that were previously only visible to internal plugin hooks.
- Adds `"tool"` event type to `InternalHookEventType` union with `ToolAfterCallHookContext` and `ToolAfterCallHookEvent` types
- Adds `isToolAfterCallEvent()` type guard for type-safe event filtering
- Bridges tool execution events in both `pi-embedded-subscribe.handlers.tools.ts` (embedded agent path) and `pi-tool-definition-adapter.ts` (tool definition adapter paths)
- Fire-and-forget pattern ensures hooks never block tool execution
- Comprehensive test coverage with 6 new tests validating type guards and handler triggering
<h3>Confidence Score: 5/5</h3>
- This PR is safe to merge with minimal risk
- The implementation follows established patterns in the codebase, uses fire-and-forget to prevent blocking, includes comprehensive test coverage (6 new tests, all passing), and maintains type safety throughout. The changes are isolated to the hook system with no breaking changes to existing APIs.
- No files require special attention
<sub>Last reviewed commit: 81572ae</sub>
<!-- greptile_other_comments_section -->
<sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub>
<!-- /greptile_comment -->
Most Similar PRs
#10678: feat(hooks): wire after_tool_call hook into tool execution pipeline
by yassinebkr · 2026-02-06
85.1%
#17667: feat: tool-hooks extension — run shell commands on tool calls
by FaradayHunt · 2026-02-16
82.9%
#22068: Add tool:before/tool:after internal hook events
by yhindy · 2026-02-20
80.7%
#18889: feat(hooks): add agent and tool lifecycle boundaries
by vincentkoc · 2026-02-17
78.1%
#17930: fix: evaluate tool_result_persist hooks lazily to avoid race condition
by TheArkifaneVashtorr · 2026-02-16
77.9%
#16618: feat: bridge message lifecycle hooks to workspace hook system
by DarlingtonDeveloper · 2026-02-14
77.3%
#18860: feat(agents): expose tools and their schemas via new after_tools_re...
by lan17 · 2026-02-17
77.0%
#16028: feat/before-tool-result
by ambushalgorithm · 2026-02-14
76.9%
#6405: feat(security): Add HTTP API security hooks for plugin scanning
by masterfung · 2026-02-01
76.8%
#23019: fix(hooks): use globalThis singleton for internal hooks handlers Map
by karmafeast · 2026-02-21
76.8%