Tool calls and approvals
Show what the agent is doing, and let a person, or your code, decide what it may do.
Show tool calls
Tool calls arrive as tool-call events, with a callId that ties related events together:
const cards = new Map<string, Card>()
core.subscribe(e => {
if (e.type !== "session.event") return
const ev = e.event
if (ev.kind === "tool-call") {
cards.set(ev.callId, { tool: ev.tool, title: ev.title, phase: ev.phase, category: ev.category, input: ev.input })
}
if (ev.kind === "command-output") cards.get(ev.callId)?.output.push(ev.delta)
if (ev.kind === "file-diff") showDiff(ev.path, ev.diff)
})phase goes started → updated → completed or failed. category (read, edit, execute, fetch, mcp…) is handy for icons.
Ask a person
A request stays pending until it's answered. Show it, and send back what the person picked:
// When the page loads, list what's waiting:
const waiting = core.sessions.live(id)?.requests.list() ?? []
// When the person clicks a button:
await core.sessions.live(id)?.requests.respond(requestId, { optionId })- Each option has an
id, akind(allow_once,allow_always,reject_once,reject_always) and alabel. Show the labels; send the id. - A session with something pending is a good "needs you" signal in your UI.
- A request never expires while your process is attached. See Permissions.
Questions work the same way, with a different answer:
await session.requests.respond(requestId, { answers: { [questionId]: optionId } })
await session.requests.respond(requestId, { decline: true })Nobody at the keyboard
For scripts and CI, refuse anything you don't expect, or interrupt:
core.subscribe(e => {
if (e.type !== "session.event") return
const session = core.sessions.live(e.sessionId)
if (e.event.kind === "permission-request") {
const reject = e.event.options.find(o => o.kind === "reject_once")
if (reject) session?.requests.respond(e.event.requestId, { optionId: reject.id, message: "Not allowed in CI" })
else session?.interrupt({ pending: "discard" })
}
if (e.event.kind === "user-question" && e.event.blocking) {
session?.requests.respond(e.event.requestId, { decline: true })
}
})Or start the session in a mode that never asks. See Running unattended.
Your own tools, without prompts
Tools from your own MCP server can be allowed up front:
- Claude Code: list them in
allowedTools, namedmcp__<server>__<tool>, for exampleallowedTools: ["mcp__ops__deploy"]. - Codex: host tools are approved automatically, even with
sandbox: "read-only"andapprovalPolicy: "never".