A3S Docs
A3S CodeExamples

Hooks

Lifecycle event interception — PreToolUse, PostToolUse, PrePrompt, PostResponse, OnError

Hooks

Hooks let you intercept agent lifecycle events to audit, modify, or block operations. Register a Hook + HookHandler pair on a session.

Hook Event Types

Prop

Type

Audit Hook (Log All Tool Calls)

const toolLog: string[] = [];

session.registerHook('audit', 'pre_tool_use', (event) => {
  const tool = event.tool || 'unknown';
  console.log(`→ tool: ${tool}`);
  toolLog.push(tool);
  return null; // allow execution
});

console.log(`Hooks: ${session.hookCount()}`);

const result = await session.send('List files in the workspace');

console.log('Tools called:', toolLog);
session.unregisterHook('audit');

Run: npx ts-node examples/test_advanced_features.ts Source: sdk/node/examples/test_advanced_features.ts

tool_log = []

def on_pre_tool_use(event):
    tool = event.get("tool", "unknown")
    print(f"→ tool: {tool}")
    tool_log.append(tool)
    return None  # allow execution

session.register_hook("audit", "pre_tool_use", on_pre_tool_use)
print(f"Hooks: {session.hook_count()}")

result = session.send("List files in the workspace")

print(f"Tools called: {tool_log}")
session.unregister_hook("audit")

Run: python examples/test_advanced_features.py Source: sdk/python/examples/test_advanced_features.py

Block Hook (Deny Specific Tools)

session.registerHook('no-bash', 'pre_tool_use', (event) => {
  if (event.tool === 'bash') {
    return { action: 'block', reason: 'bash is not allowed' };
  }
  return null;
});
def deny_bash(event):
    if event.get("tool") == "bash":
        return {"action": "block", "reason": "bash is not allowed"}
    return None

session.register_hook("no-bash", "pre_tool_use", deny_bash)

Sentinel Agent (Monitor + Block + Alert)

A sentinel attaches two hooks to a session — one that blocks dangerous commands before they run, and one that detects sensitive data in output. Hooks propagate automatically to every sub-agent at any depth.

import { Agent } from '@a3s-lab/code';

const BLOCKED = ['rm -rf', 'mkfs', 'dd if=', 'DROP TABLE', 'curl | sh'];
const SENSITIVE = ['password=', 'secret=', 'api_key=', 'token='];

function attachSentinel(session: ReturnType<Agent['session']>, label = '') {
  const tag = label ? `sentinel-${label}-` : 'sentinel-';

  session.registerHook(
    `${tag}pre`, 'pre_tool_use', null, { priority: 1 },
    (event) => {
      const p = (event.payload ?? event) as Record<string, unknown>;
      if (!['bash', 'shell'].includes(String(p.tool ?? ''))) return null;
      const cmd = String((p.args as Record<string, unknown>)?.command ?? '');
      for (const pat of BLOCKED) {
        if (cmd.toLowerCase().includes(pat.toLowerCase())) {
          console.log(`⛔  BLOCKED: '${pat}' in command`);
          return { action: 'block', reason: `[sentinel] '${pat}'` };
        }
      }
      return null;
    },
  );

  session.registerHook(
    `${tag}post`, 'post_tool_use', null,
    { priority: 1, asyncExecution: true },
    (event) => {
      const p = (event.payload ?? event) as Record<string, unknown>;
      const output = String((p.result as Record<string, unknown>)?.output ?? '');
      for (const kw of SENSITIVE) {
        if (output.toLowerCase().includes(kw.toLowerCase())) {
          console.log(`⚠️   ALERT: sensitive keyword '${kw}' in output`);
          break;
        }
      }
      return null;
    },
  );
}

const agent = await Agent.create('agent.hcl');
const session = agent.session('/workspace', { permissive: true });
attachSentinel(session);

// safe — passes through
const r1 = await session.send('Use bash to list /tmp');
console.log(r1.text);

// dangerous — sentinel blocks before bash runs
const r2 = await session.send('Use bash: rm -rf /tmp/test');
console.log(r2.text); // agent explains it was blocked

Source: sdk/node/examples/sentinel_agent.ts

from typing import Optional
from a3s_code import Agent

BLOCKED = ["rm -rf", "mkfs", "dd if=", "DROP TABLE", "curl | sh"]
SENSITIVE = ["password=", "secret=", "api_key=", "token=", "Bearer "]

def on_pre_tool_use(event: dict) -> Optional[dict]:
    p = event.get("payload", event)
    tool = p.get("tool", "")
    if tool not in ("bash", "shell"):
        return None
    cmd = p.get("args", {}).get("command", "")
    for pat in BLOCKED:
        if pat.lower() in cmd.lower():
            print(f"⛔  BLOCKED: '{pat}' in command")
            return {"action": "block", "reason": f"[sentinel] '{pat}'"}
    return None

def on_post_tool_use(event: dict) -> Optional[dict]:
    p = event.get("payload", event)
    result = p.get("result", {})
    output = result.get("output", "") if isinstance(result, dict) else ""
    for kw in SENSITIVE:
        if kw.lower() in output.lower():
            print(f"⚠️   ALERT: sensitive keyword '{kw}' in output")
            break
    return None

def attach_sentinel(session, label: str = "") -> None:
    tag = f"sentinel-{label}-" if label else "sentinel-"
    session.register_hook(
        f"{tag}pre", "pre_tool_use",
        config={"priority": 1},
        handler=on_pre_tool_use,
    )
    session.register_hook(
        f"{tag}post", "post_tool_use",
        config={"priority": 1, "async_execution": True},
        handler=on_post_tool_use,
    )

agent = Agent.create("agent.hcl")
session = agent.session("/workspace", permissive=True)
attach_sentinel(session)

# safe — passes through
r = session.send("Use bash to list /tmp")
print(r.text)

# dangerous — sentinel blocks before bash runs
r = session.send("Use bash: rm -rf /tmp/test")
print(r.text)  # agent explains it was blocked

Source: sdk/python/examples/sentinel_agent.py

To add a sentinel to every member of an AgentTeam, attach it to each session before binding:

from a3s_code import Agent, Team, TeamRunner, TeamConfig

agent  = Agent.create("agent.hcl")
team   = Team("my-team", TeamConfig(max_tasks=20, max_rounds=6))
team.add_member("lead",     "lead")
team.add_member("worker",   "worker")
team.add_member("reviewer", "reviewer")

runner = TeamRunner(team)

for role in ("lead", "worker", "reviewer"):
    s = agent.session("/workspace", permissive=True)
    attach_sentinel(s, role)
    runner.bind_session(role, s)

result = runner.run_until_done("Analyze the codebase for security issues")

For persistent hook configuration across sessions, see Hooks.

API Reference

Hook Registration

Prop

Type

HookResponse (Rust)

Prop

Type

Hook Event Fields

Prop

Type

On this page