Skip to main content

Documentation Index

Fetch the complete documentation index at: https://lab.pollack.ai/llms.txt

Use this file to discover all available pages before exploring further.

Every agent framework implements hooks differently. Claude Code has shell-based hooks. Strands has steering callbacks. Spring AI has advisors. Your safety policy, logging, and steering logic gets rewritten for each one. Agent Hooks is a portable Java API that lets you write hook logic once and run it on any runtime that has an adapter. The core module has zero dependencies — it defines the event model, decision types, and registry. 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 Block decision 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:
BeforeToolCall ──► Tool Executes ──► AfterToolCall
     │                                    │
  Block?  ◄── short-circuit            Retry?
  Modify? ◄── chains                   Log / observe
  Proceed ◄── default                  Cleanup (reverse order)
Register hooks with type-safe generics and optional tool-name filtering:
// Block all shell commands
registry.onTool("shell.*", BeforeToolCall.class, event ->
    HookDecision.block("Shell access disabled in this environment"));

// Log every tool call
registry.on(AfterToolCall.class, event -> {
    log.info("{} completed in {}ms", event.toolName(), event.duration().toMillis());
    return HookDecision.proceed();
});

// Redirect file writes to a sandbox directory
registry.onTool("write.*", BeforeToolCall.class, event -> {
    String sandboxed = event.toolInput()
        .replace("/home/user", "/sandbox");
    return HookDecision.modify(sandboxed);
});

Decision Model

HookDecision is a sealed type with four variants:
DecisionWhenBehavior
ProceedDefaultTool executes normally
BlockSafety / policyShort-circuits immediately — later hooks never run
ModifyInput transformationPasses modified input to the next hook in the chain
RetryAfterToolCall onlyRe-executes the tool (e.g., after transient failure)
Multiple hooks execute in priority order (default: 100, lower = earlier). AfterToolCall hooks fire in reverse priority order for proper cleanup semantics.

Modules

ModuleWhat it doesDependencies
agent-hooks-corePure Java 17 API — events, decisions, registryZero (portable)
agent-hooks-springSpring AI adapter — wraps ToolCallback with hook dispatch, auto-configures via BootSpring AI, Spring Boot
agent-hooks-claudeClaude Agent SDK adapter — bridges hook providers to Claude CLI hooks via AgentHookBridgeClaude Code SDK (provided)
agent-hooks-geminiGemini CLI adapter — stateless stdin/stdout dispatcher for Gemini’s subprocess-per-event modelJackson (compile)

Event Hierarchy

The event system is open (unsealed) — you can define custom events for your runtime:
EventInterfaceDecisions
BeforeToolCallToolEventProceed, Block, Modify
AfterToolCallToolEventProceed, Retry
SessionStartHookEventObservation only
SessionEndHookEventObservation only
UserPromptSubmitHookEventObservation only (Claude adapter)
AgentStopHookEventObservation only (Claude adapter)
SubagentStopHookEventObservation only (Claude adapter)
PreCompactHookEventObservation only (Claude adapter)
GeminiBeforeAgentHookEventObservation only (Gemini adapter)
GeminiAfterAgentHookEventObservation only (Gemini adapter)
GeminiBeforeModelHookEventObservation only (Gemini adapter)
GeminiAfterModelHookEventObservation only (Gemini adapter)
GeminiBeforeToolSelectionHookEventObservation only (Gemini adapter)
GeminiNotificationHookEventObservation only (Gemini adapter)
GeminiPreCompressHookEventObservation only (Gemini adapter)

Quick Start

<!-- Core API (zero dependencies) -->
<dependency>
    <groupId>io.github.markpollack</groupId>
    <artifactId>agent-hooks-core</artifactId>
    <version>0.6.2</version>
</dependency>

<!-- Spring AI adapter (auto-configured) -->
<dependency>
    <groupId>io.github.markpollack</groupId>
    <artifactId>agent-hooks-spring</artifactId>
    <version>0.6.2</version>
</dependency>
<!-- Claude Agent SDK adapter -->
<dependency>
    <groupId>io.github.markpollack</groupId>
    <artifactId>agent-hooks-claude</artifactId>
    <version>0.6.2</version>
</dependency>
<!-- Gemini CLI adapter (stateless subprocess) -->
<dependency>
    <groupId>io.github.markpollack</groupId>
    <artifactId>agent-hooks-gemini</artifactId>
    <version>0.6.2</version>
</dependency>

Write Once, Run Anywhere

The same AgentHookProvider works on all three runtimes — this is the core value proposition.
// This provider works on Spring AI, Claude CLI, and Gemini CLI
public class SecurityHooks implements AgentHookProvider {
    @Override
    public void registerHooks(AgentHookRegistry registry) {
        registry.onTool("Bash", BeforeToolCall.class, event ->
            HookDecision.block("Shell access not permitted"));
    }
}
Spring AI — register as a @Component bean, auto-configuration handles the rest:
@Component
public class MySecurityHooks extends SecurityHooks {}
Claude Agent SDK — bridge into the Claude HookRegistry:
AgentHookRegistry registry = new AgentHookRegistry();
registry.register(new SecurityHooks());

AgentHookBridge bridge = new AgentHookBridge(registry);
bridge.registerInto(claudeHookRegistry);
The bridge registers callbacks for all six Claude hook events (PreToolUse, PostToolUse, UserPromptSubmit, Stop, SubagentStop, PreCompact). It converts Claude SDK types to core events, dispatches through your hooks, and maps decisions back to Claude’s 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. Gemini CLI — stateless subprocess dispatcher reads JSON from stdin:
public class MyGeminiHooks {
    public static void main(String[] args) throws Exception {
        GeminiHookDispatcher.create(new SecurityHooks())
            .run();  // reads stdin, dispatches, writes stdout, exits
    }
}
Gemini CLI spawns the hook process per event. The dispatcher maps all 11 Gemini events to core and Gemini-specific 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

Used By

  • Agent Workflow — hooks apply automatically to any workflow step that invokes tools
  • Agent Journal — hook provider that logs tool-call events to a journal Run