Hooks

Hooks let you attach your own code to points in the agent loop. Where permissions answer “is this allowed?”, hooks answer “what should happen around it?” — logging, validation, enrichment, or outright blocking. Amara fires each hook at a well-defined moment, passes it a payload describing what is about to happen, and lets your callback observe or influence the outcome.

Lifecycle events

The SDK exposes hook points that map to the phases of a turn. The most commonly used are:

  • preToolUse — fires before a tool executes, with the tool name and input. Return a decision to allow, block, or modify the call.
  • postToolUse — fires after a tool returns, with the result. Useful for auditing, redaction, or triggering side effects.
  • sessionStart / sessionEnd — fire when a session begins and ends, ideal for setting up and tearing down external resources.
  • userPromptSubmit — fires when a new prompt enters the loop, letting you inject context or reject the request.

Registering hooks

Hooks are supplied through the query options as a map from event name to one or more callbacks. Each callback is async and receives the event payload.

import { query } from "@syntic/agent-sdk";
 
for await (const message of query({
  prompt: "Update the changelog.",
  options: {
    hooks: {
      preToolUse: [
        async ({ toolName, input }) => {
          console.log(`About to run ${toolName}`);
          if (toolName === "Bash" && input.command.includes("curl")) {
            return { decision: "block", reason: "Network calls are disabled." };
          }
          return { decision: "continue" };
        },
      ],
    },
  },
})) {
  // ...
}

Observing versus intervening

A hook that returns continue (or nothing) simply observes — perfect for telemetry and audit trails. A hook that returns block stops the action and feeds your reason back to Amara, which can adapt its plan. This makes hooks a natural place to encode organization-specific guardrails that go beyond static allow/deny lists.

async def guard_writes(event):
    if event.tool_name == "Write" and "secrets" in event.input["path"]:
        return {"decision": "block", "reason": "Cannot write to secrets."}
    return {"decision": "continue"}
 
async for message in query(
    prompt="Persist the config.",
    options={"hooks": {"pre_tool_use": [guard_writes]}},
):
    ...