Skip to content

defineHook

ts
import { defineHook } from "@pivanov/agent-hooks-bridge";

Type-checked builder for your event handler map. Identity at runtime; its only job is to give you autocomplete and per-event type narrowing.

Signature

ts
const defineHook: (handlers: THandlerMap) => THandlerMap;

type THandlerMap = {
  SessionStart?: (event: ISessionStartEvent) => THandlerResult | Promise<THandlerResult>;
  PreToolUse?: (event: IPreToolUseEvent) => THandlerResult | Promise<THandlerResult>;
  PostToolUse?: (event: IPostToolUseEvent) => THandlerResult | Promise<THandlerResult>;
  UserPromptSubmit?: (event: IUserPromptSubmitEvent) => THandlerResult | Promise<THandlerResult>;
  Stop?: (event: IStopEvent) => THandlerResult | Promise<THandlerResult>;
};

type THandlerResult = IHookResponse | void;

Single event

ts
const hooks = defineHook({
  PreToolUse: (event) => {
    if (event.tool === "Bash") {
      return { decision: "allow" };
    }
    return {};
  },
});

event is narrowed to IPreToolUseEvent inside the handler. event.tool, event.tool_input are typed.

Multi-event

ts
const hooks = defineHook({
  SessionStart: async (event) => ({
    additional_context: await readFile("AGENTS.md", "utf8"),
  }),

  PreToolUse: (event) => {
    if (event.tool === "Bash" && isDangerous(event.tool_input.command)) {
      return { decision: "deny", reason: "blocked by policy" };
    }
    return {};
  },

  Stop: () => {
    sendDesktopNotification("Agent done");
  },
});

Handlers run only for events the host fires. Events without a handler exit cleanly with code 0.

Returning nothing

A handler that returns undefined (or has no return) is treated as { decision: "allow" }: pure observation, no decision, no exit-code change.

ts
defineHook({
  PreToolUse: (event) => {
    log(`tool: ${event.tool}`);
    // no return → no-op response
  },
});

Handlers can be async

ts
defineHook({
  PreToolUse: async (event) => {
    const allowed = await checkPolicy(event);
    return allowed ? { decision: "allow" } : { decision: "deny", reason: "policy" };
  },
});

run() awaits the handler before serializing. The bridge does not impose a per-handler timeout; the host's hook timeout governs.

Accessing host-specific data via _native

Every parsed event carries the original payload as event._native: unknown. Cast to inspect tool-specific fields:

ts
defineHook({
  PreToolUse: (event) => {
    if (event.host === "cursor") {
      const native = event._native as { cursor_version?: string; sandbox?: boolean };
      // ...
    }
  },
});

This is the escape hatch for fields the unified API doesn't model.

Not affiliated with Anthropic, Anysphere, or OpenAI. Supported by LogicStar AI.