org.jspecify annotations as its only compile dependency.
Adapters (Spring AI, Claude Agent SDK, and Gemini CLI) wire the core into their runtimeβs tool-call lifecycle.
Your hooks move with you when your agent infrastructure changes.
Why Hooks
LLMs are probabilistic β prompt-based instructions drift under token pressure. Agents skip steps, forget constraints, and ignore guardrails. Hooks solve this by moving critical logic out of the prompt and into deterministic code that intercepts every tool call, the same way servlet filters intercept HTTP requests:- Safety β Block dangerous operations before they execute. A
Blockdecision short-circuits immediately and cannot be overridden by later hooks. - Observability β Log every tool call, capture timing data, and feed traces into Agent Journal for behavioral analysis.
- Steering β Modify tool inputs in flight. Subsequent hooks see the modified input, so transformations chain cleanly.
How It Works
Hooks intercept at two points in the tool-call lifecycle:Decision Model
HookDecision is a sealed type with four variants:
Multiple hooks execute in priority order (default: 100, lower = earlier).
AfterToolCall hooks fire in reverse priority order for proper cleanup semantics.
Two behaviors are worth knowing before you rely on them. Dispatch matches an eventβs exact
runtime class, so a hook registered against a supertype such as
ToolEvent.class compiles
but never fires β register against the concrete record. And a hook that throws is logged at
WARNING and treated as Proceed: Block short-circuits, but a failing hook does not fail
closed.
Modules
agent-hooks-claude requires Java 21 because every published claude-code-sdk version is
Java 21 bytecode. The other three modules are unaffected on Java 17.
Event Hierarchy
The event system is open (unsealed) β you can define custom events for your runtime:Quick Start
Write Once, Run Anywhere
The sameAgentHookProvider works on all three runtimes β this is the core value proposition.
@Component bean, auto-configuration handles the rest:
HookContext is an application-wide singleton: every hooked tool call
in the application shares one state map and one tool-call history. That suits a single-user CLI
or desktop app. A multi-user server should define its own request- or session-scoped
HookContext bean, or build one per conversation and call HookedTools.wrap at that point.
Claude Agent SDK β bridge into the Claude HookRegistry:
HookOutput.
Tool call duration is tracked via wall-clock timing across the pre/post hook boundary.
Each Claude session gets its own HookContext for isolated state and history. The bridge
retains those contexts for its own lifetime β fine for one bridge per CLI process; a
long-lived bridge shared across sessions should call evictSession(sessionId) when a session
finishes.
Gemini CLI β stateless subprocess dispatcher reads JSON from stdin:
HookEvent records.
HookContext is fresh per invocation β stateless hooks (security gates, audit logging) work out of the box.
Note: Gemini BeforeTool can only allow or block β Modify is downgraded to allow with a warning.
Documentation
Source Code
Core API, Spring AI adapter, Claude adapter, and Gemini adapter
Design Notes
Architecture decisions, event hierarchy, dispatch semantics