# Claude Agent SDK

Source: https://pushary.com/docs/agents/build/claude-agent-sdk
Last updated: 2026-10-04

> Claude Agent SDK human in the loop with canUseTool. Your user taps Approve or Deny on their phone before a tool runs, and no answer means the tool is denied.

The Claude Agent SDK asks your code before it runs a tool that needs permission. It calls `canUseTool`, and whatever you return decides. `@pushary/claude-agent-sdk` adds human in the loop to the Claude Agent SDK: it is a `canUseTool` that sends the request to your user's phone and returns their answer. The tool runs only if they tap Approve.

```bash
npm i @pushary/claude-agent-sdk @anthropic-ai/claude-agent-sdk
export PUSHARY_API_KEY=pk_xxx.xxx
```

Get the key at [Partner onboarding](https://pushary.com/onboarding/partner).

## Set up Claude Agent SDK human in the loop [#set-up-claude-agent-sdk-human-in-the-loop]

<Steps>
  <Step>
    ### Connect a user once [#connect-a-user-once]

    ```ts
    import { connect } from '@pushary/claude-agent-sdk'

    const { universalLink } = await connect({ apiKey: process.env.PUSHARY_API_KEY! }, user.id)
    ```

    Show `universalLink` to the signed-in user as a button or QR code. They open it on their phone once. They install the free Pushary app and never sign up or pay.
  </Step>

  <Step>
    ### Pass pusharyCanUseTool to query [#pass-pusharycanusetool-to-query]

    ```ts
    import { query } from '@anthropic-ai/claude-agent-sdk'
    import { pusharyCanUseTool } from '@pushary/claude-agent-sdk'

    for await (const message of query({
      prompt: 'Refund order 1234',
      options: { canUseTool: pusharyCanUseTool({ externalId: user.id }) },
    })) {
      if (message.type === 'result') console.log(message.subtype)
    }
    ```
  </Step>

  <Step>
    ### Run it and answer on the phone [#run-it-and-answer-on-the-phone]

    When Claude wants a tool that needs permission, your user's phone buzzes with the tool name and its input. Approve runs the tool with its input unchanged.
  </Step>
</Steps>

Tools you allow with `allowedTools` never reach `canUseTool`, so your user only sees the calls that need them.

## What happens when nobody answers [#what-happens-when-nobody-answers]

The tool does not run. Each question waits up to `timeoutMs` (55 seconds by default). The model gets a denial it can read:

| Outcome                            | Message the model reads                                                                         |
| ---------------------------------- | ----------------------------------------------------------------------------------------------- |
| Your user taps Deny                | `The approver denied this action.`                                                              |
| Nobody answers in time             | `No answer from the approver, so this was denied. Do not retry the same action.`                |
| The run is cancelled while waiting | `The run was cancelled before anyone answered. Do not retry the same action.`                   |
| Pushary cannot be reached          | `Pushary could not reach a person to decide, so this was denied. Do not retry the same action.` |

Cancelling the run stops the wait and denies at once.

## Who answers [#who-answers]

`externalId` is your own id for the user. Pass a string, or a function that picks the person for each call. The function also runs for `AskUserQuestion`:

```ts
pusharyCanUseTool({ externalId: (toolUse) => ownerOf(toolUse.toolName) })
```

Never take the person from the tool input. The model writes the tool input, so a prompt injection could send the approval to someone else.

## Clarifying questions from AskUserQuestion [#clarifying-questions-from-askuserquestion]

When Claude calls `AskUserQuestion`, the same `canUseTool` puts each question on the person's phone as a choice between Claude's options, with each option's description. It asks one question at a time and returns their picks as the answers. Each question waits up to `timeoutMs`.

If a question goes unanswered, Pushary takes it off the phone, denies the call and tells Claude: `Nobody answered the question in time. Do not assume an answer.` If Pushary cannot take it back within five seconds, the reason goes to `console.warn` and the question expires on its own.

The phone takes one choice per question, so a multi-select question says "(choose one)" and comes back with the one option the person picked. A question too long for the phone is denied, and Claude is told to ask it in plain text instead. Your rules are not asked about questions, including a rule that names `AskUserQuestion`, because a question is not an action.

## Site rules are off unless you turn them on [#site-rules-are-off-unless-you-turn-them-on]

Claude's tools have the same names in your own Claude Code: `Bash`, `Read`, `Write`, `WebFetch`. A rule you wrote for your own Claude Code sessions would also answer your users' calls, so by default every gated call goes to a person.

Set `policy: true` to ask your site's rules first. A rule can then allow a call without paging anyone, or deny it outright. Only turn it on for a site whose rules you wrote for this agent. Rules need a server key from Settings > API keys. With the key from onboarding, `policy: true` has no effect and every gated call still goes to a person.

## Options [#options]

| Option             | What it does                                                                                                       |
| ------------------ | ------------------------------------------------------------------------------------------------------------------ |
| `externalId`       | The person who answers. Required.                                                                                  |
| `question`         | Builds the approval text the person sees. Defaults to the tool name and its input. Not used for `AskUserQuestion`. |
| `sessionId`        | Groups one session's calls, so a replayed turn does not ask twice.                                                 |
| `timeoutMs`        | How long to wait for an answer before denying. Applies to each clarifying question in turn.                        |
| `expiresInSeconds` | How long the question stays answerable.                                                                            |
| `requireReachable` | Deny at once when the person has no connected phone.                                                               |
| `policy`           | Ask your site's rules before a person. Defaults to `false`.                                                        |
| `agentName`        | Shown on the approval so the person knows which agent is asking.                                                   |
| `apiKey`           | Defaults to `PUSHARY_API_KEY`.                                                                                     |

Source: [pushary-claude-agent-sdk on GitHub](https://github.com/Pushary/pushary-claude-agent-sdk). Anthropic reference: [permissions in the Agent SDK](https://code.claude.com/docs/en/agent-sdk/permissions).

## Next steps [#next-steps]

<Cards>
  <Card title="Try a simulated decision" description="Approve or deny a refund in your browser before you connect a phone." href="/try" />

  <Card title="See the Claude Agent SDK approval workflow" description="The whole flow for Claude Agent SDK agents, from connect to answer." href="/human-in-the-loop-claude-agent-sdk" />

  <Card title="Test your integration" description="Use a sandbox key to check approve, deny and no answer without a phone." href="/docs/agents/embed-testing" />
</Cards>