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 blockedfrom 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 blockedTo 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