# Tool call hooks

`com.tnsai.server.hooks` runs a log line or a shell command when an agent calls a tool, when a tool call fails, or when you start and stop an agent. Each hook pairs an event, an optional tool-name matcher and one action. You build a `DeclarativeHookRegistry` from YAML or from code and attach it to an agent with `applyTo`.

The hooks are a library. `TnsServer` does not load a hooks file and does not attach hooks to the agents it creates. Your code decides which agents get hooks.

## Example

<!-- java-contract: src/main/java/com/example/tnsai/docs/ServerHooksExample.java -->
```java
package com.example.tnsai.docs;

import com.tnsai.agents.Agent;
import com.tnsai.server.hooks.DeclarativeHookRegistry;
import com.tnsai.server.hooks.HookAction;
import com.tnsai.server.hooks.HookConfig;
import com.tnsai.server.hooks.HookEvent;
import com.tnsai.server.hooks.HookMatcher;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;

/** Compilation contract for server/hooks.md. */
public final class ServerHooksExample {

    private ServerHooksExample() {
    }

    static DeclarativeHookRegistry fromFile(Agent agent, Path hooksYaml) throws IOException {
        DeclarativeHookRegistry hooks;
        try (InputStream in = Files.newInputStream(hooksYaml)) {
            hooks = DeclarativeHookRegistry.fromYaml(in);    // never throws; bad entries are skipped
        }
        hooks.applyTo(agent);                                // replaces the agent's tool call listener
        return hooks;
    }

    static DeclarativeHookRegistry inCode(Agent agent) {
        DeclarativeHookRegistry hooks = DeclarativeHookRegistry.of(List.of(
            new HookConfig(HookEvent.PRE_TOOL_USE, new HookMatcher("file_*"),
                HookAction.log("${tool} starting")),
            new HookConfig(HookEvent.POST_TOOL_USE, HookMatcher.ALL,
                HookAction.log("${tool} took ${latencyMs} ms, success=${success}")),
            new HookConfig(HookEvent.ON_ERROR, null,
                HookAction.command("echo tool failed >> hooks.log"))));
        hooks.applyTo(agent);                                // fires ON_START once
        return hooks;
    }

    static void stop(DeclarativeHookRegistry hooks, Agent agent) {
        hooks.fireEvent(HookEvent.ON_STOP, Map.of("agent", agent.getId()));   // the registry never fires it
    }

    static DeclarativeHookRegistry recorded(List<String> sink) {
        return DeclarativeHookRegistry.of(
            List.of(new HookConfig(HookEvent.POST_TOOL_USE, HookMatcher.ALL, HookAction.log("${tool}"))),
            (action, context) -> sink.add(action.type() + " " + context));       // your own executor
    }
}
```

## YAML format

`fromYaml` reads a top-level `hooks` list:

```yaml
hooks:
  - event: preToolUse
    matcher: "file_*"
    action:
      type: log
      message: "${tool} starting"
  - event: postToolUse
    matcher: "*"
    action:
      type: log
      message: "${tool} took ${latencyMs} ms, success=${success}"
  - event: onError
    action:
      type: command
      command: "echo tool failed >> hooks.log"
```

Event names can be camelCase (`preToolUse`) or upper snake case (`PRE_TOOL_USE`). Action types are `log` and `command`, in any case. A `log` action needs `message`, and a `command` action needs `command`.

`fromYaml` does not throw. An entry with a missing or unknown event, action type or field is skipped with a WARN log. If the whole document fails to parse, it logs an ERROR and returns an empty registry. Check `getHooks()` after loading if a missing hook would matter.

## Events

| Event | Fired by | Context variables |
|-------|----------|-------------------|
| `PRE_TOOL_USE` | The agent, before each tool call | `tool`, `event` (`preToolUse`) |
| `POST_TOOL_USE` | The agent, after each tool call, whether it succeeded or failed | `tool`, `event` (`postToolUse`), `success`, `latencyMs` |
| `ON_ERROR` | The agent, after a failed tool call | `tool`, `event`, `success`, `latencyMs`, `error` (the exception message) |
| `ON_START` | `applyTo`, once | `agent` (the agent id) |
| `ON_STOP` | Only your own `fireEvent` call | Whatever you pass |

A tool call that the agent's authorization check denies never reaches the tool, so it fires no hooks.

You can fire any event yourself. `fireEvent(event, context)` runs every hook for that event and passes your map as the context. `fireToolEvent(event, toolName, extraContext)` applies the matchers to `toolName` and adds it to the context as `tool`.

## Matchers

A `HookMatcher` compares the tool name against a glob. `*` matches any run of characters, and every other character matches itself. The comparison ignores case. `HookMatcher.ALL` is `*`.

Matchers apply to `PRE_TOOL_USE` and `POST_TOOL_USE`. A hook with no matcher runs for every tool. On `ON_START`, `ON_STOP` and `ON_ERROR` the matcher is ignored, so an `ON_ERROR` hook runs for every failing tool.

## Actions

A `log` action writes the message at INFO, prefixed with `[HOOK]`, to the `com.tnsai.server.hooks.DeclarativeHookExecutor` logger.

A `command` action runs `sh -c <command>` in the process working directory. It waits for up to 30 seconds and then kills the command. A non-zero exit is logged at WARN with up to 50 lines of output. A failing action never stops the tool call or the other hooks.

In both actions, `${name}` is replaced with the context value of that name. A name that is not in the context stays as written.

Hooks run on the thread that runs the tool, so the tool waits for every `PRE_TOOL_USE` hook to finish. A slow command delays each matching tool call by up to 30 seconds. A hook cannot veto a call; use [tool approval](tool-approval.md) or the [approval policies](../security/approvals.md) for that.

Substituted values are not quoted. In a `command` action, `${tool}` comes from the model's tool call and `${error}` from an exception message, and either can contain shell syntax. Use these variables in `log` actions. If a command needs them, pass them to a script that treats them as data.

## Custom executor

`fromYaml(in, executor)` and `of(hooks, executor)` take your own `HookExecutor` in place of the shell and log executor. The executor receives each matching action and its context map. Use it to record hooks in tests or to send them to your own sink.

## Attaching to an agent

`applyTo(agent)` sets the agent's tool call listener. An agent has one listener, so `applyTo` replaces any listener already set, and a later `setToolCallListener` call removes the hooks. A registry with no hooks does nothing: it sets no listener and fires no `ON_START`.

One registry can be applied to several agents. `ON_START` fires once for each `applyTo` call.

## Related

- [Tool approval](tool-approval.md)
- [Run the server](run.md)
- [Approval policies](../security/approvals.md)
