Skip to main content
Some calls shouldn’t happen without a person watching. You’ll hold every write your agent tries to make over HTTP, find it waiting, and either let it through or turn it down with a reason that lands in the thread’s log.

1. Add the hook

A hook at before_tool_call sees the tool name and its input before the tool runs. A client processor means “stop and ask my application”:
A hook doesn’t grant anything. Roles are checked first, so a tool the roles leave out never reaches the hook. If you came from Narrow an agent’s permissions, that role only allows 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

You should see three lines, in order. 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 a client hook, which is also the maximum) and the call is dropped with wait_expired. Under fail_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.