1. Add the hook
A hook atbefore_tool_call sees the tool name and its input
before the tool runs. A client processor means “stop and ask my application”:
http.request for GET. Drop the where from that entry; the hook is
what stops writes now, and it can ask you instead of refusing outright. Also,
the hook list you send replaces the whole list, so the six
default approvals are gone unless you send
them again.
2. Trigger it
Ask the agent for something it can only do by writing:3. Find what’s waiting
One filter on the thread list gives you everything waiting on a person. The turn runs in the background, so poll until yours shows up:pendingWaits is the list of calls stopped and waiting for an answer. Each
entry carries the hookId that fired, the toolName and toolInput about to
run, and an expiresAt. That’s your approval queue; show it to whoever
decides.
4. Decide
allow releases the call and the turn picks up where it stopped. deny drops
it and the model is told why, so give a reason worth reading.
Check it worked
hook_decision ask is the hook deciding
to ask you instead of answering for you. wait_created is the turn stopping.
wait_resolved is your answer; its waitResult holds the decision and the
reason you sent. After that, the tool_result shows the call finally running.
Deny instead and that tool_result carries failureCode: hook_denied with your
reason as failureText. See Thread events.
When it goes sideways
- Nobody answers. The wait expires after
asyncTimeoutMs(24 hours for aclienthook, which is also the maximum) and the call is dropped withwait_expired. Underfail_closed, silence counts as a no. - Two people answer. The second resolve gets
409 Conflict. A wait settles once. - You edit the profile while a call is waiting. Nothing changes. The turn is running against the snapshot it started with, so your edit applies to the next turn, not this one.
Where next
Hooks reference
Points, processors, decisions, and the defaults.
Run on a schedule
Same agent, no one at the keyboard.