Claude Agent SDK
Phone approvals for Claude Agent SDK agents. Your agent asks, your user taps Approve or Deny.
The Claude Agent SDK asks your code before it runs a tool that needs permission. It
calls canUseTool, and whatever you return decides. Somebody still has to decide.
@pushary/claude-agent-sdk is a canUseTool that sends the request to your user's
phone and returns their answer.
Install
npm i @pushary/claude-agent-sdk @anthropic-ai/claude-agent-sdk
export PUSHARY_API_KEY=pk_xxx.sk_xxxGet the key at Partner onboarding.
Connect a user once
import { connect } from '@pushary/claude-agent-sdk'
const { universalLink } = await connect({ apiKey: process.env.PUSHARY_API_KEY! }, user.id)The user opens the link on their phone once. They install the free Pushary app and never sign up or pay.
Ask before a tool runs
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)
}Approve runs the tool with its input unchanged. Deny, or no answer in time, returns a denial the model can read, and the tool does not run. Cancelling the run stops the wait and denies at once.
Tools you allow with allowedTools never reach canUseTool, so your user only sees
the calls that need them.
Who answers
externalId is your own id for the user. Pass a string, or a function that picks the
person for each call:
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
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,
one question at a time, and returns their picks as the answers. Each question waits up
to timeoutMs. If a question goes unanswered, the call is denied and Claude is told
not to assume an answer.
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. Your rules are not asked
about questions, including a rule that names AskUserQuestion, because a question is
not an action. A question too long for the phone is denied, and Claude is told to ask
it in plain text instead.
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.
Source: pushary-claude-agent-sdk on GitHub. Anthropic reference: permissions in the Agent SDK.