# Quickstart
Source: https://pushary.com/docs/agents/quickstart
Last updated: 2026-10-12
> Connect Claude Code, Codex, Gemini CLI, Cursor, Hermes or another AI agent to your phone with one command. Then approve risky actions from anywhere.
This quickstart connects your AI agent to your phone. Run one command on the computer where your agent works, then open the QR link on your phone. You sign in or create an account during pairing, and setup saves your key automatically. Supported hooks apply your approval policy, including its wait and timeout behavior. It works with Claude Code, Codex, Gemini CLI, Cursor, VS Code, OpenCode, Hermes, fx and the [other supported agents](/docs/agents/supported-agents).
```bash
npx pushary@latest setup
```
On a Mac? The [Mac app, Pushary Isle](/docs/agents/mac-app), finds your agents and sets them up for you. No terminal needed.
## What you need [#what-you-need]
* **Your AI agent**, installed on the computer where it runs.
* **Node.js for CLI setup: 20.17 or newer** (22.13+, 23.5+ and 24+ also work; 21 does not). Check with `node -v`. [Get Node.js](https://nodejs.org/en/download). Native Mac app setup does not need Node/npm.
* **Your phone.** Install the Pushary app from the [App Store](https://apps.apple.com/us/app/pushary/id6785677563) or [Google Play](https://play.google.com/store/apps/details?id=com.pushary.app), before or during pairing.
* **A Pushary account and paid plan.** During pairing, the phone app shows the store's local price and any eligible trial. Review its price, trial eligibility and payment method before confirming. Eligible web signups offer a 3-day trial with a card required, then $9.99 a month for the Agent plan; these web terms do not determine native store offers. Existing subscribers sign in to the same account.
## Already set up? [#already-set-up]
Keep the installation that already owns your agent's hooks. For Mac app setup, check the intended account, subscription and agent connection in the app, use its connection/repair controls, and send a harmless test question through your current agent. Do not install Node/npm to inspect native setup.
For CLI-owned setup with Node/npm available, run `npx pushary@latest status --json` to check the selected account and answering devices before running setup again. Use [Troubleshooting](/docs/agents/reference/troubleshooting) to repair it. Changing hook ownership is an explicit choice described in the [CLI reference](/docs/agents/reference/cli).
## If your AI agent is doing setup [#if-your-ai-agent-is-doing-setup]
Give it this page and the [Pushary agent skill](https://raw.githubusercontent.com/Pushary/pushary-skill/main/skills/pushary/SKILL.md). It should first check the computer and preserve any existing setup owner. For fresh CLI setup, it runs setup, shows you the exact phone link and fingerprint, and keeps the process running while you connect. You complete sign-in, subscription checkout where required, notification permission, and connection approval on your phone.
Setup works with piped output and automatically selects detected agents without a terminal checkbox. To connect only one, your agent can run `npx pushary@latest setup --agents codex`, for example. CI without a key cannot pair. Do not use `--skip-phone` when the goal is phone approvals, or claim that tools are available before the agent restarts and loads them.
## Set it up [#set-it-up]
### Run setup on your computer [#run-setup-on-your-computer]
Open a terminal on the computer where your agent runs and paste:
```bash
npx pushary@latest setup
```
On Windows PowerShell, use `npx.cmd pushary@latest setup` instead.
The terminal shows a QR code, a phone link, and a fingerprint under **Connect your phone**. Keep setup running while you complete the phone steps; the pairing code lasts about 15 minutes.
### Open the link on your phone and sign in [#open-the-link-on-your-phone-and-sign-in]
Scan the QR with your phone's camera or open the printed link on your phone. Install the [Pushary app](/download) if needed, then sign in or create an account. Activate a plan if required, reviewing the displayed price and any trial offer first; existing subscribers use the same account. After installing the app, return to the original phone link and tap **Open Pushary** to finish connecting. If the app restarts during sign-in or payment, reopen that link or scan again. Setup keeps waiting while you do this; if the code expires, run setup again for a fresh one. You can also scan from inside the app.
### Approve the connection [#approve-the-connection]
The app asks **Connect this computer?** Check that the code on your phone matches the code in the terminal, then tap **Approve**. Allow notifications when the app asks.
The terminal says **App connected**. Your API key is now created and saved on this computer. There is nothing to copy or paste.
### Pick your agents [#pick-your-agents]
At an interactive terminal, setup asks **Which agents do you use?** The agents it found on your computer are already ticked. Press space to tick or untick one, then Enter. When an AI agent runs setup with piped output, detected agents are selected automatically; `--agents` selects the ones to configure.
Setup then writes each agent's config: the Pushary MCP server, and approval hooks for agents that support them. It may ask two yes or no questions, and both default to No:
* Add a Pushary section to `CLAUDE.md` or `AGENTS.md` in this folder, so agents here know to ask through Pushary.
* Start `claude` through Pushary, so your phone can reach idle Claude Code sessions.
### Answer the test question [#answer-the-test-question]
Setup sends a test notification, then a test question. Your phone buzzes. Answer the question on your phone.
Read the result to see which surface answered. A returned test answer proves the question-and-answer path; native hook enforcement is checked in the next step. A skipped or unanswered test does not prove phone approvals work.
### Try a real approval [#try-a-real-approval]
Restart your agent so it loads the new MCP tools, instructions and hooks. Follow its [setup guide](/docs/agents/supported-agents), including hook trust where required.
Through that agent, try a harmless action that your policy requires approval for. For Claude Code, follow its [real approval check](/docs/agents/guides/claude-code#verify-a-real-approval) with a new scratch file. For Codex, its [setup guide](/docs/agents/guides/codex#check-that-it-works) uses `curl -I https://example.com`. Check that the action waits for your answer, then resumes when you approve. If it runs without asking, check the integration and policy before reporting enforcement as verified. An MCP-only connection can ask questions but does not intercept every tool call.
If you want to test phone delivery while at your desk, select **Always** in the app's **Questions & approvals** settings, then restore your preferred mode after testing. Your agent should leave this setting to you. Confirm the phone receives the request and the answer returns to the same task.
New accounts start in the **Always** approval mode. In this mode, approvals go to your phone wherever you are, and the agent waits for your answer. If you do not answer within 60 seconds, the agent asks in its own prompt instead. To keep your phone quiet while you are at the agent's computer, open the Pushary app, tap **Questions & approvals** at the top of your inbox, and pick **When away**. See [Policies](/docs/agents/policies) for all four modes.
## Check that everything works [#check-that-everything-works]
For Mac app setup, use its connection tests and send a harmless question through your current agent. The CLI cannot verify a roundtrip when the app holds the key and the CLI has no credential.
For CLI-owned setup with Node/npm available, run:
```bash
npx pushary@latest doctor
```
It checks your key and plan, how each agent is wired, and the connection to Pushary. At an interactive terminal it also sends a test notification; piped runs skip that push by default. Add `--roundtrip` to explicitly send a test notification and question and wait for your answer. This synthetic test does not run a tool through your agent's hooks, and an answer from your Mac or browser does not prove phone delivery. Check the intended surface and the real approval step above before calling setup verified.
## If something goes wrong [#if-something-goes-wrong]
* **No phone app, or a phone that cannot scan?** Press Ctrl-C, then run `npx pushary@latest setup --connect browser`. You sign in through a browser tab, and you can get push notifications in your phone's browser instead of the app.
* **"Your Pushary workspace has no active subscription"**: activate a plan in the app or at [pushary.com](https://pushary.com), then run setup again.
* **The QR code stopped working**: it lasts 15 minutes. Run setup again for a new one.
* **Setup already ran on this computer and you want to add the app now**: run `npx pushary@latest setup --connect app`. A plain `setup` reuses your saved key and does not show a QR code.
More fixes are in [Troubleshooting](/docs/agents/reference/troubleshooting).
## Setup options [#setup-options]
| Option | What it does |
| ---------------------------- | ----------------------------------------------------------------------------------------------- |
| `--agents claude_code,codex` | Connect these agents without asking. `auto` picks every agent found; `none` saves the key only. |
| `--yes` | Accept the defaults without prompts. |
| `--key pk_xxx.xxx` | Use a key you already have. Skips the QR code and sign-in. |
| `--key-stdin` | Read the key from standard input, so it never appears in your shell history or CI logs. |
| `--connect app` | Pair the phone app, even if this computer already has a key. This creates a new key. |
| `--connect browser` | Sign in through a browser tab instead of the phone app. |
| `--skip-phone` | Skip the phone step, for CI or scripts. |
The [CLI reference](/docs/agents/reference/cli) lists every command and flag.
## Next steps [#next-steps]
---
# Overview
Source: https://pushary.com/docs/agents
Last updated: 2026-10-04
> Push notifications, permission approvals and human in the loop for AI agents. Approve risky actions and answer your agent's questions from your phone.
Pushary connects your AI agents to your phone. When Claude Code, Codex, Cursor or another agent finishes, fails, or needs a yes before a risky step, your phone buzzes. You tap Approve or Deny, or type an answer, and the agent keeps going. You do not have to sit and watch the terminal.
Run this on the computer where your agent runs:
```bash
npx pushary@latest setup
```
It finds your agents, connects them, saves your API key and pairs your phone.
## What you can do [#what-you-can-do]
* **Walk away while an agent works.** Your phone buzzes when the task is done or something breaks.
* **Approve risky actions before they run.** The agent wants to run `rm -rf ./dist`? You see the exact command and tap Approve or Deny.
* **Answer questions from anywhere.** Pick an option or type a reply on your phone.
* **Set rules once.** [Policies](/docs/agents/policies) decide which actions run on their own and which ask you.
* **Stop a runaway agent.** The [kill switch](/docs/agents/kill-switch) denies every action that session tries for the next hour.
* **See what happened.** The [audit log](/docs/agents/audit-log) keeps every question, answer and decision.
## How it works [#how-it-works]
```text
Your agent -> Pushary -> Your phone (or your Mac)
```
Pushary works in two ways. Setup turns on both where your agent supports them.
* **Tools.** Pushary runs a hosted [MCP server](https://modelcontextprotocol.io). MCP is a standard way for an agent to call outside tools. Your agent gets tools to notify you and ask you questions. Any MCP client can use them, and the agent decides when to call them.
* **Hooks.** Some agents, such as Claude Code, Codex, Gemini CLI and Cursor, let another program check an action before it runs. Pushary uses these hooks to hold a risky action until you answer, so the agent cannot run it first. If Pushary cannot be reached, the agent's own prompt decides instead.
Read [how it works in detail](/docs/agents/how-it-works), or see [which agents support hooks](/docs/agents/supported-agents).
## Set up your agent [#set-up-your-agent]
See every agent and what Pushary can enforce for it on [Supported agents](/docs/agents/supported-agents).
## Build approvals into your product [#build-approvals-into-your-product]
Your own agent can ask your users before it sends a refund, an email or a delete. They tap Approve or Deny on their phone.
---
# Framework adapters
Source: https://pushary.com/docs/agents/adapters
Last updated: 2026-10-04
> Human in the loop adapters for the Vercel AI SDK, LangGraph, OpenAI Agents SDK, Mastra, Claude Agent SDK, CrewAI and more. Phone approvals before a tool runs.
Pushary has a human in the loop adapter for each major agent framework. Each one hooks the framework's own approval step, so your user approves or denies a tool call on their phone before it runs. If nobody answers, the tool does not run. Every adapter is a thin layer over the same core SDK and runs on the [Partner plan](/docs/pricing-and-limits).
New to this? Start with the [builder quickstart](/docs/agents/build/quickstart). It covers the two calls every adapter makes.
## Choose your adapter [#choose-your-adapter]
| Framework | Install | Hook it uses |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------- |
| [Vercel AI SDK](/docs/agents/build/vercel-ai-sdk) | `npm i @pushary/ai-sdk ai zod` | `toolApproval: pusharyApproval()` |
| [LangGraph](/docs/agents/build/langgraph) (TypeScript) | `npm i @pushary/langgraph @langchain/langgraph @langchain/core zod` | Approval node with `pusharyInterrupt` |
| [LangGraph](/docs/agents/build/langgraph) (Python) | `pip install pushary-langgraph` | Approval node with `pushary_interrupt` |
| [OpenAI Agents SDK](/docs/agents/build/openai-agents) (TypeScript) | `npm i @pushary/openai-agents @openai/agents zod` | `needsApproval` + `resolvePusharyInterruptions` |
| [OpenAI Agents SDK](/docs/agents/build/openai-agents) (Python) | `pip install pushary-openai-agents` | `needs_approval` + `resolve_pushary_interruptions` |
| [Mastra](/docs/agents/build/mastra) | `npm i @pushary/mastra @mastra/core zod` | `requireApproval` + `resolvePusharyApprovals` |
| [Claude Agent SDK](/docs/agents/build/claude-agent-sdk) | `npm i @pushary/claude-agent-sdk @anthropic-ai/claude-agent-sdk` | `canUseTool: pusharyCanUseTool()` |
| [Eve](#eve) | `npm i @pushary/eve` | `approval: pusharyApproval()` or `pusharyChannel()` |
| [Pydantic AI](#pydantic-ai) | `pip install pushary-pydantic-ai` | `requires_approval=True` + `PusharyApprovals` |
| [CrewAI](#crewai) | `pip install pushary-crewai` | Ask tool `make_ask_human_tool` |
| [Inngest, Temporal, Vercel Workflow](#durable-workflows-inngest-temporal-vercel-workflow) | `npm i @pushary/durable` | `createApproval` + `resolveApproval` |
| [Hermes](#hermes) and [OpenClaw](#openclaw) | `pip install pushary` or `npm i @pushary/server` | A plugin tool or a `before_tool_call` hook |
| [Any other framework](#any-framework-the-core-sdk) | `npm i @pushary/server` or `pip install pushary` | `decisions.ask()` |
| [Your own adapter](#build-your-own-adapter) | `npm i @pushary/server` | `@pushary/server/adapters` |
| [Any MCP client](#no-code-mcp) | Nothing | `create_decision` MCP tool |
## Ask or gate: pick the right one [#ask-or-gate-pick-the-right-one]
There are two ways to involve a person.
**Ask** gives the model a tool it can call. It suits "check with someone before you guess". The model decides whether to call it, and a model in a hurry can skip it.
**Gate** hooks the framework's own approval step. The runtime asks the person *before* the tool function runs, and the model never gets a say. Use it for anything that spends money, deletes data, or leaves your system.
| | Ask | Gate |
| ------------------------------- | ---------------------------- | ----------------------- |
| Who decides to involve a person | The model | Your code |
| Can the model skip it | Yes | No |
| Right for | "Is this the right address?" | "Refund $4,000" |
| What you write | Add a tool | Add one field to a tool |
Most products want both: a general `ask_human` tool, plus a gate on the two or three tools that can do real harm.
## What the adapters handle for you [#what-the-adapters-handle-for-you]
If you wire a person into an agent yourself, this is the list you end up owning:
| The part | What Pushary does |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| Reaching the person | Native iOS and Android push, PWA push and Slack, from one call |
| Them answering | Confirm, choice or text in the native app. Confirm notifications offer quick actions. The web decision page stays available. |
| Waiting | Pushary keeps the decision. Your framework and application keep the run and resume it by polling or from a signed callback. |
| Asking twice | Reuse an idempotency key for the same saved action. Your application must still stop duplicate business execution. |
| Nobody answering | Fails closed: silence is a no, never a yes |
| Proving it later | Every ask and answer in the [audit log](/docs/agents/audit-log) |
## The two calls under every adapter [#the-two-calls-under-every-adapter]
Whatever framework you use, it comes down to two calls:
1. **Connect a phone once.** `enroll(externalId)` returns a link. Show it to your end user. One tap opens the Pushary app, which puts Approve and Deny on their lock screen. It costs them nothing and needs no account. Show the link as a QR code they can scan again: iOS does not reopen a universal link after an App Store install, so a first-time user finishes by scanning it from inside the app.
2. **Ask a person.** `ask({ question, externalId })` creates a decision, delivers it and waits up to 55 seconds for the answer. You get back a fail-closed `approved` flag.
`externalId` is your own stable id for the end user (a user id, an email hash, a tenant key). Use the same value for `enroll` and every `ask` for that person.
## Any framework: the core SDK [#any-framework-the-core-sdk]
If your framework can call a function, the core SDK works. The Python SDK has the same two calls with snake\_case arguments and dict returns.
```bash
npm i @pushary/server
```
```ts
import { createPusharyServer } from '@pushary/server'
const pushary = createPusharyServer({ apiKey: process.env.PUSHARY_API_KEY! })
// Once per end user: show the link, they tap to connect their phone.
const { universalLink } = await pushary.enroll(user.id)
// Whenever your agent needs a person:
const { approved } = await pushary.decisions.ask({
externalId: user.id,
question: 'Issue a $50 refund?',
type: 'confirm', // confirm | select | input
})
if (approved) await issueRefund()
```
```bash
pip install pushary
```
```python
import os
from pushary import PusharyServer
px = PusharyServer(api_key=os.environ["PUSHARY_API_KEY"])
# Once per end user: show the link, they tap to connect their phone.
link = px.enroll("user_123")["universalLink"]
# Whenever your agent needs a person:
d = px.decisions.ask(
question="Issue a $50 refund?",
external_id="user_123",
type="confirm", # confirm | select | input
)
if d["approved"]:
issue_refund()
```
## Vercel AI SDK [#vercel-ai-sdk]
Full walkthrough: [Vercel AI SDK guide](/docs/agents/build/vercel-ai-sdk).
`pusharyApproval()` goes in the AI SDK's `toolApproval`, so the SDK asks before the tool runs. `createPusharyTools` adds an optional `askHuman` question tool. A prompt that asks the model to seek permission does not protect another tool. Set `policy: false` when a person must answer. Your server binds `externalId` to the signed-in customer, then shows their [enrollment link](/docs/agents/embed#2-connect-the-person-who-will-approve).
`@pushary/ai-sdk` 0.3 requires `@pushary/server` 2.1. For late answers, the [delayed-review recipe](https://github.com/Pushary/pushary-ai-sdk/blob/main/examples/DELAYED-REVIEWS.md) saves the AI SDK messages and application state in SQLite. It keeps generated output and refuses to re-run an uncertain call. It needs AI SDK 7 and Node.js 22.13 or later. Your application owns persistence and recovery.
## LangGraph and LangChain [#langgraph-and-langchain]
Full walkthrough: [LangGraph guide](/docs/agents/build/langgraph).
An approval node calls `pusharyInterrupt` (Python: `pushary_interrupt`), and the graph routes to the risky node only on a confirm answer of `'yes'`. Select and text answers do not grant permission. `createAskHumanTool` is an optional question tool for prebuilt LangChain agents.
A blocking question suits a short wait. For delayed answers, use a persistent LangGraph checkpointer, save the decision-to-thread mapping, and resume only after you read and validate the decision. `@pushary/langgraph` 0.4 and `pushary-langgraph` 0.4 require the shared SDK 2.1 and bind each review to its recipient and action. Follow the [native interrupt guide](https://github.com/Pushary/pushary-langgraph#durable-native-interrupt) and its [restart test](https://github.com/Pushary/pushary-langgraph/blob/main/src/interrupt.test.ts). Do not mix examples from different package versions. Your worker still handles callbacks, duplicate resumes, output storage and uncertain business effects.
## OpenAI Agents SDK [#openai-agents-sdk]
Full walkthrough: [OpenAI Agents SDK guide](/docs/agents/build/openai-agents). For OpenAI-hosted sessions, see the [hosted Agents API recipe](/docs/agents/build/hosted-openai-agents).
`pusharyTool` (Python: `pushary_tool`) lets the model ask a question. Native `needsApproval` (Python: `needs_approval`) protects a tool before it runs, and `resolvePusharyInterruptions` answers the interruptions. Your server picks the customer, shows their enrollment link, and resolves only the interruptions of that customer's saved run. Set `policy: false` when a person must answer.
`@pushary/openai-agents` 0.4 and `pushary-openai-agents` 0.4 require the shared SDK 2.1. The [TypeScript delayed-answer recipe](https://github.com/Pushary/pushary-openai-agents/blob/main/examples/DELAYED-REVIEWS.md) saves native `RunState`, fixed call bindings, claims and generated output in SQLite. It needs Node.js 22.13 or later and handles answers after a process restart without re-running uncertain calls. The Python resolver blocks inside one request, so Python callers build durable scheduling themselves.
## Mastra [#mastra]
Full walkthrough: [Mastra guide](/docs/agents/build/mastra).
Set Mastra's `requireApproval: true` on the tool, then call `resolvePusharyApprovals` for the trusted suspended runs of this customer and action. Set `policy: false` when a person must answer. Save every returned generation and check pending and error results. A model-callable ask tool protects nothing else. Confirm yes or no is permission to execute. A choice or typed answer is data, even the word `yes`.
Before you open a review for a product customer, connect them with their [enrollment link](/docs/agents/embed#2-connect-the-person-who-will-approve), wait for consent, and check reachability. An owner demo on your own phone is separate from connecting a customer.
For delayed answers, `openPusharyAgentReview` saves the exact call and customer binding, and `resumePusharyAgentReview` reads the authoritative decision before it resumes the saved run. `@pushary/mastra` 0.4 requires `@pushary/server` 2.1, `@mastra/core` 1.59 or later below 2, and Node.js 22.13 or later. The [restart and live-phone recipe](https://github.com/Pushary/pushary-mastra/blob/main/examples/durable-agent.mjs) needs no model key and moves no money. Simulated HTTP checks alone do not prove phone delivery.
## Claude Agent SDK [#claude-agent-sdk]
Full walkthrough: [Claude Agent SDK guide](/docs/agents/build/claude-agent-sdk).
`@pushary/claude-agent-sdk` is a `canUseTool`. The SDK calls it before a tool that needs permission runs, and it returns your user's answer from their phone.
```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)
}
```
For your own agents, the SDK's MCP support works with no install. One `mcpServers` entry puts the hosted server's tools in front of the model:
```ts
import { query } from '@anthropic-ai/claude-agent-sdk'
for await (const message of query({
prompt: 'Clean up stale feature flags. Ask before deleting anything.',
options: {
mcpServers: {
pushary: {
type: 'http',
url: 'https://pushary.com/api/mcp/mcp',
headers: { Authorization: `Bearer ${process.env.PUSHARY_API_KEY}` },
},
},
allowedTools: ['mcp__pushary__ask_user', 'mcp__pushary__wait_for_answer', 'mcp__pushary__send_notification'],
},
})) {
if (message.type === 'result') console.log(message.subtype)
}
```
These MCP tools reach the phones connected to your workspace. When the agent serves your own end users, use `canUseTool` with each user's `externalId`.
## Eve [#eve]
```bash
npm i @pushary/eve
```
`@pushary/eve` needs `eve@0.31` or newer and Node.js 24 or later.
**Ask.** Eve finds tools by file. Add two one-line files:
```ts
// agent/tools/ask-human.ts
import { pusharyAskHuman } from '@pushary/eve'
export default pusharyAskHuman()
```
```ts
// agent/tools/connect-phone.ts
import { pusharyConnectPhone } from '@pushary/eve'
export default pusharyConnectPhone()
```
By default each tool asks the **session principal**, so run user-scoped auth and each end user is their own principal. To bind a fixed end user, pass one: `pusharyAskHuman({ externalId: user.id })`.
**Gate.** Eve evaluates a per-tool `approval` field before `execute`. Pass `pusharyApproval()` and the tool cannot run without a yes:
```ts
// agent/tools/issue-refund.ts
import { defineTool } from 'eve/tools'
import { z } from 'zod'
import { pusharyApproval } from '@pushary/eve'
export default defineTool({
description: 'Refund an order',
inputSchema: z.object({ amount: z.number() }),
approval: pusharyApproval({ externalId: (ctx) => ctx.session.auth.current?.principalId }),
execute: async ({ amount }) => refund(amount),
})
```
The person who answers is the signed-in user of the session. Never take it from `ctx.toolInput`: the model writes the tool input, so a prompt injection could send the approval to a different customer. It sits next to Eve's built-in `always()`, `never()` and `once()`. Those decide on their own. This one asks a person and waits.
**Channel.** `pusharyChannel()` is the option to use in a real product. It puts everything Eve already pauses on onto a phone: any tool gated with `approval`, plus the built-in `ask_question`. Eve parks the turn durably while it waits, so an approval can sit for hours with no idle compute.
```ts
// agent/channels/pushary.ts
import { pusharyChannel } from '@pushary/eve'
export default pusharyChannel()
```
```bash
PUSHARY_API_KEY=pk_xxx.xxx
PUSHARY_WEBHOOK_SECRET=whsec_... # decisions.getWebhookSecret()
PUSHARY_CALLBACK_ORIGIN=https://your-agent.vercel.app
```
The channel also exposes `POST /pushary/message`, `/pushary/stop` and `/pushary/reset`, so the rest of the session is reachable from the phone too.
Eve is in public preview and its channel API has already changed once, so pin your `eve` version.
## Pydantic AI [#pydantic-ai]
`pushary-pydantic-ai` adds `PusharyApprovals`, a Pydantic AI capability. When the model calls a tool marked `requires_approval=True`, it asks your user on their phone and waits. Approve runs the tool in the same run. Deny, or no answer in time, returns a denial the model can read, and the tool does not run. The decision page shows every argument. A call too long to show in full is denied without asking anyone.
```bash
pip install pushary-pydantic-ai
```
```python
from pydantic_ai import Agent
from pushary_pydantic_ai import PusharyApprovals
agent = Agent(
'openai:gpt-4.1-mini',
capabilities=[PusharyApprovals(external_id=customer.id)],
)
@agent.tool_plain(requires_approval=True)
def issue_refund(order_id: str) -> str:
return refund_order_once(order_id)
```
For a multi-tenant agent, pass a function that reads the person from trusted deps, such as `external_id=lambda ctx: ctx.deps.user_id`, never from the tool input. The run waits `timeout_seconds` (55 by default) and the question expires with it. Set `policy=False` when a person must always answer.
Use `run`, `iter` or `run_stream_events`. `run_stream` ends before deferred calls are handled. Under Temporal, DBOS or Prefect the wait runs as a durable operation. To wait minutes or hours without holding a worker, let the run pause with `DeferredToolRequests` and resume it with `create_reviews` and `resolve_reviews`. The [package README](https://github.com/Pushary/pushary-pydantic-ai) covers that flow, the `ask_human` question tool and the storage it needs.
## CrewAI [#crewai]
Install `pushary-crewai` and give the agent a ready-made tool instead of the console `human_input=True` prompt.
```bash
pip install pushary-crewai
```
```python
from crewai import Agent
from pushary_crewai import connect, make_ask_human_tool
link = connect("user_123") # show this link to the user once
agent = Agent(role="Ops", goal="Ship safely", tools=[make_ask_human_tool("user_123")])
```
The `external_id` is bound to the tool, so a prompt-injected agent cannot ask the wrong person. The tool blocks until they answer and fails closed.
## Hermes [#hermes]
Run `pip install pushary` in Hermes' environment, then register a tool in a plugin:
```python
import os
from pushary import PusharyServer
px = PusharyServer(api_key=os.environ["PUSHARY_API_KEY"])
APPROVER_EXTERNAL_ID = "user_123"
def ask_human(params, **kwargs):
d = px.decisions.ask(
question=params["question"],
external_id=APPROVER_EXTERNAL_ID,
type="confirm",
)
return "approved" if d["approved"] else f"not approved ({d['status']})"
def register(ctx):
ctx.register_tool(
name="ask_human",
toolset="approvals",
schema={"type": "object", "properties": {
"question": {"type": "string"},
}, "required": ["question"]},
handler=ask_human,
description="Ask a person to approve before acting.",
)
```
The approver comes from your code, not from the tool arguments, so a prompt-injected agent cannot send the question to the wrong person. In gateway mode, look up each user's `external_id` in your own user model for the session that is running. Never let the model fill it in.
## OpenClaw [#openclaw]
Gate risky tool calls with a `before_tool_call` hook:
```ts
import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry'
import { createPusharyServer } from '@pushary/server'
const pushary = createPusharyServer({ apiKey: process.env.PUSHARY_API_KEY! })
export default definePluginEntry({
id: 'pushary-approvals',
name: 'Pushary approvals',
register(api) {
api.on('before_tool_call', async (event) => {
if (event.toolName !== 'deploy_service') return
const d = await pushary.decisions.ask({
question: `Allow ${event.toolName}?`,
externalId: 'user_123',
type: 'confirm',
})
if (!d.approved) return { block: true, blockReason: 'A human declined this action.' }
})
},
})
```
## Durable workflows (Inngest, Temporal, Vercel Workflow) [#durable-workflows-inngest-temporal-vercel-workflow]
For waits of minutes or hours that must not hold idle compute, install `@pushary/durable`. It has no framework dependency of its own, so it fits whichever runner you use.
```bash
npm i @pushary/durable
```
```ts
import { createApproval, resolveApproval, deterministicKey } from '@pushary/durable'
// Inside a step: open the decision and park the run.
const { correlationId } = await createApproval(
{ apiKey: process.env.PUSHARY_API_KEY! },
{
externalId: user.id,
question: 'Approve this transfer?',
callbackUrl: 'https://yourapp.com/webhooks/pushary',
idempotencyKey: deterministicKey([runId, 'approve-transfer']),
},
)
// Store correlationId -> runId in your own state, then suspend or wait.
// In your callback route: verify and parse in one call.
const approval = resolveApproval(rawBody, signature, process.env.PUSHARY_WEBHOOK_SECRET!)
if (approval?.approved) resumeRun(approval.correlationId)
```
`idempotencyKey` is required and must stay the same across retries and replays. A durable step runs again on resume, so a per-attempt key would ask your user twice.
`resolveApproval` checks the HMAC signature first, so a spoofed request never resumes your workflow. It then returns `{ correlationId, answer, value, approved, context?, answeredAt }`. Look up your run by `correlationId`, or pass `context: runId` at create time and read it off the callback. The [package README](https://github.com/Pushary/pushary-durable) has copy-paste recipes for Inngest, Temporal and Vercel Workflow.
## Build your own adapter [#build-your-own-adapter]
Every adapter above is a thin binding over one shared kernel, and that kernel is public. If you run an in-house harness, or a framework without an adapter, build on the same surface:
```ts
import { createAdapterKernel, renderApprovalQuestion } from '@pushary/server/adapters'
const kernel = createAdapterKernel('the Acme helpers')
// Blocking ask, with idempotency and fail-closed handling built in.
export const askHuman = kernel.askExternalUser
// Durable create, for a framework that parks its own run.
export const openDecision = kernel.createDurableDecision
// An enforced gate: build it once, call it per tool call.
const gate = kernel.createGate({ apiKey: process.env.PUSHARY_API_KEY! })
export const approve = async (toolName: string, callId: string, input: unknown) => {
const decision = await gate({
toolName,
callId,
sessionId: currentRunId,
question: renderApprovalQuestion(toolName, input),
externalId: kernel.requireExternalId(currentUserId),
// Optional: lets a rule decide on the arguments, not only the action name.
input,
})
return decision // { approved: true, authorization? } | { approved: false, reason }
}
```
The gate asks your policy before it asks a person. A rule that names the action settles it with nobody paged: `allow` returns approved and opens no decision, and `deny` returns a reason the model reads. Anything no rule names still asks. A site with no rules behaves as before, because a rule has to name the action and `*` is never selected. Pass `policy: false` to get the always-ask gate.
The label you pass to `createAdapterKernel` appears in the error when a key or an end user is missing, so it points at your helpers.
Also on the subpath:
* `describeAnswer` turns an outcome into an instruction the model cannot misread.
* `resolvePusharyCallback` verifies and parses a webhook in one call. `SIGNATURE_HEADER` names the header to read.
* `idempotencyKeyFor` keeps an explicit `idempotencyKey`, or makes a fresh key for an independent ask.
* `isApproved` is the fail-closed check on its own.
Python has the same surface as `pushary.adapters`:
```python
from pushary.adapters import AdapterKernel, render_approval_question
kernel = AdapterKernel("the Acme helpers")
ask_human = kernel.ask_human
gate = kernel.create_gate()
```
`create_gate(policy=False)` is the same always-ask option. Neither language evaluates a rule locally. The verdict comes from the server, so the two cannot disagree about what a policy means.
Both give you the guarantees the shipped adapters have: explicit operation keys deduplicate retries, independent blocking calls get fresh identities, durable creates require a key, and unanswered decisions count as a no.
### Protect one action with protect() [#protect-one-action-with-protect]
`createGate` answers yes or no. `protect()` does that and then runs the action, so authorize, escalate and execute are one call in your business code.
`protect()` uses the authorization and execution-permit endpoints. Configure it with a separately authorized server credential. The onboarding runtime key cannot call those endpoints. Keep that credential away from the model and the customer's browser.
```ts
const protect = kernel.protect({ apiKey: process.env.PUSHARY_API_KEY! })
const outcome = await protect({
action: 'refund.create',
target: 'order_4471',
externalId: customer.id,
facts: { amount: 4800, currency: 'EUR' }, // what a rule may decide on
callId: attemptId,
runId: sessionId,
run: () => stripe.refunds.create({ charge, amount: 4800 }),
})
outcome.ok ? outcome.result : outcome.reason
```
A rule that names `refund.create` settles it with nobody paged. Anything no rule names asks a person. `run` is called only after the action is authorized. An error it throws is not caught, because reporting it as `ok: false` would look the same as a refusal.
`protect()` runs the action at most once. The decision is idempotent: a replay of the same `runId`, `callId` and action lands on the same approval instead of asking twice. The approval is then spent against a durable permit before `run` is called. A retry, a concurrent worker and a resumed run all reach the same permit, and exactly one of them proceeds. The rest come back `ok: false` with a reason worded to stop the model, not to invite another try.
The permit is bound to the exact subject that was authorized. Change the amount between approval and execution and you have no permit for the new action. `protect()` then records `succeeded` or `failed` against the permit. A process that dies mid-action leaves the permit unresolved, which you can see and reconcile, instead of an action that ran twice.
See [Run it once, and prove it ran](/docs/agents/embed#run-it-once-and-prove-it-ran) for the two calls underneath and the refusals you can branch on.
In Python, build it once with `protect = kernel.create_protect()`, then call `protect(action, run, external_id=..., call_id=..., run_id=...)`. Building it once makes a missing key fail where the protector is defined, not on the first action.
A runnable end-to-end version, against the sandbox and with no phone, is in `examples/protected-action.ts` in `@pushary/server`.
## No code: MCP [#no-code-mcp]
If your agent speaks the Model Context Protocol (MCP), point it at the Pushary MCP server with your API key and skip the SDK. See [Connect any agent](/docs/agents/connect-any-agent).
On the Partner plan, an MCP connection with the right authorization exposes end-user decision tools. Use a recipient-bound credential issued by your trusted backend. The onboarding runtime key's REST access does not grant these MCP tools. An MCP agent can then use:
* `enroll_end_user({ externalId })` makes the one-tap connect link for one of your users. The user needs no account or key.
* `create_decision({ question, externalId, ... })` writes a durable decision and delivers it to that user's phone.
* `get_decision({ decisionId, waitSeconds })` is the durable wait. Loop it until the status leaves `pending`. `waitSeconds` caps at 20.
* `cancel_decision({ decisionId })` withdraws a stale request.
The same fail-closed rule applies: only an answered, approving decision is a yes. Whoever holds the API key controls which `externalId` a decision targets, so treat the key like the production credential it is. See the [MCP reference](/docs/agents/reference/tools#partner-decisions).
## Which one to use [#which-one-to-use]
| You need | Use |
| --------------------------------------------------------- | ---------------------------------------------------- |
| The model decides when to ask | The **ask** tool |
| This must not happen without a yes | The **gate** |
| One approval in a request that finishes in under a minute | `ask()` |
| Long waits or crash-safe resume | `create()` with `callbackUrl` under a durable runner |
| No code | MCP |
Every path writes to the same decision store, so an answer survives a restart of your process. Every decision shows up in the same [audit log](/docs/agents/audit-log).
## Next steps [#next-steps]
---
# Get your API key
Source: https://pushary.com/docs/agents/api-key
Last updated: 2026-10-04
> Find your Pushary API key, check what a valid key looks like, and replace or revoke one. The setup command creates and saves a key for you.
Your Pushary API key connects your AI agents to your Pushary account. You rarely need to copy it. The setup command creates a key when you pair your phone or sign in, and saves it on your computer. You only handle a key yourself when you connect a tool by hand, such as an MCP client, a REST call or a CI job.
```bash
npx pushary@latest setup
```
On Windows PowerShell, run `npx.cmd pushary@latest setup`.
## Copy a key from the dashboard [#copy-a-key-from-the-dashboard]
Open **Settings > API keys** at [pushary.com/dashboard/agent/settings](https://pushary.com/dashboard/agent/settings).
Click **Create Key** and give it a name you will recognise later, such as "Claude Code laptop".
Copy the key and store it somewhere safe right away. The full key is shown only this once.
Keys made during web onboarding are named "MCP API Key" in the list.
## What a key looks like [#what-a-key-looks-like]
```text
pk_1a2b3c4d5e6f7a8b9c0d1e2f.3a4b5c6d7e8f...
```
A key is `pk_` plus 24 hex characters, a dot, then a 48 hex character secret. There is no `sk_` part. Examples in these docs write it as `pk_xxx.xxx`.
Treat the whole key as a password. Anyone who has it can send you notifications and ask you questions as your agent. A key created in the dashboard can also change your approval mode and push window, and accept suggested auto-approve rules. Do not commit a key to git or paste it anywhere public.
Pushary stores only a hash of the secret, so nobody can show you a key again after it is created. If you lose a key, create a new one and revoke the old one.
## Use your key [#use-your-key]
Pass a key to the CLI instead of pairing:
```bash
npx pushary@latest setup --key pk_xxx.xxx
```
That puts the key in your process list. In CI, use `--key-stdin` so the key never appears in argv or shell history:
```bash
echo "$PUSHARY_API_KEY" | npx pushary@latest setup --key-stdin --yes
```
The CLI saves the key to `~/.pushary/config.json`. If `PUSHARY_API_KEY` is also set in your shell, the environment variable wins, and the CLI warns you when the two differ.
For MCP and REST connections, send the key as a Bearer token:
```text
Authorization: Bearer pk_xxx.xxx
```
## Replace a key [#replace-a-key]
Keys cannot be rotated in place. To replace one:
Create a new key in **Settings > API keys**.
Update each agent that uses the old key. Run `npx pushary@latest setup --key pk_xxx.xxx` with the new key, or paste it into your MCP config.
Revoke the old key in **Settings > API keys**. It stops working right away, and revoking cannot be undone.
To revoke the key a machine uses from that machine, run:
```bash
npx pushary@latest sign-out --revoke
```
This revokes only a key the CLI created when you signed in with `login`, `setup` or the pairing QR. A key you passed with `--key`, or one the Mac app uses, is removed from the machine but stays valid, so revoke it in **Settings > API keys**.
You can keep several keys active at once. With one key per machine or per agent, you can cut off a single device without touching the rest.
## Next step [#next-step]
---
# Audit trail and export
Source: https://pushary.com/docs/agents/audit-log
Last updated: 2026-10-05
> Pushary keeps an append-only audit trail of every agent question, approval and notification. Filter it by agent, session or decision and export CSV or JSON.
The audit trail is Pushary's record of what your AI agents asked and what you let them do. Every question, approval, denial and notification is kept as a row that nobody can rewrite later, not even Pushary. Use it to answer "who approved that `git push`, and when?", to review a run, or to hand an export to a compliance reviewer.
Open it at **Dashboard > Agent > Activity**, on [pushary.com/dashboard/agent/activity](https://pushary.com/dashboard/agent/activity). The **Audit trail** tab shows the rows. The **What changed** tab lists every file edit, command and commit your agents made.
## What each row records [#what-each-row-records]
Each row holds the agent type and name, the session, the tool involved, the question or notification text, your decision, who decided, and timestamps.
| Decision | Meaning |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `approved` | You answered yes |
| `denied` | You answered no |
| `answered` | You answered a choice or text question |
| `defer` | Left to the terminal: the decision went back to your agent's own prompt. You chose to handle it on your machine, it was withdrawn, or Pushary did not send it to your phone |
| `pending` | Asked, not answered yet |
| `notification` | A one-way notification with no decision |
## Filter the audit trail [#filter-the-audit-trail]
The filterable table, the What changed tab and export need the Agent Pro, Team, Partner or Enterprise plan. On other plans the Activity page shows your 100 most recent entries with an upgrade prompt.
You can filter by:
* **Agent type**: any agent Pushary supports, such as `claude_code`, `codex`, `cursor`, `gemini_cli` or `hermes`
* **Session**: one agent run
* **Decision**: `approved`, `denied`, `pending` or `notification`
* **Origin**: who decided. `human`, or a machine origin: `autopilot`, `safe_readonly`, `policy_timeout` or `kill_switch`
* **End user**: `externalId`, for Partner sites that ask their own users
* **Search**: text inside the question or notification
* **Date range**: `from` and `to`
Results come 50 to a page by default, up to 200, with a cursor for the next page.
## Export the audit trail to CSV or JSON [#export-the-audit-trail-to-csv-or-json]
Click **CSV** or **JSON** above the table. The export uses the filters you set. On the Team plan it covers every site in the workspace; on other plans, the current site.
The CSV columns are:
```text
createdAt, site, agentType, agentName, sessionId, type, content, decision,
decisionSource, answeredAt, decidedByUserId, decidedByActor, externalId,
correlationId, decisionLatencyMs, toolName, toolTarget, executionState,
permitId, autoResolved, decisionOrigin
```
`decidedByUserId` is a Pushary user id. `decidedByActor` can be an external actor such as `slack:U0123ABC`. Both are empty when the answer came over a credential that carries no identity.
New columns are only ever added at the end, never inserted or reordered, so a parser written against an earlier export keeps working.
The What changed tab exports the same way, with these columns:
```text
createdAt, site, agentType, sessionId, machineId, toolName, kind, target, ok
```
One export holds up to 10,000 rows, and it cannot reach further back than your plan's [retention](#retention) window.
## Append-only by construction [#append-only-by-construction]
The audit table is protected in the database, not only in the app. A `BEFORE UPDATE OR DELETE` trigger on `agent_interactions` refuses changes, so the guarantee holds even against Pushary's own server code.
* **Edits.** The trigger allows writing the first answer to a question that has none yet. That write cannot change any of 16 pinned columns, including the site, the agent, the session, the tool, the text and the creation time. The only other change it allows is clearing the link to a policy rule after that rule is removed. Any other edit fails with an error.
* **Deletions.** The trigger refuses a delete unless the scheduled [retention](#retention) sweep declared itself for that transaction, or the site the row belongs to is being deleted along with it.
Once an interaction is written, what it says cannot change. Only your retention window and deleting the site remove it.
## Retention [#retention]
The trail is kept for your plan's data retention window, and no longer. A scheduled sweep removes rows once they pass it, and that sweep is the only process allowed to delete an audit row. Deleting a site removes its trail with it.
Retention belongs to your plan, so moving to a plan with a longer window does not bring back rows that have already aged out.
Not one entry, and not from the app. The database trigger refuses the delete, with two exceptions: the scheduled retention sweep, and deleting the whole site. No UI, API or support action removes one row and leaves the rest. If you need data removed for a legal reason, contact support. It is a deliberate operation on the whole trail, not a per-row edit.
[`list_sessions`](/docs/agents/control-panel) is a live view of running agents and open questions. The audit trail is the record of everything that already happened, for as long as your plan keeps it.
When you answer, the pending row is updated once with your response and the time you answered. That first answer is the one write the append-only trigger allows.
## Next steps [#next-steps]
---
# 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]
### 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.
### 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)
}
```
### 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.
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]
---
# Hosted OpenAI Agents API
Source: https://pushary.com/docs/agents/build/hosted-openai-agents
Last updated: 2026-10-04
> Human in the loop for OpenAI's hosted Agents API. Save the required function call, get phone approval, then submit the exact tool result after checks.
The hosted Agents API keeps the session in OpenAI, but your application still runs the function tools. Put the approval in front of that run: save the required action, ask the right customer on their phone, then read the session again before you submit a result. If the customer denies, or nobody answers, the refund does not run.
```bash
git clone https://github.com/Pushary/pushary-openai-agents.git
cd pushary-openai-agents
npm install
npm run test:hosted
```
This page uses the **hosted Agents API**. If you run agents yourself with the OpenAI Agents SDK, use the [OpenAI Agents SDK adapter](/docs/agents/build/openai-agents) instead.
## Run the checked example [#run-the-checked-example]
The [public example source](https://github.com/Pushary/pushary-openai-agents/tree/main/examples) has three files: `hosted-agent-review.mjs`, `delayed-review-store.mjs` and `check-hosted-agent-review.mjs`. The commands above clone it and run its check.
The check uses the installed OpenAI SDK against a local transport simulation and a temporary SQLite file. It makes no refund and no live API call. It verifies:
* reopening the saved review
* pending, approve, deny, expiry and cancellation
* a changed native call and a customer mismatch
* concurrent resume attempts and an ambiguous submission
## Approve a paused hosted session [#approve-a-paused-hosted-session]
You need Node.js 22.18+ within 22.x, or Node.js 24, for `node:sqlite`. You also need `openai` 7.15+, `zod` 4 and `@pushary/server` 2.1+. Keep the review database on persistent storage. Keep the customer and operation ids your application owns out of the model's control.
```js
import { DatabaseSync } from 'node:sqlite'
import OpenAI from 'openai'
import { createReviewStore, openReview } from './delayed-review-store.mjs'
import { saveHostedReview, resumeHostedReview } from './hosted-agent-review.mjs'
const openai = new OpenAI()
const config = { apiKey: process.env.PUSHARY_API_KEY }
const store = createReviewStore(new DatabaseSync('./reviews.sqlite'))
const target = {
operationId: 'refund-order-1-v1',
externalId: 'customer-1',
framework: 'hosted-agents-v1',
codeVersion: 'refund-v1',
}
const session = await openai.beta.agents.sessions.retrieve(trustedSessionId)
saveHostedReview(store, target, session)
await openReview(config, store, target.operationId)
// Run again from your durable worker after the customer answers.
const result = await resumeHostedReview(config, store, target, openai, async (action, operationId) => {
return { simulated: true, operationId, action }
})
```
`trustedSessionId` comes from your application's customer-to-session mapping. Connect `target.externalId` with the [builder quickstart](/docs/agents/build/quickstart) before you open the review.
The recipe accepts one pending `refund` call with `orderId`, an integer `amount` in EUR cents, and `draftVersion`. When you replace the simulation, call an idempotent operation at the real refund service.
## What happens on deny, expiry or no answer [#what-happens-on-deny-expiry-or-no-answer]
Denial, expiry and cancellation submit a failed tool result to the session. The refund never runs.
A changed pending call stops before the refund. Any ambiguous effect or submission leaves the review `uncertain` for you to reconcile by hand. Nothing retries it automatically.
The saved claim stops two local workers resuming at once. It cannot make an external refund and the OpenAI submission one atomic step.
## Recover after a worker crash [#recover-after-a-worker-crash]
A crash during continuation leaves the review `resuming`. Later workers return `busy` and never replay the refund.
1. Stop every worker that could still own that operation.
2. Call `store.recoverInterrupted(target.operationId)` once. It moves only a `resuming` review to `uncertain` and returns whether it changed the record. Repeat calls are safe.
3. Reconcile the refund and the hosted session by hand before you choose a new operation.
Never run this recovery while a worker is active, and never reset the review to `pending`.
## Wake the worker, then retrieve the session [#wake-the-worker-then-retrieve-the-session]
The session stream event is `agent.session.requires_action`. The HTTP webhook is `agent.session.action_required`. Verify the OpenAI webhook signature, then retrieve the session to read `required_actions`. Older `function_call` items in the transcript are not permission to execute.
A Pushary answer callback or a durable poll wakes your worker. The worker still reads the saved decision and checks its customer, question and action binding. The callback alone does not authorize the refund.
OpenAI reference: [function tools](https://developers.openai.com/api/docs/guides/agents-api/tools/functions), [session events](https://developers.openai.com/api/docs/guides/agents-api/sessions/events) and [session webhooks](https://developers.openai.com/api/docs/guides/agents-api/sessions/webhooks).
## Next steps [#next-steps]
---
# LangGraph
Source: https://pushary.com/docs/agents/build/langgraph
Last updated: 2026-10-04
> LangGraph human in the loop with phone approvals. An approval node asks your user to Approve or Deny before the risky node runs, in TypeScript or Python.
In LangGraph, the graph decides what runs next. So the way to add human in the loop to LangGraph is a node: put an approval node in front of the risky node, and draw the edge to it only when the answer is yes. `@pushary/langgraph` (TypeScript) and `pushary-langgraph` (Python) give you that node. It asks your user on their phone and returns their answer.
```bash
npm i @pushary/langgraph @langchain/langgraph @langchain/core zod
export PUSHARY_API_KEY=pk_xxx.xxx
```
```bash
pip install pushary-langgraph
export PUSHARY_API_KEY=pk_xxx.xxx
```
Get the key at [Partner onboarding](https://pushary.com/onboarding/partner). The Python package installs `langgraph` for you.
## Set up LangGraph human in the loop [#set-up-langgraph-human-in-the-loop]
### Connect a user once [#connect-a-user-once]
```ts
import { connect } from '@pushary/langgraph'
const { universalLink } = await connect({ apiKey: process.env.PUSHARY_API_KEY }, user.id)
```
```python
from pushary_langgraph import connect
link = connect(user.id)
```
Show the link to that user. They open it on their phone once. They install the free Pushary app and never sign up or pay.
### Put an approval node before the risky step [#put-an-approval-node-before-the-risky-step]
```ts
import { Annotation, END, START, StateGraph } from '@langchain/langgraph'
import { pusharyInterrupt } from '@pushary/langgraph'
const config = { apiKey: process.env.PUSHARY_API_KEY }
const Refund = Annotation.Root({
orderId: Annotation,
customerId: Annotation,
approved: Annotation,
})
const graph = new StateGraph(Refund)
.addNode('approval', async (state) => {
const answer = await pusharyInterrupt(config, {
externalId: state.customerId,
question: `Refund order ${state.orderId} in full?`,
type: 'confirm',
})
return { approved: answer === 'yes' }
})
.addNode('refund', async (state) => {
await refundOrder(state.orderId)
return {}
})
.addEdge(START, 'approval')
.addConditionalEdges('approval', (state) => (state.approved ? 'refund' : END))
.addEdge('refund', END)
.compile()
await graph.invoke({ orderId: '4471', customerId: user.id, approved: false })
```
```python
from typing import TypedDict
from langgraph.graph import END, START, StateGraph
from pushary_langgraph import pushary_interrupt
class Refund(TypedDict):
order_id: str
customer_id: str
approved: bool
def approval(state: Refund) -> dict:
answer = pushary_interrupt(
f"Refund order {state['order_id']} in full?",
external_id=state["customer_id"],
)
return {"approved": answer == "yes"}
def refund(state: Refund) -> dict:
refund_order(state["order_id"])
return {}
graph = (
StateGraph(Refund)
.add_node("approval", approval)
.add_node("refund", refund)
.add_edge(START, "approval")
.add_conditional_edges("approval", lambda state: "refund" if state["approved"] else END)
.add_edge("refund", END)
.compile()
)
graph.invoke({"order_id": "4471", "customer_id": user.id, "approved": False})
```
`refundOrder` and `refund_order` are your own functions.
### Run the graph and answer on the phone [#run-the-graph-and-answer-on-the-phone]
Your user's phone buzzes with "Refund order 4471 in full?". They tap Approve, the node returns `approved: true`, and the graph takes the edge to `refund`.
The model has no way around this node. The graph's edges decide what runs, not the model.
## What happens when nobody answers [#what-happens-when-nobody-answers]
The refund does not run. The approval node gets one of three answers:
| Answer | What it means | Edge taken |
| ------------------------- | --------------------------------------------------------------------------- | ---------- |
| `'yes'` | Your user tapped Approve | `refund` |
| `'no'` | Your user tapped Deny | `END` |
| `null` (`None` in Python) | Nobody answered within 55 seconds, or the question expired or was cancelled | `END` |
A timeout does not cancel the question on the phone. It stays open for 1 hour by default, so the user can still answer. To act on that late answer, use the durable path below.
## Let the model ask a question [#let-the-model-ask-a-question]
```ts
import { ChatOpenAI } from '@langchain/openai'
import { createReactAgent } from '@langchain/langgraph/prebuilt'
import { createAskHumanTool } from '@pushary/langgraph'
const askHuman = createAskHumanTool({ apiKey: process.env.PUSHARY_API_KEY }, { externalId: user.id })
const agent = createReactAgent({ llm: new ChatOpenAI({ model: 'gpt-4o' }), tools: [askHuman] })
await agent.invoke({
messages: [{ role: 'user', content: 'Find out which order the customer means.' }],
})
```
```python
from pushary_langgraph import ask_human
reply = ask_human("Which order do you need help with?", external_id=user.id, type="input")
order_id = reply["value"]
```
The TypeScript example also needs `npm i @langchain/openai`. The tool is called `ask_human` by default.
A tool is the model's choice to call, so use it for questions like "which order". Use the approval node above for anything that must not happen without a yes.
## Wait longer than 55 seconds [#wait-longer-than-55-seconds]
Pass a `callbackUrl` and an `idempotencyKey` (Python: `callback_url` and `idempotency_key`). The node then pauses the graph with LangGraph's own `interrupt()` instead of waiting, and you resume it when the answer arrives.
```ts
const answer = await pusharyInterrupt(config, {
externalId: state.customerId,
idempotencyKey: state.reviewOperationId,
question: `Refund order ${state.orderId} in full?`,
type: 'confirm',
callbackUrl: process.env.PUSHARY_CALLBACK_URL,
})
```
`reviewOperationId` is a stable id you keep in graph state for this one action. For this path you need:
1. A persistent checkpointer (`MemorySaver` does not survive a restart) and a stable, customer-scoped `thread_id`.
2. A callback route that verifies the raw body with `resolvePusharyCallback(raw, signature, webhookSecret)` before it trusts anything.
3. A saved mapping from customer, thread id and interrupt id to the Pushary `correlationId`.
Then resume the thread by interrupt id with the correlated answer:
```ts
import { Command } from '@langchain/langgraph'
await graph.invoke(new Command({
resume: {
[pendingInterrupt.id]: {
correlationId: verifiedCallback.correlationId,
answer: verifiedCallback.answer,
},
},
}), { configurable: { thread_id: trustedThreadId } })
```
The resume value is checked. A wrong `correlationId`, a confirm answer that is not `yes` or `no`, or a choice that was not offered throws instead of running the node. To release a graph whose question expired, resume with `{ correlationId, status: 'expired', answer: null }` (or `'cancelled'`). Never make up an approval to free a waiting graph.
The interrupt payload carries `decisionId`, `correlationId`, `operationKey`, `type`, `question`, `options` and `externalId`. The [restart test](https://github.com/Pushary/pushary-langgraph/blob/main/src/interrupt.test.ts) runs this path against a real SQLite checkpointer.
## Production notes [#production-notes]
### Retries and idempotency [#retries-and-idempotency]
LangGraph runs the whole node again on resume, so code before the interrupt must be safe to repeat. The adapter fingerprints your `idempotencyKey` with the recipient and the request. The same request reuses one question. A changed recipient, argument or question gets a new one. Use a fresh key for a new action.
### Approve means yes, nothing else [#approve-means-yes-nothing-else]
Only a confirm answer of `'yes'` is permission. A `select` or `input` answer is data, even when its text is `yes`. Route to the risky node only from a confirm.
### Pick the right person [#pick-the-right-person]
Take the user id from your signed-in user, never from the model's input. A prompt injection could otherwise send the approval to someone else.
### Versions [#versions]
`@pushary/langgraph` 0.4 and `pushary-langgraph` 0.4 require the shared SDK 2.1 (`@pushary/server` 2.1 or `pushary` 2.1). A durable resume must send the correlated object above. Let runs paused under 0.3 finish on 0.3.
Source and more examples: [pushary-langgraph on GitHub](https://github.com/Pushary/pushary-langgraph). LangGraph reference: [interrupts in TypeScript](https://docs.langchain.com/oss/javascript/langgraph/interrupts) and [in Python](https://docs.langchain.com/oss/python/langgraph/interrupts).
## Next steps [#next-steps]
---
# Mastra
Source: https://pushary.com/docs/agents/build/mastra
Last updated: 2026-10-04
> Mastra human in the loop with phone approvals. Tools marked requireApproval suspend, your user taps Approve or Deny, and the run resumes with the answer.
Mastra can hold a tool call until someone approves it. Set `requireApproval: true` on the tool and the agent run suspends before the tool executes. `@pushary/mastra` adds human in the loop to Mastra: it finds that suspended run, asks your user on their phone, and approves or declines the call so the run finishes.
```bash
npm i @pushary/mastra @mastra/core zod
export PUSHARY_API_KEY=pk_xxx.xxx
```
Get the key at [Partner onboarding](https://pushary.com/onboarding/partner). It needs Node.js 22.13 or later, like Mastra itself, and `@mastra/core` 1.59 or later below 2.
## Set up Mastra human in the loop [#set-up-mastra-human-in-the-loop]
### Connect a user once [#connect-a-user-once]
```ts
import { connect } from '@pushary/mastra'
const config = { apiKey: process.env.PUSHARY_API_KEY! }
const { universalLink } = await connect(config, user.id)
```
Show the link to that user. They open it on their phone once, install the free Pushary app, and never sign up or pay.
### Mark the tool and resolve the suspended run [#mark-the-tool-and-resolve-the-suspended-run]
```ts
import { Agent } from '@mastra/core/agent'
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'
import { resolvePusharyApprovals } from '@pushary/mastra'
const issueRefund = createTool({
id: 'issue-refund',
description: 'Refund an order',
inputSchema: z.object({ orderId: z.string(), amount: z.number().positive() }),
outputSchema: z.object({ refunded: z.boolean() }),
requireApproval: true,
execute: async ({ orderId, amount }) => refundOrder(orderId, amount),
})
const agent = new Agent({
id: 'refunds',
name: 'Refunds',
instructions: 'Refund orders when the customer asks.',
model: 'openai/gpt-4o',
tools: { issueRefund },
})
const output = await agent.generate('Refund order 1234 in full.', {
memory: { thread: conversation.id, resource: user.id },
})
if (output.finishReason === 'suspended') {
const outcome = await resolvePusharyApprovals(
{ externalId: user.id, policy: false },
{ agent, resourceId: user.id },
)
}
```
`refundOrder` and `conversation` are your own. Suspended runs live in Mastra's storage, so give your Mastra instance a storage adapter.
### Run it and answer on the phone [#run-it-and-answer-on-the-phone]
When the model calls `issueRefund`, Mastra suspends the run instead of executing it. `resolvePusharyApprovals` asks your user, one call at a time. They tap Approve, and the run resumes and the refund runs. Each resumed reply is in `outcome.resolved`.
## What happens when nobody answers [#what-happens-when-nobody-answers]
The refund does not run. Each question waits up to 55 seconds. A deny or no answer in that time declines the call, and Mastra gives the model the reason:
| Outcome | Reason 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.` |
To wait longer than one request, use [deferred reviews](#wait-minutes-or-hours-for-an-answer).
## Let the model ask a question [#let-the-model-ask-a-question]
```ts
import { Agent } from '@mastra/core/agent'
import { createPusharyAskTool } from '@pushary/mastra'
const agent = new Agent({
id: 'support',
name: 'Support',
instructions: 'Ask the customer which order they need help with.',
model: 'openai/gpt-4o',
tools: { askHuman: createPusharyAskTool(config, { externalId: user.id }) },
})
await agent.generate('Find out which order the customer means.')
```
The tool returns `{ approved, value, status }` to the model. The model chooses when to call it, so use it for questions and keep `requireApproval` for actions that need a yes. A choice or a typed answer is data, not permission, even when the text is `yes`.
## Production notes [#production-notes]
### Make sure a person always decides [#make-sure-a-person-always-decides]
`policy: false` means a person always decides. Without it, the shared gate asks your site's policy first, and a policy rule that names the tool can allow or deny it without paging anyone.
### Read every result [#read-every-result]
`resolvePusharyApprovals` returns `{ resolved, pending, allApproved }`:
| Field | What it holds |
| --------------------------- | --------------------------------------------------------------------------------------------------- |
| `resolved[].output` | The resumed generation. Save each one. |
| `resolved[].reviewError` | Routing, question or gate failure. The resume was not tried and the run is still pending. |
| `resolved[].resumeError` | Mastra's resume was tried and failed. |
| `resolved[].discoveryError` | A later suspension could not be found. Retry it without rerunning the action. |
| `pending` | Runs still waiting. The resolver stops at `maxApprovals` (default 10), a data question or an error. |
| `allApproved` | True only with no pending work and no errors. It does not prove your side effect succeeded. |
### Scope it to one customer [#scope-it-to-one-customer]
Pass trusted `runs`, a `threadId` or a `resourceId`. A call with none of them is rejected, so the resolver never sweeps every suspended run in your app.
### Pick the right person [#pick-the-right-person]
Take the user id from your signed-in user, never from the model's tool input. A prompt injection could otherwise send the approval to someone else.
### Wait minutes or hours for an answer [#wait-minutes-or-hours-for-an-answer]
`resolvePusharyApprovals` waits inside one request. For later answers, use deferred reviews:
1. `createPusharyDeferredAskTool` for questions, and `requireApproval: true` for actions.
2. `openPusharyAgentReview` after Mastra suspends. It saves the exact call and customer binding, then asks.
3. `resumePusharyAgentReview` from a signed callback or a polling job. It reads the authoritative decision before it resumes the saved run.
Your application supplies persistent Mastra storage, an atomic review store, output storage and business idempotency. The [public package guide](https://github.com/Pushary/pushary-mastra) covers the store contract. The [restart and live-phone recipe](https://github.com/Pushary/pushary-mastra/blob/main/examples/durable-agent.mjs) needs no model key, moves no money and polls without a public callback server. Its approval, denial, choice and text checks run in separate processes and check duplicate handling.
Pushary records the answer. Your application records the continuation and the business result. If execution becomes uncertain, reconcile the saved run and the business receipt before you retry. A timeout alone never makes a retry safe.
### Versions [#versions]
`@pushary/mastra` 0.4 requires `@pushary/server` 2.1, `@mastra/core` 1.59 or later below 2, and Node.js 22.13 or later.
Source and more examples: [pushary-mastra on GitHub](https://github.com/Pushary/pushary-mastra). Mastra reference: [agent approval](https://mastra.ai/docs/agents/agent-approval).
## Next steps [#next-steps]
---
# OpenAI Agents SDK
Source: https://pushary.com/docs/agents/build/openai-agents
Last updated: 2026-10-04
> OpenAI Agents SDK human in the loop approvals. Answer needsApproval interruptions from your user's phone, in TypeScript or Python. No answer means no.
The OpenAI Agents SDK has human in the loop approvals built in: mark a tool with `needsApproval` and the run stops with `interruptions` before the tool executes. Somebody still has to decide. `@pushary/openai-agents` (TypeScript) and `pushary-openai-agents` (Python) send each interruption to your user's phone, record their answer on the run state, and hand it back so you can continue.
```bash
npm i @pushary/openai-agents @openai/agents zod
export PUSHARY_API_KEY=pk_xxx.xxx
```
```bash
pip install pushary-openai-agents
export PUSHARY_API_KEY=pk_xxx.xxx
```
Get the key at [Partner onboarding](https://pushary.com/onboarding/partner). The TypeScript package needs Node.js 22 or later. The Python package installs `openai-agents` 0.18 or later for you.
## Set up OpenAI Agents SDK human in the loop [#set-up-openai-agents-sdk-human-in-the-loop]
### Connect a user once [#connect-a-user-once]
```ts
import { connect } from '@pushary/openai-agents'
const { universalLink } = await connect({ apiKey: process.env.PUSHARY_API_KEY! }, user.id)
```
```python
from pushary_openai_agents import connect
link = connect(user.id)
```
The user opens the link on their phone once. They install the free Pushary app and never sign up or pay.
### Mark the tool and resolve its interruptions [#mark-the-tool-and-resolve-its-interruptions]
```ts
import { Agent, run, tool } from '@openai/agents'
import { z } from 'zod'
import { pusharyNeedsApproval, resolvePusharyInterruptions } from '@pushary/openai-agents'
const issueRefund = tool({
name: 'issue_refund',
description: 'Refund an order',
parameters: z.object({ orderId: z.string(), amount: z.number() }),
needsApproval: pusharyNeedsApproval(),
execute: async ({ orderId, amount }) => refundOrder(orderId, amount),
})
const agent = new Agent({
name: 'Refunds',
instructions: 'Refund orders when the customer asks.',
tools: [issueRefund],
})
let result = await run(agent, 'Refund order 1234 in full.')
while (result.interruptions?.length) {
const outcome = await resolvePusharyInterruptions(
{ externalId: user.id },
{ interruptions: result.interruptions, state: result.state },
)
result = await run(agent, result.state)
if (!outcome.allApproved) break
}
```
```python
import asyncio
from agents import Agent, Runner, function_tool
from pushary_openai_agents import pushary_needs_approval, resolve_pushary_interruptions
@function_tool(needs_approval=pushary_needs_approval())
def issue_refund(order_id: str, amount: float) -> str:
return refund_order(order_id, amount)
agent = Agent(name="Refunds", instructions="Refund orders when the customer asks.", tools=[issue_refund])
async def main() -> None:
result = await Runner.run(agent, "Refund order 1234 in full.")
while result.interruptions:
outcome = resolve_pushary_interruptions(result, external_id=user.id)
result = await Runner.run(agent, outcome.state)
if not outcome.all_approved:
break
asyncio.run(main())
```
`refundOrder` and `refund_order` are your own functions.
### Run it and answer on the phone [#run-it-and-answer-on-the-phone]
The run stops before `issue_refund` executes. Your user's phone buzzes with the tool name and its arguments. They tap Approve, and the tool runs when you continue the run.
Each interruption becomes one question on the phone, one at a time, in order.
## What happens when nobody answers [#what-happens-when-nobody-answers]
The tool does not run. Each question waits up to 55 seconds. A deny or no answer in time rejects the interruption, and the model gets the reason as the rejection message so it does not keep retrying:
| Outcome | Rejection 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.` |
`allApproved` (`all_approved` in Python) is then false. The loop continues the run once, so the model reads the reason, and then stops.
## Let the model ask a question [#let-the-model-ask-a-question]
```ts
import { Agent, run } from '@openai/agents'
import { pusharyTool } from '@pushary/openai-agents'
const agent = new Agent({
name: 'Support',
instructions: 'Ask the customer which order they need help with.',
tools: [pusharyTool({ apiKey: process.env.PUSHARY_API_KEY! }, { externalId: user.id })],
})
const result = await run(agent, 'Which order needs help?')
```
```python
from agents import Agent, Runner
from pushary_openai_agents import pushary_tool
agent = Agent(
name="Support",
instructions="Ask the customer which order they need help with.",
tools=[pushary_tool(user.id)],
)
result = Runner.run_sync(agent, "Which order needs help?")
```
The model decides when to call this tool, so it suits questions. Keep `needsApproval` for anything that must not happen without a yes.
## Production notes [#production-notes]
### Make sure a person always decides [#make-sure-a-person-always-decides]
The resolver asks your site's policy first. A policy rule that names the tool can allow or deny it without paging anyone. Pass `policy: false` (Python: `policy=False`) when a person must always answer.
### Continue from the saved state [#continue-from-the-saved-state]
In Python, continue with `outcome.state`. It carries the answers. Continuing from `result.to_input_list()` replays the conversation without them, so the model asks for the same tool again and your user gets asked twice.
### Retries and idempotency [#retries-and-idempotency]
Each question is keyed on the tool call id, the recipient and the arguments, so an exact retry reuses it. Changed arguments or a changed recipient ask again. If you replay a run under ids you mint yourself, pass a stable `runId` (Python: `run_id`).
### Pick the right person [#pick-the-right-person]
The user id is bound in your code, never taken from the model, so a tricked model cannot ask the wrong person. For a multi-tenant agent, pass a function as `externalId` that picks the person for each interruption from your own records.
### Answers that come later than 55 seconds [#answers-that-come-later-than-55-seconds]
The resolver waits inside one request. For answers that may take hours, save the run state with the decision id, customer and exact call arguments, then resume only that call after you read the answer. The [TypeScript delayed-answer recipe](https://github.com/Pushary/pushary-openai-agents/blob/main/examples/DELAYED-REVIEWS.md) does this with SQLite and needs Node.js 22.13 or later. In Python you build the same coordination around the saved state. To wake your worker when an answer lands, use a [signed webhook](/docs/agents/embed#webhooks).
### Versions [#versions]
`@pushary/openai-agents` 0.4 and `pushary-openai-agents` 0.4 require the shared SDK 2.1. Finish pending runs on their old version before you upgrade.
Source and more examples: [pushary-openai-agents on GitHub](https://github.com/Pushary/pushary-openai-agents). OpenAI reference: [human in the loop in TypeScript](https://openai.github.io/openai-agents-js/guides/human-in-the-loop/) and [in Python](https://openai.github.io/openai-agents-python/human_in_the_loop/).
## Next steps [#next-steps]
---
# Build approvals into your agent
Source: https://pushary.com/docs/agents/build/quickstart
Last updated: 2026-10-04
> Add human in the loop approvals to the AI agent in your product. Your user gets a push notification, taps Approve or Deny, and your code gets the answer.
Pushary adds human approval to the agent inside your own product. Before a risky step, such as a refund, a customer email or a delete, your backend asks one of your users. They get a push notification with your product's name, tap Approve or Deny, and your code carries on. If nobody answers, the action does not run.
```bash
npm i @pushary/server
```
Want to see it work first? [Try it in your browser](/try). No signup.
## What you need [#what-you-need]
* A Partner plan and its API key. Get both at [Partner onboarding](https://pushary.com/onboarding/partner).
* Your own id for each user, such as `user.id`. Pushary calls it `externalId`.
* Your users install the free Pushary app and open your link once. They never sign up or pay.
## Ask your first question [#ask-your-first-question]
### Install the SDK and set your key [#install-the-sdk-and-set-your-key]
```bash
npm i @pushary/server
export PUSHARY_API_KEY=pk_xxx.xxx
```
```bash
pip install pushary
export PUSHARY_API_KEY=pk_xxx.xxx
```
Keep the key on your server. Never put it in browser code or a prompt.
### Connect a user once [#connect-a-user-once]
```ts
import { createPusharyServer } from '@pushary/server'
const pushary = createPusharyServer({ apiKey: process.env.PUSHARY_API_KEY! })
const { universalLink } = await pushary.enroll(user.id)
// Show universalLink to this user as a button or a QR code.
```
```python
import os
from pushary import PusharyServer
pushary = PusharyServer(api_key=os.environ["PUSHARY_API_KEY"])
enrolled = pushary.enroll(user.id)
# Show enrolled["universalLink"] to this user as a button or a QR code.
```
The user opens the link on their phone. It opens the Pushary app if they have it. If not, the page offers the app or browser notifications. After that, every question for this `externalId` reaches their phone.
The link can be redeemed once per channel (the phone app and browser push) and expires after 24 hours. Save the fact that the user connected, not the link.
### Ask before the risky step [#ask-before-the-risky-step]
```ts
const decision = await pushary.decisions.ask({
externalId: user.id,
question: 'Refund $480 to order #4471?',
})
if (decision.approved) {
await issueRefund()
}
```
```python
decision = pushary.decisions.ask("Refund $480 to order #4471?", external_id=user.id)
if decision["approved"]:
issue_refund()
```
Your user's phone buzzes with the question. When they tap Approve, `approved` is true and the refund runs. `issueRefund` is your own function.
## What happens when nobody answers [#what-happens-when-nobody-answers]
`ask` waits up to 55 seconds for an answer. `approved` is true only when the person tapped Approve. Deny, an expired question and no answer at all give `approved: false`, so the refund does not run.
The question stays open after `ask` returns, for 1 hour by default. To act on a late answer, save `decision.decisionId` and read it again later. The [full Partner guide](/docs/agents/embed#4-resume-only-after-approval) shows how.
## What your user sees [#what-your-user-sees]
1. A notification titled with your product name, such as **Acme needs your approval**.
2. They tap **Approve** or **Deny** from the lock screen.
3. Your code gets the answer and carries on.
## Use your framework's approval hook [#use-your-frameworks-approval-hook]
Most agent frameworks can stop before a tool runs. These adapters put your user's phone behind that stop.
## Next steps [#next-steps]
---
# Vercel AI SDK
Source: https://pushary.com/docs/agents/build/vercel-ai-sdk
Last updated: 2026-10-04
> Vercel AI SDK human in the loop with toolApproval. Your user taps Approve or Deny on their phone before a risky tool runs, and no answer means no.
`@pushary/ai-sdk` adds human in the loop approval to Vercel AI SDK agents. Before a tool runs, the AI SDK asks an approval function whether it may. Pushary is that function: it sends the tool call to the right user's phone and returns their answer. The tool runs only if they tap Approve. It works with `generateText`, `streamText` and `ToolLoopAgent` in AI SDK 7.
```bash
npm i @pushary/ai-sdk ai zod
export PUSHARY_API_KEY=pk_xxx.xxx
```
Get the key at [Partner onboarding](https://pushary.com/onboarding/partner). The approval gate needs AI SDK 7 and Node.js 22 or later.
## Set up AI SDK human in the loop [#set-up-ai-sdk-human-in-the-loop]
### Connect a user once [#connect-a-user-once]
```ts
import { enroll } from '@pushary/ai-sdk'
const { universalLink } = await enroll({ apiKey: process.env.PUSHARY_API_KEY! }, user.id)
// Show universalLink to this user. They open it on their phone once.
```
`user.id` is your own id for the person who answers. Your users install the free Pushary app. They never sign up or pay.
### Gate the risky tool with toolApproval [#gate-the-risky-tool-with-toolapproval]
Pass `pusharyApproval()` as `toolApproval` and list the tools that need a yes. Every other tool runs as normal.
```ts
import { generateText, stepCountIs, tool } from 'ai'
import { z } from 'zod'
import { pusharyApproval } from '@pushary/ai-sdk'
const issueRefund = tool({
description: 'Refund an order',
inputSchema: z.object({ orderId: z.string(), amount: z.number() }),
execute: async ({ orderId, amount }) => refundOrder(orderId, amount),
})
const lookupOrder = tool({
description: 'Look up an order',
inputSchema: z.object({ orderId: z.string() }),
execute: async ({ orderId }) => findOrder(orderId),
})
const { text } = await generateText({
model: 'openai/gpt-4o',
tools: { issueRefund, lookupOrder },
toolApproval: pusharyApproval({ externalId: user.id, tools: ['issueRefund'] }),
stopWhen: stepCountIs(5),
prompt: 'Refund order 1234 in full.',
})
```
`refundOrder` and `findOrder` are your own functions.
### Run it and answer on the phone [#run-it-and-answer-on-the-phone]
When the model calls `issueRefund`, the AI SDK stops before `execute`. Your user's phone buzzes with the tool name and its input. They tap Approve and the refund runs. `lookupOrder` never asks.
The model cannot skip this step. The AI SDK asks the approval function before the tool runs, so the model never gets a say.
## What happens when nobody answers [#what-happens-when-nobody-answers]
The tool does not run. Each approval waits up to 55 seconds. A deny, an expired question or no answer in that time all come back as denied, and the AI SDK tells the model the call was not approved:
| Outcome | Reason 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.` |
## Let the model ask a question [#let-the-model-ask-a-question]
Sometimes the agent needs an answer, not a permission: which order, which address. `createPusharyTools` gives the model an `askHuman` tool it can call.
```ts
import { generateText, stepCountIs } from 'ai'
import { createPusharyTools } from '@pushary/ai-sdk'
const { text } = await generateText({
model: 'openai/gpt-4o',
tools: createPusharyTools({ apiKey: process.env.PUSHARY_API_KEY!, externalId: user.id }),
stopWhen: stepCountIs(10),
prompt: 'Ask the customer which order they need help with.',
})
```
The model chooses when to call `askHuman`, so use it for questions. Use `toolApproval` for anything that spends money, deletes data, or leaves your system. To add your own tools, spread them in: `tools: { ...createPusharyTools(config), ...myTools }`.
## Production notes [#production-notes]
### Make sure a person always decides [#make-sure-a-person-always-decides]
`pusharyApproval()` asks your site's policy first. A policy rule that names the tool can allow or deny it without paging anyone. Set `policy: false` when a person must always answer:
```ts
toolApproval: pusharyApproval({ externalId: user.id, tools: ['issueRefund'], policy: false })
```
### Gate one tool by name [#gate-one-tool-by-name]
The per-tool form of `toolApproval` takes `pusharyToolApproval`:
```ts
import { pusharyToolApproval } from '@pushary/ai-sdk'
toolApproval: {
issueRefund: pusharyToolApproval({ toolName: 'issueRefund', externalId: user.id }),
}
```
Drop `tools` from `pusharyApproval()` to gate every call.
### Retries and idempotency [#retries-and-idempotency]
The same tool call never asks twice. The question is keyed on the tool call id, the recipient and the full input, so a provider-level retry reuses the first question. A changed recipient or changed input asks again. If you replay a run under tool call ids you mint yourself, pass a stable `sessionId`.
### Pick the right person [#pick-the-right-person]
Take `externalId` from your signed-in user, never from the model's tool input. The model writes the tool input, so a prompt injection could send the approval to someone else.
### Answers that come later than 55 seconds [#answers-that-come-later-than-55-seconds]
Set `timeoutMs` to change the wait. A later answer does not restart a finished request. For answers that may take minutes or hours, save the AI SDK's pending approval and messages with the decision id, then resume only after you read the matching answer. The [delayed-review recipe](https://github.com/Pushary/pushary-ai-sdk/blob/main/examples/DELAYED-REVIEWS.md) does this with SQLite and needs Node.js 22.13 or later. To wake your worker when an answer lands, use a [signed webhook](/docs/agents/embed#webhooks).
### Versions [#versions]
`toolApproval` needs `ai@7`. It is not in `ai@5.0.0` or `ai@6.0.0`. The `askHuman` tool works from `ai@5`. `@pushary/ai-sdk` 0.3 requires `@pushary/server` 2.1.
Source and more examples: [@pushary/ai-sdk on GitHub](https://github.com/Pushary/pushary-ai-sdk). AI SDK reference: [Tool calling and tool approval](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling).
## Next steps [#next-steps]
---
# Connect any agent
Source: https://pushary.com/docs/agents/connect-any-agent
Last updated: 2026-10-08
> Connect any agent to Pushary over MCP or plain HTTP so it can ask you a question on your phone and wait for the answer. Works with n8n, Zapier and LangChain.
Pushary works with any agent that can call an MCP server or send an HTTP request: an ops bot, a research agent, an n8n or Zapier workflow, a LangGraph graph, or a Python script. The agent sends you a question, your phone shows it, and the agent continues with your answer. You need an API key and nothing else.
Set `PUSHARY_API_KEY` to your key, then ask your first question:
```bash
curl -X POST https://pushary.com/api/v1/server/ask \
-H "Authorization: Bearer $PUSHARY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"question":"Deploy the new build to production?"}'
```
Your phone shows a yes or no question. Answer it and the call returns `"answered": true` with `"value": "yes"` or `"value": "no"`.
Using Claude Code, Codex, Gemini CLI, Cursor or Hermes? Run the [quickstart](/docs/agents/quickstart) instead. One command installs the hooks, so risky actions stop until you approve them.
## Get your API key [#get-your-api-key]
Open **Settings > API keys** at [pushary.com/dashboard/agent/settings](https://pushary.com/dashboard/agent/settings) and click **Create Key**. The key looks like `pk_xxx.xxx`.
Export it where your agent runs:
```bash
export PUSHARY_API_KEY="pk_xxx.xxx"
```
Already ran `npx pushary@latest setup`? The key it saved works for `/ask` and `/send` too. The subscriber list, campaign, template and flow endpoints need a key created here.
Run the curl command above. Your phone shows the question.
No question on your phone? Install the Pushary app from the [App Store](https://apps.apple.com/us/app/pushary/id6785677563) or [Google Play](https://play.google.com/store/apps/details?id=com.pushary.app), sign in with the same account, and try again.
Pushary can only block an action before it runs when the agent has a hook for it, as in the [supported agents](/docs/agents/supported-agents) with enforced approvals. Any other agent gets questions and notifications. It decides for itself when to ask and must respect your answer.
## Ask a question and wait for the answer [#ask-a-question-and-wait-for-the-answer]
`POST https://pushary.com/api/v1/server/ask` sends the question to your phone app. It can also reach your browser and Slack, if you connected them. By default the call blocks until you answer, for up to 30 seconds.
```bash
curl -X POST https://pushary.com/api/v1/server/ask \
-H "Authorization: Bearer $PUSHARY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"question": "Send the campaign to all 4,000 contacts?",
"type": "confirm",
"agentName": "Marketing agent",
"timeoutSeconds": 50
}'
```
An answered question returns:
```json
{
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"question": "Send the campaign to all 4,000 contacts?",
"type": "confirm",
"status": "answered",
"answered": true,
"value": "yes"
}
```
The response also carries `liveUrl` (a page where you can answer), `pollUrl`, `expiresInSeconds` (600) and `mode` (your approval mode).
Proceed only when `answered` is `true` and `value` is `"yes"`. Anything else is not a yes.
| Result | What it means | What to do |
| ---------------------------------------------------- | -------------------------------------------------------------------- | --------------------------------------- |
| `"status": "answered"` | You answered. `value` holds the answer. | Act on `value`. |
| `"status": "timeout"` | The call stopped waiting. The question is still open for 10 minutes. | Poll `pollUrl`, or stop and do not act. |
| `"status": "stopped"` with `"handoffAction": "stop"` | The kill switch is on for this agent. Nothing was sent. | Stop the run. |
### Request fields [#request-fields]
| Field | Required | Notes |
| ---------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------- |
| `question` | Yes | Up to 8000 characters. |
| `type` | No | `confirm` (default), `select` or `input`. |
| `options` | For `select` | At least 2 choices. The MCP `ask_user` tool accepts 2 to 6, so keep to 6 if you use both. `value` is the chosen option. |
| `placeholder` | No | Hint text for an `input` question. `value` is the typed text, up to 5,000 characters. |
| `context` | No | Background shown above the question. |
| `agentName` | No | Shown in the push title as "Marketing agent is asking". |
| `sessionId` | No | Lets a kill switch for this session stop the ask. |
| `wait` | No | `true` (default) blocks. `false` returns at once. |
| `timeoutSeconds` | No | How long the call blocks. Default 30, maximum 55. |
| `callbackUrl` | No | HTTPS URL that receives the answer. See the webhook section below. |
## Ask without blocking, then poll [#ask-without-blocking-then-poll]
Set `"wait": false` and the call returns at once with `"status": "pending"` and a `pollUrl`:
```json
{ "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "pending", "answered": false, "pollUrl": "https://pushary.com/api/v1/server/ask/a1b2c3d4-e5f6-7890-abcd-ef1234567890" }
```
Poll it with `GET`. Add `?wait=30` to hold the request open for up to 30 seconds (maximum 55):
```bash
curl "https://pushary.com/api/v1/server/ask/a1b2c3d4-e5f6-7890-abcd-ef1234567890?wait=30" \
-H "Authorization: Bearer $PUSHARY_API_KEY"
```
`status` is `pending`, `answered`, `expired` or `cancelled`. An unknown ID returns 404.
Cancel a question you no longer need:
```bash
curl -X DELETE "https://pushary.com/api/v1/server/ask/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
-H "Authorization: Bearer $PUSHARY_API_KEY"
```
The response says `"cancelled": true` when a pending question was removed. `false` means it was already answered, expired or cancelled.
## Get the answer on a webhook [#get-the-answer-on-a-webhook]
Use a webhook when your process cannot wait, for example an n8n or Make workflow. In n8n or Make, pass the URL of a node that waits for a webhook as `callbackUrl`. The workflow resumes when the answer arrives.
Fetch your signing secret once. The first call creates it:
```bash
curl https://pushary.com/api/v1/server/webhook-secret \
-H "Authorization: Bearer $PUSHARY_API_KEY"
```
It returns `{ "webhookSecret": "whsec_..." }`. A `POST` to the same URL rotates it. Do this before your first webhook ask, or the callback is signed with a secret you cannot check.
Add `callbackUrl` to the ask. It must be HTTPS and must not point at localhost or a private network address:
```bash
curl -X POST https://pushary.com/api/v1/server/ask \
-H "Authorization: Bearer $PUSHARY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"question": "Email the refund confirmation to the customer?",
"wait": false,
"callbackUrl": "https://your-app.com/webhooks/pushary"
}'
```
Verify each callback. When you answer, Pushary sends a `POST` with this body:
```json
{
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"answer": "yes",
"value": "yes",
"answeredAt": "2026-10-04T12:00:00.000Z",
"context": "Only present when you passed context"
}
```
The `X-Pushary-Signature` header is the hex HMAC-SHA256 of the raw request body, keyed with your webhook secret:
```ts
import crypto from 'node:crypto'
const isFromPushary = (rawBody: string, signature: string, secret: string): boolean => {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
return expected.length === signature.length
&& crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
}
```
The callback fires only when someone answers, never on a timeout or a cancel. Return a 2xx status within 10 seconds. Pushary does not follow redirects, and it retries a failed delivery up to 12 attempts in total, so handle the same `correlationId` twice safely.
The REST API and the MCP server share one question store. An answer from your phone, the Mac app, the web page or Slack settles the question however it was asked.
## Send a one-way notification [#send-a-one-way-notification]
The MCP `send_notification` tool reaches your phone app and your browser. You can call it over plain HTTP, with no MCP library:
```bash
curl -X POST https://pushary.com/api/mcp/mcp \
-H "Authorization: Bearer $PUSHARY_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"send_notification","arguments":{"title":"Backfill finished","body":"Processed 12,400 rows with 0 errors."}}}'
```
`title` takes up to 100 characters and `body` up to 500. The limit is 10 notifications per 5 minutes per site.
`POST /api/v1/server/send` reaches **browser push subscribers only**. It does not reach the Pushary phone app. If nobody has subscribed in a browser, it returns 400 `No subscribers found matching criteria`.
## Connect an MCP client [#connect-an-mcp-client]
Point any MCP client at the Pushary endpoint. The agent then has `ask_user`, `wait_for_answer`, `cancel_question`, `send_notification` and `propose_scope` as tools.
```json title="mcp.json"
{
"mcpServers": {
"pushary": {
"url": "https://pushary.com/api/mcp/mcp",
"headers": {
"Authorization": "Bearer ${PUSHARY_API_KEY}"
}
}
}
}
```
The official MCP SDKs for TypeScript and Python connect with only the URL and the `Authorization` header. A custom agent, or a LangChain or LangGraph tool, can use the same settings. For the approval patterns, see [Human in the loop](/docs/agents/human-in-the-loop).
## Name your agent [#name-your-agent]
Pass `agentName` (and `sessionId` if you have one) on every call. The dashboard and the Fleet Board then label the work with your agent's name instead of a generic session. Several agents running at once each show up on their own.
## Errors and limits [#errors-and-limits]
| Response | Cause |
| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400 | `question` missing or over 8000 characters, a `select` with fewer than 2 options, or a `callbackUrl` that is not HTTPS or points at a private host. |
| 401 | The API key is missing, wrong or revoked. |
| 403 `forbidden`, `This key cannot access this endpoint.` | The key cannot call this endpoint. A key from setup or phone pairing cannot call the subscriber list, campaign, template or flow endpoints. Create a key in **Settings > API keys** for those. |
| 403 `subscription_required` | The workspace has no active plan or trial. |
| 403 `partner_decision_required` | Partner plan workspaces use `POST /api/v1/server/decisions` instead. |
| 429 | 25 questions are already pending for this site, or you hit your plan's request limit. |
If your approval mode is In agent, `/ask` sends no push and the response says `"mode": "terminal_only"`. To get every question on your phone, open the Pushary app, tap **Questions & approvals** at the top of your inbox, and pick **Always**. See [Permission policies](/docs/agents/policies) for all four modes.
## Next steps [#next-steps]
---
# Multi-agent control panel
Source: https://pushary.com/docs/agents/control-panel
Last updated: 2026-10-07
> See every AI agent session in one control panel. Tell parallel sessions apart, spot the one waiting on your approval, and stop it in one tap.
The control panel shows every agent session you run, on every machine, in one place. Two Claude Code windows, a Codex session and a background refactor each get their own row, so you can see which one is waiting on you, which is working, and which has failed. It lives at **Dashboard > Agent**, and agents can read the same view over MCP with [`list_sessions`](/docs/agents/reference/tools#list_sessions).
## The blocked queue [#the-blocked-queue]
Open [pushary.com/dashboard/agent](https://pushary.com/dashboard/agent). The overview groups everything by session and puts the sessions waiting on you first.
* **Board** view sorts sessions into four columns: Working, Waiting on you, Done and Errored.
* **List** view shows each session as a card with its agent, machine, status and any open question.
You can answer a pending question right on the card, and **Stop** a session from it. Stopping works on every plan; see the [kill switch](/docs/agents/kill-switch). Without a paid plan, the overview shows 3 sessions.
## Session status [#session-status]
Each session has a status computed from what it is doing now:
| Status | Meaning |
| --------- | ------------------------------------------------------------- |
| `waiting` | Has an open approval question. This is the one that needs you |
| `active` | Seen in the last 2 minutes, nothing pending |
| `idle` | Was active, but not seen for 2 minutes or more |
| `errored` | Last reported an error |
| `offline` | Not seen for 15 minutes or more |
`waiting` always wins: a session with a pending question shows as waiting however long ago it was last seen. `errored` stays until the session recovers or goes quiet for 15 minutes, when it shows as `offline`. Sessions not seen for 24 hours drop out of the dashboard.
## How Pushary tells parallel sessions apart [#how-pushary-tells-parallel-sessions-apart]
The hook attaches two ids to everything a session sends:
Site, agent type, machine and session together identify one session. That is why two `claude` windows in the same repo on the same machine show as two rows, and why a pending approval is pinned to the exact session that raised it.
The machine id is random, not derived from your computer. The CLI does send your computer's hostname when it registers the machine, so the dashboard and the phone app can show a name you recognize.
## Start a session from your phone [#start-a-session-from-your-phone]
The phone app can start a new agent session on a machine that runs the Pushary daemon. Install it as a background service with `pushary daemon install`; see the [CLI reference](/docs/agents/reference/cli).
The app accepts a launch only when the machine you picked, or at least one machine behind **Any machine**, is online and supports starting sessions remotely. It then shows the launch state: queued, claimed, retrying, started, failed or expired. A claimed launch holds a short lease, so if a daemon exits before it starts the process, another daemon can retry it, up to 3 attempts.
Current daemons confirm a launch only after the operating system creates the process. Older daemons still work, but the app cannot confirm their launches. It labels the result unconfirmed and suggests updating the CLI. Pushary never retries an unconfirmed launch, so it cannot start the same session twice.
## Read sessions from an agent with list\_sessions [#read-sessions-from-an-agent-with-list_sessions]
A supervising agent can check the fleet over MCP. `list_sessions` is read-only: it never starts, stops or steers anything, and it sends no notification. It sees only your API key's site, and no parameter can widen it to another account.
```json
{
"tool": "list_sessions",
"arguments": { "status": "waiting", "limit": 20 }
}
```
It returns `{ sessions, pendingQuestions }`. Each session carries its identity (`agentType`, `agentName`, `machineId`, `sessionId`), its `computedStatus`, its last action and event, and a readable `timeAgo`.
The `status` filter accepts `active`, `idle`, `waiting` and `errored`. It does not accept `offline`. An unfiltered call still returns offline sessions, but you cannot filter for them.
`list_sessions` needs a key from setup or the dashboard. Claude and ChatGPT connector keys cannot call it. See the [tools reference](/docs/agents/reference/tools#list_sessions) for the full schema.
Each window has its own `sessionId`, and Pushary keys sessions by it. A pending approval is attributed to the exact session that raised it, not guessed from the agent's name.
Every hook event, such as a tool call, a question or a session end, updates the session's `lastSeenAt`. Sessions not seen in 24 hours drop out of the dashboard.
No. It writes nothing: no audit row, no session update, no push. It reads the same data the dashboard shows.
## Next steps [#next-steps]
---
# Test your integration
Source: https://pushary.com/docs/agents/embed-testing
Last updated: 2026-10-04
> Test your human in the loop integration. Simulate approve, deny and no answer with a sandbox key, then check retries, webhooks and real phone delivery.
Before you turn on real customer actions, check that your code handles every answer. Start with a sandbox key, which settles each decision as approve, deny or no answer without sending a notification. Then test the real channel your customers will use. Keep the action behind the approval replaced with a harmless local operation until both pass.
While signed in to your Pushary dashboard, run this in the browser's developer console to get a sandbox key:
```js
const response = await fetch("/api/agent/sandbox", { method: "POST" })
const sandbox = await response.json()
if (!response.ok) throw new Error(sandbox.error)
```
## API sandbox [#api-sandbox]
The API sandbox needs a signed-in workspace with a site, permission to act on agents, and an active or trialing subscription on any plan. It does **not** need the Partner plan. Without an active or trialing subscription, sandbox calls return `403` with `code: "subscription_required"`. It is separate from the [public demo](/try) and from the **Send a test approval** card in the Partner dashboard.
### Get a sandbox key [#get-a-sandbox-key]
Call `POST /api/agent/sandbox` while signed in, with the snippet above. The response's `key` is shown once. Copy it into your local server environment as `PUSHARY_API_KEY`. Do not paste your dashboard session cookie into scripts.
| Response | What it means |
| -------- | ---------------------------------------------------------------- |
| 404 | No current site is set up |
| 403 | Your role cannot act on agents |
| 409 | The workspace already has 5 live sandbox keys. Revoke one first. |
### Run all three answers [#run-all-three-answers]
Install `@pushary/server`, then run this script with your sandbox key:
```ts
import assert from "node:assert/strict"
import { randomUUID } from "node:crypto"
import { createPusharyServer } from "@pushary/server"
const pushary = createPusharyServer({ apiKey: process.env.PUSHARY_API_KEY! })
for (const outcome of ["approve", "deny", "no_answer"] as const) {
const result = await pushary.decisions.ask({
externalId: "integration-test-user",
question: "Allow this test action?",
type: "confirm",
sandboxOutcome: outcome,
idempotencyKey: randomUUID(),
timeoutMs: 1000,
expiresInSeconds: 60,
})
assert.equal(result.approved, outcome === "approve")
assert.equal(result.answered, outcome !== "no_answer")
assert.equal(result.status, outcome === "no_answer" ? "pending" : "answered")
console.log(outcome, result.decisionId, result.status)
}
```
It prints three lines, one per outcome, and every assertion passes.
### Check what no answer leaves behind [#check-what-no-answer-leaves-behind]
`no_answer` leaves the decision open. The one-second SDK wait does not expire it. Save the id and check that it becomes `expired` after its 60-second lifetime, or cancel it when you are done.
### What the sandbox key can do [#what-the-sandbox-key-can-do]
Sandbox keys can reach `identity`, `authorize`, `authorization-rules`, `decisions`, `authorizations` and `reachability`. They cannot enroll a real device, send notifications, issue keys, or manage webhook secrets.
Without `sandboxOutcome`, the simulator approves. For a `select` question, `approve` records the first option, and `deny` withdraws the question, so it reads as `cancelled`.
Sandbox decisions use the real decision store and record the simulator as the answer source. They send no phone, browser or Slack notification and use none of your production notification allowance. They do not prove webhook delivery or any downstream action in your application.
### Check retries and recovery [#check-retries-and-recovery]
| Check | Expected result |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| Repeat create with the same recipient and idempotency key | Same decision id |
| Retry after HTTP 503 or a failed create | Honor `Retry-After` when present, reuse the key, and check that the returned id can be read |
| Race two creates with the same key and recipient | One stored decision and one delivery fan-out |
| Read the saved id after restarting your worker | Same stored result |
| Leave a decision unanswered until expiry | `expired`, and the action stays blocked |
| Cancel an abandoned decision | `cancelled`, and no action runs |
| Use an `allow`, `deny` or unmatched authorization rule | `allow`, `deny` or `requires_human` |
| Claim the same authorization permit twice | Second claim refused as `already_consumed` |
Test your workflow's execution count too. A stored approval must not cause two side effects when a webhook or worker job is replayed.
## Test live delivery [#test-live-delivery]
Use a **live Partner key** from [Agent settings](/dashboard/agent/settings) and an id for a test user you control. Finish enrollment before you send the approval.
### Connect a test user [#connect-a-test-user]
```bash
curl --fail-with-body https://pushary.com/api/v1/server/enroll \
-H "Authorization: Bearer $PUSHARY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"externalId":"integration-test-user"}'
```
Open the returned `universalLink` and finish one of these paths:
| Channel | What to check |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Native app | Install Pushary if needed, open the link, and agree. After a fresh install, tap **Invited by a company?** and scan the saved QR code from another screen if the link does not reopen the app. |
| Browser | Choose **Enable browser notifications** in a supported browser and allow notifications. Ordinary iOS Safari tabs cannot receive browser push notifications. Use the native app or a supported Home Screen web app. |
| Your own channel | Create with `approvalUrl: true` and a full-access key, then send the returned link through your own email, SMS or inbox service. |
### Create a decision [#create-a-decision]
```bash
curl --fail-with-body https://pushary.com/api/v1/server/decisions \
-H "Authorization: Bearer $PUSHARY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: integration-test-1" \
-d '{
"externalId":"integration-test-user",
"question":"Allow this test action?",
"type":"confirm",
"requireReachable":true,
"expiresInSeconds":3600
}'
```
Use a new idempotency key for each new test. Leave out `requireReachable` if you are testing your own approval link instead of push. Your test phone buzzes with "Allow this test action?".
### Answer on the device and read the result [#answer-on-the-device-and-read-the-result]
Save the returned decision id, answer on the device, then read it:
```bash
curl --fail-with-body \
"https://pushary.com/api/v1/server/decisions/DECISION_ID?wait=30" \
-H "Authorization: Bearer $PUSHARY_API_KEY"
```
Check both `"yes"` and `"no"`. Only a yes should release your test action.
### Slack [#slack]
Connect Slack in your dashboard integrations. A decision with no `externalId` and no `email` goes to the configured shared channel, or to the member DM set on the integration. An `email` sends a DM to the Slack member with that email. If Pushary cannot find that member, it does not fall back to the shared channel, and a customer decision with no `email` is not sent to Slack.
Test with non-sensitive content. Slack does not count toward end-user push reachability. Review which team members may see and answer customer requests before you turn this channel on.
### Webhooks and your own UI [#webhooks-and-your-own-ui]
Set your callback URL, fetch the site's `webhookSecret`, and use the [verified callback example](/docs/agents/embed#webhooks). Then check that:
* a real answer reaches your endpoint
* an invalid signature is rejected
* a replayed callback does not run your workflow twice
* polling recovers the answer while the callback is unavailable
For headless answers, check that your backend rejects a user who tries to answer another user's decision. If a phone already recorded `"no"`, a later API answer must keep that result.
## What these tests prove [#what-these-tests-prove]
The sandbox checks API behavior and how your code handles each response. Live tests check enrollment, notification permissions, delivery and callback setup. Neither proves an external side effect is safe to retry. Use the downstream provider's idempotency controls and your saved workflow state for that.
## Next steps [#next-steps]
---
# Embed human approval in your product
Source: https://pushary.com/docs/agents/embed
Last updated: 2026-10-07
> Embed human in the loop approval in your product. Connect an end user, create an approval, verify the webhook and resume your workflow only after a yes.
This is the full Partner guide for asking **your users** before your agent acts. Your backend picks who answers, creates a decision, and carries on when the answer comes back. Your user gets a push notification. Yes or no works from the lock screen. Choices and text open the app. A missing answer never counts as approval.
```bash
npm install @pushary/server
```
New here? The [builder quickstart](/docs/agents/build/quickstart) covers the two calls most products need. This page covers the rest: callbacks, resuming later, sandbox keys and answers from your own app.
To get your own agents' questions on your phone instead, use the [agent quickstart](/docs/agents/quickstart).
## What you need [#what-you-need]
* A backend that owns the action and signs in your users.
* An active Partner or Enterprise workspace for live delivery. Partner is **$99 a month**, with a **3-day trial and a card required at checkout**.
* A stable user id from your application, passed as `externalId`.
* A way to reach that user: the Pushary app, browser push notifications, a link you deliver yourself, or your own signed-in approval screen.
Pushary handles the decision, delivery, answer storage and callbacks. Your code must check the result before it runs the action, and save enough workflow state to resume after a restart. A tool the model may choose to call does not guard your other tools.
[Try the loop](/try) before you integrate, or use the [API sandbox](/docs/agents/embed-testing#api-sandbox) to test without the Partner plan. The API sandbox needs a signed-in workspace with a site and an active or trialing subscription on any plan, the Agent plan included.
## 1. Set up your workspace and key [#1-set-up-your-workspace-and-key]
Start [Partner signup](/sign-up?from=agent\&plan=partner_launch), finish checkout, and follow onboarding to set your product name and logo. You can test delivery to your own phone during onboarding.
Onboarding issues a runtime key with `agent` scope. Use it to connect users and to create and read decisions. It cannot administer the site, answer for a customer, or return an `approvalUrl`.
Copy your full API key from [Agent settings](/dashboard/agent/settings) and store it as `PUSHARY_API_KEY` on your server. The full key looks like `pk_xxx.xxx`. The public `pk_xxx` prefix alone cannot call this API.
```ts
import { createPusharyServer } from "@pushary/server"
const pushary = createPusharyServer({
apiKey: process.env.PUSHARY_API_KEY!,
})
```
Keep the full key out of browser code, prompts and logs. Your server takes the recipient from the signed-in session. For an agent runtime that serves one user, use a [bound key](#per-end-user-keys-multi-tenant). It can create, read and cancel that user's decisions, but it cannot answer them.
## 2. Connect the person who will approve [#2-connect-the-person-who-will-approve]
Run this on your backend for the signed-in user:
```ts
const enrollment = await pushary.enroll(user.id)
// Show enrollment.universalLink as a button or QR code in your app.
```
Creating the link does **not** connect a device. The user must open it and agree.
| Channel | What the user does |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Pushary app | Install the iPhone or Android app, open the link, and turn on approvals. An invited end user needs no Pushary account or subscription. |
| Browser push | Open the link in a supported browser and choose **Enable browser notifications**. On iOS this needs a Home Screen web app, which is separate from the native Pushary app. |
| Your email, SMS or inbox | Ask for an `approvalUrl` when you create the decision and send it through your own service. No push enrollment is needed. |
| Your own screen | Sign the user in and relay their answer from your backend. See [headless answers](#answer-from-your-own-app-headless). |
### What your end user sees [#what-your-end-user-sees]
When the app is installed, the link opens a consent screen with your name and logo. If they install the app after opening the link, the install does not pick up the enrollment. They can open the link again, or tap **Invited by a company?** in the app and scan the QR code from another screen. Keep the original link and QR code available until setup finishes.
Once connected, each approval arrives as a notification titled with your product name, such as **Acme needs your approval**. On iPhone, a second line names the agent from `agentName`, such as **via Refund bot**, when it differs from your product name. Android shows no second line. The notification icon is the Pushary app's own. Your logo appears on the consent screen and on the approval card inside the app, which also names your product and the agent.
The end user can disconnect from your product at any time in the app, without affecting other companies they are connected to. That phone then stops counting as a push channel for them. Send a new link to reconnect.
Links last **24 hours** and can be redeemed once per channel (native app and browser push). Make a fresh link for another device or an expired invitation. Treat these personal links as credentials: whoever redeems one connects as that user.
If single-use storage is unavailable, redeeming returns HTTP 503 without connecting a device. Retry after the returned `Retry-After` delay.
Invited users do not sign in or pay. **Sign in** in the app starts the separate flow for people who use Pushary for their own agents.
### Check reachability [#check-reachability]
```ts
const reach = await pushary.reachability(user.id)
// deliverability: "push" | "fallback" | "unreachable"
```
| `deliverability` | What it means |
| ---------------- | ----------------------------------------------------------------------------------------------------------------- |
| `push` | An enrolled native device or active browser subscription exists. It does not confirm a notification was received. |
| `fallback` | You named an end user, but Pushary has no confirmed push channel for them. Deliver an approval link yourself. |
| `unreachable` | No recipient was named. |
Set `requireReachable: true` on a create to reject it with HTTP 409 and `code: "unreachable"` when the recipient has no registered push channel. The check does not count Slack or links you deliver yourself. An unavailable reachability read is not a delivery guarantee.
## 3. Create an approval and save its ID [#3-create-an-approval-and-save-its-id]
Use create-and-resume for serverless functions and jobs that may restart. `decisions.create()` returns at once by default.
```ts
const decision = await pushary.decisions.create({
externalId: user.id,
question: "Publish this video to your public profile?",
type: "confirm",
toolName: "video.publish",
toolTarget: video.id,
context: runId,
idempotencyKey: `${runId}:publish:${video.id}`,
expiresInSeconds: 3600,
callbackUrl: "https://yourapp.com/webhooks/pushary",
})
// Save decision.decisionId against this run and its exact proposed action.
// Mark the run as waiting and return from the request.
```
A new decision normally returns `status: "pending"` and `answered: false`. Handle whatever state comes back: a retry can return a decision that is already answered, and a stopped workspace can return `status: "stopped"`.
### Idempotency keys [#idempotency-keys]
Use the **same idempotency key for retries of one operation**, and a new key for a new operation. The deduplication window is 24 hours, scoped to your site and the recipient. Do not build the key from the question text alone, and do not reuse it for a changed action. `decisions.ask()` makes a fresh key for each call if you omit one, so it does not deduplicate a later call after your process restarts.
If idempotency storage is unavailable, a create with an idempotency key returns HTTP 503 instead of risking a duplicate decision. Honor `Retry-After` and retry with the same key. A retry after a failed insert can complete the originally reserved id. A reservation alone is never returned as a pending decision.
### REST equivalent [#rest-equivalent]
```bash
curl --fail-with-body https://pushary.com/api/v1/server/decisions \
-H "Authorization: Bearer $PUSHARY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: run-42-publish-video-7" \
-d '{
"externalId": "user-123",
"question": "Publish this video to your public profile?",
"type": "confirm",
"toolName": "video.publish",
"toolTarget": "video-7",
"expiresInSeconds": 3600
}'
```
### Deliver the link over your own channel [#deliver-the-link-over-your-own-channel]
Set `approvalUrl: true` and `externalId` on the create request. On a new decision, the response includes an `approvalUrl` bound to that decision, site and recipient. It needs a full-access key: the onboarding `agent` key gets no link back. The link expires after 12 hours or when the decision expires, whichever comes first. Store it securely if you send it later, because idempotent replays do not return it.
The link lets whoever holds it answer as the named recipient. Do not log it or treat it as proof of the person's identity. For sensitive actions, use your own signed-in screen. Pushary does not send your email or SMS for you.
## 4. Resume only after approval [#4-resume-only-after-approval]
Poll the saved decision id from a worker or scheduled job:
```ts
const state = await pushary.decisions.get(decisionId, { wait: 30 })
if (state.status === "answered" && state.value === "yes") {
// Atomically claim your saved workflow step, then execute the saved action.
} else if (state.status === "pending") {
// Keep the action paused; schedule another read of this same decision ID.
} else {
// Denied, expired, or cancelled: do not execute the action.
}
```
This example is for `type: "confirm"`. For `select` or `input`, check the returned value against what your workflow accepts. An answer is not permission to run any action.
### Webhooks [#webhooks]
A callback can wake your worker as soon as an answer arrives:
```json
{
"correlationId": "the-decision-id",
"answer": "yes",
"value": "yes",
"answeredAt": "2026-09-04T12:00:00Z",
"context": "run-42"
}
```
`correlationId` is the `decisionId` you saved. `answer` is canonical and `value` is an alias. `context` is optional and goes through secret redaction.
With an explicitly authorized administration credential, fetch the site's signing secret once with `pushary.decisions.getWebhookSecret()` and store its `webhookSecret` securely. In your callback handler, verify the **raw request body** before you parse it:
```ts
import { parseDecisionCallback, verifyWebhookSignature } from "@pushary/server"
const rawBody = await request.text()
if (!verifyWebhookSignature(
rawBody,
request.headers.get("x-pushary-signature"),
process.env.PUSHARY_WEBHOOK_SECRET!,
)) {
return new Response("Invalid signature", { status: 401 })
}
const callback = parseDecisionCallback(rawBody)
if (!callback) return new Response("Invalid callback", { status: 400 })
// Enqueue a read of callback.correlationId using your saved run mapping.
// Deduplicate execution in your workflow; callbacks can be retried.
return new Response(null, { status: 204 })
```
Keep polling as a backup for missed callbacks and to discover expiry. A webhook alone does not restart your agent or guarantee the side effect runs once.
### If nobody answers [#if-nobody-answers]
The decision stays open for **1 hour by default**. You can set anything from **60 seconds to 24 hours** with `expiresInSeconds`. Expired and cancelled decisions are not approvals.
For a long-lived worker, `decisions.ask()` combines create and polling. Its default wait is **55 seconds**. When it returns `approved: false`, the decision may still be pending. Save its id and poll later, or cancel it if you abandon the action. `create({ wait: true })` holds one HTTP request for 30 seconds by default, capped at 55 seconds. Neither wait changes the decision's expiry.
## Answer from your own app (headless) [#answer-from-your-own-app-headless]
Your backend must check that the signed-in user may answer this specific decision:
```ts
const result = await pushary.decisions.answer(decisionId, "yes")
// Read result.value: another surface may already have recorded "no".
```
This endpoint needs a full-access key, called from your server. The runtime key from onboarding and bound keys cannot submit customer answers. A bound key gets a 403 with the code `bound_key_cannot_answer`. Pushary trusts the full-access key and records the answer as a partner relay. Keep that key in trusted server code, away from the model and the end user.
## Per-end-user keys (multi-tenant) [#per-end-user-keys-multi-tenant]
Issue a recipient-bound key from your backend with a separately authorized administration credential. The onboarding runtime key cannot issue keys.
```ts
const session = await pushary.keys.issue({
externalId: user.id,
expiresInSeconds: 3600,
})
// Configure this user's trusted runtime with session.apiKey.
// When the session ends:
await pushary.keys.revoke(session.keyPrefix)
```
A bound key forces its own recipient when creating decisions. Enrollment requires your full-access key on your trusted backend. It can create, read and cancel decisions for that one user, and cannot reach another user's decisions. It cannot answer a decision, not even one for its own user. The answer comes from the person, or from your full-access key on your server. A bound key does not replace signing the person in on your own screen.
## Use MCP or a framework adapter [#use-mcp-or-a-framework-adapter]
[Framework adapters](/docs/agents/adapters) plug into your framework's own tool approval and durable workflow mechanisms.
A Partner or Enterprise MCP connection exposes `enroll_end_user`, `create_decision`, `get_decision` and `cancel_decision`. Use your full-access key on your trusted backend for `enroll_end_user`. Use a bound key in the agent runtime for the three decision tools and leave out the model-supplied `externalId`. Bound keys cannot enroll devices. None of these tools answers a decision. A connector-scoped key does not expose these tools. See the [MCP reference](/docs/agents/reference/tools#partner-decisions).
Only decisions reach a phone connected through your link. `ask_user`, `send_notification` and `POST /api/v1/server/ask` also accept `externalIds`, but there they target browser push subscribers, never a phone connected through your link.
## Let policy answer, and a person only when it cannot [#let-policy-answer-and-a-person-only-when-it-cannot]
Policy administration and the authorization endpoints need a credential that is explicitly authorized for them. The onboarding runtime key is for customer questions and decisions. It cannot administer rules or use these authorization calls. Keep the privileged client separate from the agent runtime.
`decisions.ask()` always asks a person. `pushary.authorize()` checks policy first and calls `decisions.ask()` only for `requires_human`. Check `approved` before you run an action. For serverless workflows, use `evaluateAuthorization()` and create a durable decision yourself when it returns `requires_human`.
Rules match `toolName`, optionally `toolTarget`, and scalar `parameters`. An unmatched action asks a person. A threshold rule that only requires approval above a limit does **not** allow everything below that limit. Add an explicit `allow` rule for that range.
For example, create these two rules with `POST /api/v1/server/authorization-rules`:
```json
{
"toolPattern": "refund.create",
"effect": "allow",
"conditions": [{ "parameter": "amount", "operator": "lt", "value": 500 }]
}
```
```json
{
"toolPattern": "refund.create",
"effect": "require_approval",
"conditions": [{ "parameter": "amount", "operator": "gte", "value": 500 }]
}
```
Use the same units in your rules and your calls. Among matching rules, `deny` wins over `require_approval`, which wins over `allow`. Missing or incompatible parameter values require a person. Conditions within one rule must all match. Operators are `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in` and `not_in`.
If no parameter rule applies, named permission rules are checked, then an unmatched action returns `requires_human`. The agent wildcard `*` does not authorize business actions.
## Label what the decision is about [#label-what-the-decision-is-about]
Use `toolName` and `toolTarget` for a stable action and resource. `actor` names whose authority the action uses, and `externalId` names the approver. `environment` tells deployments apart. `parameters` takes up to 32 scalar values (strings, finite numbers or booleans). Keep secrets out of every field.
These fields describe the proposed action for audit filtering and policy checks. Your execution code must run the same action with the same values that were approved.
## Say what the action does, not what the tool is called [#say-what-the-action-does-not-what-the-tool-is-called]
Send `presentation` on `decisions.create()` or `decisions.ask()` to describe the effect in business language. Each change names a key in `parameters`, so the value shown comes from the same data a rule reads:
```ts
await pushary.decisions.create({
externalId: user.id,
question: "Approve a EUR 48.00 refund?",
toolName: "refund.create",
parameters: { amount: 4800 },
idempotencyKey: `${runId}:refund:${order.id}`,
presentation: {
label: "Refund this order",
effect: "Returns EUR 48.00 to the customer's card.",
changes: [{
parameter: "amount",
label: "Refund amount",
format: { kind: "currency", currency: "EUR" },
}],
},
})
```
Currency values use integer minor units, so `4800` is EUR 48.00. The other formats are `quantity`, `timestamp`, `boolean` and `text`. An invalid presentation returns HTTP 400. Without a presentation, the plain question still shows.
## Run it once, and prove it ran [#run-it-once-and-prove-it-ran]
For execution receipts and a single-use permit, use `protect()`:
```ts
import { createAdapterKernel } from "@pushary/server/adapters"
const kernel = createAdapterKernel("my-product")
const protect = kernel.protect({ apiKey: process.env.PUSHARY_API_KEY! })
const outcome = await protect({
action: "refund.create",
target: order.id,
externalId: user.id,
facts: { amount: order.amountMinor, currency: "EUR" },
callId: toolCall.id,
runId: run.id,
run: () => issueRefund(order),
})
if (!outcome.ok) return outcome.reason
```
The permit binds the authorization to the action, target, actor, environment, recipient and facts. A second claim of the **same authorization** is refused. Keep workflow ids stable and use downstream idempotency where the provider supports it. A new authorization is a new permit, and Pushary cannot make an external API call exactly-once.
`protect()` tries to record success or failure. If the process dies or receipt storage fails, the permit can stay `running`. Reconcile the downstream outcome before you retry. Errors thrown by `run` reach your code.
For a custom executor, call `POST /api/v1/server/authorizations/consume` with the authorization id and the exact action, then `POST /api/v1/server/authorizations/{permitId}/receipt` with the outcome. Refusals are `not_authorized`, `action_mismatch`, `expired` and `already_consumed`.
## Send customer approvals to Slack [#send-customer-approvals-to-slack]
Connect Slack in your workspace's integrations. Slack and push deliver in parallel, not as an ordered fallback chain. Check that the Slack audience is right before you send customer approvals there.
| Decision | Where Slack sends it |
| ------------------------------------------------------- | ---------------------------------------------------------------------- |
| No `externalId` and no `email` (owner or team decision) | The configured shared channel, or the member DM set on the integration |
| `email` set | A DM to the Slack member with that email |
| `externalId` set but no `email` | Not sent to Slack |
Create customer decisions with your server-side Partner key, the customer's `externalId` and their Slack `email`. Pushary looks up that email in the connected Slack workspace and saves the member and DM channel before it posts. If the member cannot be found, Pushary does not post the request to the shared channel. Temporary Slack failures are retried. Enrolled phone and browser delivery work independently.
A key bound to an end user cannot supply a Slack email. Use that key with the customer's enrolled phone or browser, and keep Slack recipient selection in your trusted server code.
Slack answers must come from the saved member, workspace and channel. Shared channel answers stay available for owner and team decisions without a customer recipient. A choice or written answer is data, not permission to run an action.
Older Slack cards created before recipient pinning cannot prove their recipient and are refused. Answer those requests in the app or browser, or cancel and recreate them. New cards keep their saved destination when another surface answers, even if the default Slack channel later changes.
## Test and operate [#test-and-operate]
[Test your integration](/docs/agents/embed-testing) walks through approval, denial, unanswered decisions, retries and real delivery. [Pricing and limits](/docs/pricing-and-limits#partner-plan-embed) covers the accepted-request and legacy delivery allowances, API limits and decision lifetime.
## Next steps [#next-steps]
---
# FAQ
Source: https://pushary.com/docs/agents/faq
Last updated: 2026-10-04
> Answers to common questions about Pushary for AI agents, from setup and API keys to supported agents, the phone app, missing notifications and pricing.
Short answers to the questions people ask most about Pushary for AI agents. Pushary sends push notifications and approval requests from agents such as Claude Code, Codex, Cursor and ChatGPT to your phone, so you can answer from anywhere. For the full detail on any topic, use the table after the answers.
## Read more about each topic [#read-more-about-each-topic]
| Topic | Full page |
| -------------------------------------------------- | --------------------------------------------------------------- |
| Which agents work, and which can enforce approvals | [Supported agents](/docs/agents/supported-agents) |
| Getting, replacing or revoking a key | [Get your API key](/docs/agents/api-key) |
| Where notifications arrive | [Receiving notifications](/docs/agents/receiving-notifications) |
| Cursor plugin or plain MCP | [Cursor setup](/docs/agents/guides/cursor) |
| Which actions need your approval | [Permission policies](/docs/agents/policies) |
| Stopping an agent right now | [Kill switch](/docs/agents/kill-switch) |
| Plans, prices and limits | [Pricing and limits](/docs/pricing-and-limits) |
## Next steps [#next-steps]
---
# Antigravity CLI setup
Source: https://pushary.com/docs/agents/guides/antigravity-cli
Last updated: 2026-10-08
> Set up phone approvals for Antigravity CLI (agy) through its native hooks. Pushary becomes agy's only gate for shell commands, edits and MCP calls.
Antigravity CLI (`agy`) is the agent Google moved Google AI Pro, Ultra and free accounts to from Gemini CLI on 18 June 2026. Pushary gates it through agy's native hooks. Before agy runs a shell command, writes or edits a file, or calls an MCP tool, the call goes through your Pushary policy, and the risky ones wait for your answer on your phone.
## Set it up [#set-it-up]
Install agy with Google's installer and start it once to sign in. Then run:
```bash
npx pushary@latest setup
```
On Windows PowerShell, run `npx.cmd pushary@latest setup`. The CLI needs Node.js 20.17+, 22.13+ or 23.5+.
### Scan the QR code with your phone [#scan-the-qr-code-with-your-phone]
Setup shows a QR code first. Scan it with your phone's camera or the Pushary app. Sign in or create an account, check that the code on your phone matches the one in your terminal, then tap **Approve** on **Connect this computer?** and allow notifications.
New accounts start a 3-day free trial, then pay $9.99 a month. A card is needed to start the trial. If this computer already has a Pushary key, setup reuses it and skips this step.
### Pick Antigravity CLI [#pick-antigravity-cli]
Setup asks **Which agents do you use?** with the agents it found already selected. Keep Antigravity CLI selected. To skip the question, run `npx pushary@latest setup --agents antigravity_cli`.
### Answer the test question [#answer-the-test-question]
Setup sends a test push, then a test question. Answer it on your phone. The terminal says **You're set** once your answer arrives.
### Start a new agy session [#start-a-new-agy-session]
agy loads hooks when a session starts. Run `/hooks` inside agy and check that `pushary` is listed.
On a Mac you can use [Pushary Isle](/docs/agents/mac-app) instead. Connecting your agents there wires agy the same way.
## What setup changes on your computer [#what-setup-changes-on-your-computer]
| File | What changes |
| --------------------------------------------------- | --------------------------------------------------------------------------------- |
| `~/.gemini/config/hooks.json` | Adds a hook named `pushary` for `PreToolUse`, `PostToolUse` and `Stop`. |
| `~/.gemini/config/mcp_config.json` | Adds the Pushary MCP server (`serverUrl`, your key in an `Authorization` header). |
| `~/.gemini/antigravity-cli/settings.json` | Sets `"toolPermission": "always-proceed"`, only if you never chose a mode there. |
| `~/.pushary/setup/antigravity-tool-permission.json` | A record of that change, so disconnecting can put your setting back. |
| `~/.pushary/config.json` and your shell profile | Your API key, as `PUSHARY_API_KEY`. |
### What each hook does [#what-each-hook-does]
| Event | What happens |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `PreToolUse` | The approval gate. The kill switch and always-deny rules block here. Other gated calls go to your phone and run only if you approve. |
| `PostToolUse` | Records `tool_complete` or `tool_error` for the session. |
| `Stop` | Marks the session's turn finished in your session list. |
## Pushary is agy's only gate [#pushary-is-agys-only-gate]
In its default mode, `request-review`, agy ignores a hook's allow and keeps its own review step, so an approval from your phone cannot let the call through. That is why connecting agy sets `"toolPermission": "always-proceed"`. agy then asks nobody, and Pushary becomes the only gate. Plain `agy` works, with no flag.
Because agy has no prompt of its own in that mode, the CLI's hook asks your phone about every gated call, even while you sit at your computer. With the Mac app, the question can also show in the notch.
If you already chose a mode, such as `strict`, Pushary leaves it alone and tells you so. agy then still asks in its own prompt and ignores approvals from your phone. Set it to `always-proceed`, or start agy with `--dangerously-skip-permissions`, to hand the gate to Pushary.
| Your answer | What agy does |
| -------------------------------------- | -------------------------------------------------------------------- |
| Approve | Runs the call. |
| Deny | Blocks the call. The model sees the reason. |
| No answer before your rule's wait ends | Blocks the call with a reason, unless your rule approves on timeout. |
Pushary answers agy only with allow or deny. In always-proceed mode agy runs a hook's "ask" without prompting, so a call that would go back to the terminal with another agent is blocked here instead. For the same reason, the `pushary` hook (1.9.14 or later) treats When away like Always for agy: your phone is asked about every gated call, even while you are at the keyboard, and it waits for your policy's timeout, not the short push window.
## Check that it works [#check-that-it-works]
```bash
npx pushary@latest doctor
```
Doctor checks your key and plan, then agy's hooks and MCP server. It sends a real test push. Add `--roundtrip` to send a test question too.
Then ask agy: `Create a file called pushary-test.txt that says hello.` Your phone buzzes with the write request. Tap **Approve** and the file appears.
## What Pushary can and cannot enforce on agy [#what-pushary-can-and-cannot-enforce-on-agy]
agy's tools have their own names, so Pushary maps them onto your existing rules:
| agy tool | Pushary rule |
| ---------------------------------------------------- | ----------------------------------- |
| `run_command` | `Bash` |
| `write_to_file` | `Write` |
| `replace_file_content`, `multi_replace_file_content` | `Edit` |
| `manage_task` sending input | `Bash`, judged on the text it sends |
| `call_mcp_tool` | `mcp____` |
A rule you set for `Bash` also gates `run_command` and the text agy sends to a command it left running. Safe read-only commands such as `ls` run without a question. Sending blank input, such as pressing Enter, asks your phone under the name `TaskInput`. Listing, checking and stopping agy's own background tasks run without a question.
* **Blocks rather than guesses.** A call Pushary cannot decide, because Pushary is offline, signed out or your subscription has ended, is blocked with a reason. While Pushary is offline, agy's shell commands, edits and MCP calls stop. Reading files still works.
* **Leftover hook.** If Pushary is removed without disconnecting agy first, its approval hook still runs and blocks each call with a message that says how to undo it.
* **Not gated:** file reads (`view_file`), reading a web page (`read_url_content`) and web search. The Antigravity desktop app's browser and deploy tools are left to its own prompt.
* **Kill switch:** blocks every gated call, but cannot end the agy session itself.
* **Shared file:** `~/.gemini/config/hooks.json` is shared with the Antigravity desktop app. Pushary is tested with the CLI.
## Turn it off [#turn-it-off]
```bash
npx pushary@latest disconnect antigravity
```
This removes the `pushary` hook and the Pushary MCP server, and puts `toolPermission` back if Pushary set it. Your other hooks, servers and settings stay as they are. Turning agy off in the Mac app does the same.
## Troubleshooting [#troubleshooting]
### agy still asks in its own prompt [#agy-still-asks-in-its-own-prompt]
You chose a mode before Pushary connected, so Pushary left it alone. Change `toolPermission` to `always-proceed` with `/config` (or its alias `/settings`) inside agy, or start agy with `--dangerously-skip-permissions`. `/permissions` manages agy's allow, ask and deny rules, not this setting.
### Every call is blocked [#every-call-is-blocked]
Pushary cannot decide the call. Run `npx pushary@latest doctor` to check your key and plan. If you removed Pushary, run `npx pushary@latest disconnect antigravity` to remove the leftover hook.
### `pushary` is missing from `/hooks` [#pushary-is-missing-from-hooks]
Start a new agy session. If it is still missing, run `npx pushary@latest setup --agents antigravity_cli` again.
## Next steps [#next-steps]
---
# ChatGPT decision events
Source: https://pushary.com/docs/agents/guides/chatgpt-events
Last updated: 2026-10-04
> Wake a ChatGPT automation when one Pushary question gets its answer. Subscribe to decision.answered over MCP Events and verify each signed webhook.
MCP Events let a ChatGPT automation you authorized react when one Pushary question gets its answer. When that question is answered, Pushary sends a signed webhook to ChatGPT. The event only wakes the automation. It does not resume other tasks and it never approves an action by itself, because the answer can be a denial.
ChatGPT still chooses when to ask you a question. An event only tells it that an answer arrived.
Implemented behind `PUSHARY_MCP_EVENTS_ENABLED=true`. Apply migration `0098_mcp_event_subscriptions` first. Keep the flag off until the live ChatGPT callback check and the authorized automation flow pass. Availability also depends on the user's access to ChatGPT Events and Automations.
## Subscribe to one question [#subscribe-to-one-question]
### Connect ChatGPT [#connect-chatgpt]
Use the signed-in [ChatGPT connector](/docs/agents/guides/chatgpt). Events use the same MCP endpoint and the same sign-in.
### Ask a question and keep its id [#ask-a-question-and-keep-its-id]
Create an owner question with `ask_user`. Keep the `correlationId` it returns.
### Authorize the follow-up in ChatGPT [#authorize-the-follow-up-in-chatgpt]
In the ChatGPT automation setup, authorize the follow-up and select `decision.answered` with that `correlationId`. ChatGPT supplies its webhook URL and signing secret. The model must never invent either.
ChatGPT then sends an `events/subscribe` request like this:
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "events/subscribe",
"params": {
"name": "decision.answered",
"arguments": { "correlationId": "saved-question-id" },
"delivery": {
"mode": "webhook",
"url": "https://your-authorized-receiver.example/events",
"secret": "whsec_BASE64_ENCODED_24_TO_64_BYTE_KEY"
},
"cursor": null,
"ttlMs": 3600000
}
}
```
Pushary rejects a request that breaks these rules:
* `correlationId` is 1 to 256 characters.
* `url` is `https`, on port 443 or no port, with no username, password or `#` fragment, and at most 2,048 characters.
* `secret` is `whsec_` followed by the base64 of a 24 to 64 byte key.
The same endpoint also answers `events/list` and `events/unsubscribe`.
## Verify the callback [#verify-the-callback]
Before Pushary saves a subscription, it sends a signed verification POST with the body `{"type":"verification","challenge":"..."}`. Your receiver must answer with a 2xx JSON response that echoes `{"challenge":"the-received-challenge"}`.
Every request carries Standard Webhooks headers: `webhook-id`, `webhook-timestamp` and `webhook-signature`, plus `X-MCP-Subscription-Id`. The signature is `v1,` and a base64 HMAC-SHA256 of `webhook-id.webhook-timestamp.body`, keyed with the base64-decoded secret. Check it against the raw body, check the subscription id, and allow only a short timestamp drift. Drop event ids you have already seen.
## What an event contains [#what-an-event-contains]
The event body has `eventId`, `name`, `timestamp`, `cursor: null` and `data: { correlationId, status: "answered" }`. It does not carry the answer.
Fetch the saved answer with `wait_for_answer`. An answered question can be a denial. Check the exact action you asked about before you run anything.
## Who can subscribe [#who-can-subscribe]
Each subscription belongs to one signed-in principal, workspace, question and callback URL. Only owner questions work. Partner customer decisions and bound keys are excluded.
## Renew and change the secret [#renew-and-change-the-secret]
A subscription lives at most one day. Renew it before the `refreshBefore` time Pushary returns. Repeating a subscription refreshes its expiry and does not redeliver an event you already acknowledged.
A refresh can replace the signing secret, once the callback passes verification with the new key. For five minutes, deliveries carry signatures from both keys, then only the new key. A passed verification is cached for five minutes, and changing the secret always needs a new verification.
## Retries and failures [#retries-and-failures]
A retry cron reads the saved decision and claims a delivery lease for up to five events per run. A subscription generation number stops an old delivery from changing a replaced or re-keyed subscription. Expired subscription records are deleted.
* A 2xx response acknowledges the event.
* Other failures retry with exponential backoff, starting at 30 seconds and capped at one hour, up to eight attempts.
* HTTP 410 or 413 stops delivery.
* Expired, unsubscribed, revoked API key and removed workspace member subscriptions cannot start another delivery.
* Disconnecting an OAuth connection deletes its subscriptions in the database, even while event delivery is turned off. Reconnecting needs a new subscription.
* A request already in flight cannot be recalled.
Pushary does not keep checking whether the upstream token was revoked. The one-day limit on each subscription bounds that gap.
## What does not send an event [#what-does-not-send-an-event]
There is no history replay, no stream and no polling delivery mode. A question that was already answered, and is still kept, can send its one event after a new subscription. A question that expires or is cancelled does not send `decision.answered`.
If delivery runs out of retries, poll the question itself with `wait_for_answer`. That stays the source of truth.
## Protocol support [#protocol-support]
When the flag is on, the public `server/discover` method advertises protocol `2026-07-28`. MCP 2 tool calls use the same signed-in tool list, with complete-result envelopes and private, uncached list responses. Multi-round-trip inputs, tasks and MCP 2 resource methods are not supported. Older MCP clients keep their existing transport.
A compatibility bridge keeps the current SDK 1 and SSE integrations working. A later move to native `mcp-handler` 2 will replace this bridge and needs registration, auth context and clients migrated. You do not need that move to try these event and tool methods.
See OpenAI's [MCP Events specification](https://developers.openai.com/plugins/build/mcp-events).
## Next steps [#next-steps]
---
# ChatGPT
Source: https://pushary.com/docs/agents/guides/chatgpt
Last updated: 2026-10-12
> Connect Pushary to ChatGPT with the Pushary plugin or a custom MCP connector, so ChatGPT can notify you and ask you questions on your phone.
Pushary lets ChatGPT reach you on your phone. You connect it once, with the Pushary plugin or a custom app, and sign in to Pushary. ChatGPT can then send you a notification when a long task ends and ask you questions you answer from your lock screen. You need no terminal and nothing to install on your computer.
ChatGPT chooses when to call Pushary. Pushary cannot stop ChatGPT in the middle of a task. To approve risky commands before they run, use an agent with hooks, such as Claude Code or Codex. [Supported agents](/docs/agents/supported-agents) shows which agents Pushary can enforce.
Codex, OpenAI's agent for code, is a different and stronger integration. Codex has native hooks (scripts that run before each tool call), so Pushary can block a shell command or a patch until you approve it. See the [Codex guide](/docs/agents/guides/codex). This page is about ChatGPT itself.
## Install the Pushary plugin [#install-the-pushary-plugin]
This is the shortest path. Whether ChatGPT offers the plugin depends on your account and workspace settings.
### Open the plugin [#open-the-plugin]
Open [Pushary in ChatGPT](https://chatgpt.com/plugins/plugin_asdk_app_6abc2ebca100819198c8619e9a697ee3) and choose **Install plugin**.
### Connect and sign in [#connect-and-sign-in]
When ChatGPT asks, connect Pushary. Sign in with the same Pushary account your phone uses.
### Send a test question [#send-a-test-question]
Mention @Pushary in a chat and ask it to send a test question to your phone. Answer it in Pushary. ChatGPT gets your answer, which confirms the connection.
The same button, **Open Pushary in ChatGPT**, is on the **Claude and ChatGPT** card in [Settings > Connections](https://pushary.com/dashboard/agent/settings).
## Add Pushary as a custom app (Developer mode) [#add-pushary-as-a-custom-app-developer-mode]
Use this path if the plugin is not available to you, or your workspace prefers its own app.
Pushary's tools write (they send notifications and questions). ChatGPT allows custom apps with write tools on **ChatGPT Business, Enterprise and Edu**, on the web. A workspace admin or owner has to turn on Developer mode. Enterprise and Edu can give access to selected developers. Pro supports read and fetch tools only, and Plus cannot use this setup.
This is the URL for the custom app. It holds no credential, because ChatGPT signs you in to Pushary when it connects:
```text
https://pushary.com/api/mcp/openai/mcp
```
### Turn on Developer mode [#turn-on-developer-mode]
Ask a workspace admin or owner to turn it on under **Workspace settings > Permissions & Roles > Connected Data**. If needed, turn it on for your account in **Settings > Apps > Advanced settings**. Business admins and owners can also turn it on while creating an app.
### Create the app [#create-the-app]
In **Workspace settings > Apps**, choose **Create**. Authorized Enterprise and Edu developers can use **Settings > Apps > Create**.
### Paste the URL [#paste-the-url]
Name the app `Pushary`. Paste the URL above into the **MCP Server URL** field.
### Sign in and scan the tools [#sign-in-and-scan-the-tools]
Choose **OAuth** for authentication, then select **Scan Tools**. Sign in with the Pushary account that owns your devices.
### Finish [#finish]
Wait for the tool scan to finish, then select **Create**.
Use the `/api/mcp/openai/mcp` URL exactly. It is the Streamable HTTP endpoint for ChatGPT. Do not swap the ending for `/sse`.
If OAuth fails, install the [Pushary plugin](https://chatgpt.com/plugins/plugin_asdk_app_6abc2ebca100819198c8619e9a697ee3) instead, and sign in with the same Pushary account.
## Turn on Pushary in a chat [#turn-on-pushary-in-a-chat]
Custom apps are not always on. In a new chat, open the tools menu (the **+** or the tools icon in the composer) and turn on **Pushary** for that conversation.
## Make ChatGPT reach out on its own [#make-chatgpt-reach-out-on-its-own]
ChatGPT does not call a connector unless something tells it to. Paste this block into **Settings**, then **Personalization**, then **Custom instructions**. ChatGPT then pings you without being asked each time.
```text
Pushary is connected as a custom connector. Use it proactively to reach me on my phone. Do not wait for me to ask.
- When you need a decision or a clarifying answer, call ask_user instead of guessing or stalling. Use type "confirm" for yes or no, "select" for a fixed set of options, and "input" for free text. Ask once, on one channel.
- Before a risky or irreversible step outside what I already approved (spending money, sending anything external, bulk changes), call ask_user with type "confirm" and wait for my answer. Silence is never consent.
- When a long task finishes, or you are blocked by an error you cannot resolve, call send_notification with a short summary. Set context.type to "task_complete", "error" or "info".
- For a specific decision that can wait while I am away, call send_notification with context.askQuestion, then poll the returned linkedCorrelationId with wait_for_answer.
- Follow handoffAction when present, otherwise nextAction. While pending, keep waiting with the same correlationId. Only for an explicit handoff, call cancel_question before asking me in this chat. If cancellation returns handoffAction "stop", stop. If cancellation returns false, poll once for 1 second and honor the answer that won the race.
- Pass agentName as "ChatGPT - " on every call so I know which chat is asking.
- Use cancel_question to retract a question that is no longer needed.
```
With the agentName in place, ChatGPT sessions show up in your Activity feed and Fleet view as **ChatGPT**, grouped by task.
ChatGPT may ask you to confirm a write action, including `send_notification` or `ask_user`, based on app permissions and the action. That is ChatGPT's own safety check, not a Pushary setting.
## Tools ChatGPT gets [#tools-chatgpt-gets]
The connector gives ChatGPT five tools: `send_notification`, `ask_user`, `propose_scope`, `wait_for_answer` and `cancel_question`. See the [tools reference](/docs/agents/reference/tools). Those five are all ChatGPT sees, so it is never offered tools that would only refuse it. The session, subscriber, campaign and flow tools need an API key and are not offered on a connector.
A question blocks for up to about 55 seconds per call. If you need longer, ChatGPT can check again with `wait_for_answer` and the same question id. Questions stay open for 10 minutes.
### Prompts to try [#prompts-to-try]
**Get pinged when a long task finishes:**
```text
When you finish this research, use Pushary to send me a notification with a
short summary of what you found.
```
**Ask before something irreversible:**
```text
Before you do anything irreversible, use Pushary to ask me to confirm, and wait
for my answer before continuing.
```
## Disconnect ChatGPT [#disconnect-chatgpt]
Remove Pushary under ChatGPT's **Apps** settings to stop ChatGPT using the sign-in connection. If you pasted a key link instead, revoke it in Pushary under Settings > Connections, behind **Client cannot sign in? Use a key link instead**. The old key link stops working right away.
## Troubleshooting [#troubleshooting]
### The app will not save, or ChatGPT says it is not safe [#the-app-will-not-save-or-chatgpt-says-it-is-not-safe]
Check that the URL is exactly `https://pushary.com/api/mcp/openai/mcp` and that you chose OAuth. If OAuth still fails, open the [Pushary plugin in ChatGPT](https://chatgpt.com/plugins/plugin_asdk_app_6abc2ebca100819198c8619e9a697ee3) and choose **Install plugin**. When ChatGPT asks, connect Pushary and sign in with the same Pushary account. If it still refuses, send the exact error to [aadil@pushary.com](mailto:aadil@pushary.com).
### Developer mode is missing [#developer-mode-is-missing]
Custom apps with write tools need Business, Enterprise or Edu on the web, turned on by a workspace admin. Without it, try the [Pushary plugin in ChatGPT](https://chatgpt.com/plugins/plugin_asdk_app_6abc2ebca100819198c8619e9a697ee3). Availability depends on your account and workspace settings.
### ChatGPT never calls Pushary [#chatgpt-never-calls-pushary]
Turn on Pushary in the chat's tools menu, and add the custom instructions block above. Without it, ChatGPT uses the connector only when you ask for it by name.
### The notification reached 0 devices [#the-notification-reached-0-devices]
The `send_notification` result warns "Notification reached 0 devices" when no phone or browser is connected for your workspace. With the Mac app connected it warns "No phone or browser received this notification" instead. Connect a phone or browser first. See [Receiving notifications](/docs/agents/receiving-notifications).
## Next steps [#next-steps]
---
# Claude Code setup
Source: https://pushary.com/docs/agents/guides/claude-code
Last updated: 2026-10-12
> Set up Claude Code notifications and approvals in one command. Approve risky commands, file edits and Claude's own questions from your phone or Mac.
Pushary sends Claude Code notifications, permission prompts and questions to your phone, so you can approve `rm -rf ./dist` or pick an auth strategy while you are away from your desk. One command installs Claude Code hooks (small programs Claude Code runs around each tool call) and the Pushary MCP server. A gated action waits for your answer before it runs.
## Set it up [#set-it-up]
```bash
npx pushary@latest setup
```
On Windows PowerShell, run `npx.cmd pushary@latest setup`. The CLI needs Node.js 20.17+, 22.13+ or 23.5+.
### Scan the QR code with your phone [#scan-the-qr-code-with-your-phone]
Setup shows a QR code first. Scan it with your phone's camera or the Pushary app. Sign in or create an account, check that the code on your phone matches the one in your terminal, then tap **Approve** on **Connect this computer?** and allow notifications.
New accounts start a 3-day free trial, then pay $9.99 a month. A card is needed to start the trial. If this computer already has a Pushary key, setup reuses it and skips this step.
### Pick Claude Code [#pick-claude-code]
Setup asks **Which agents do you use?** with the agents it found already selected. Keep Claude Code selected. To skip the question, run `npx pushary@latest setup --agents claude_code`.
### Answer the test question [#answer-the-test-question]
Setup sends a test push, then a test question. Answer it on your phone. The terminal says **You're set** once your answer arrives.
### Restart Claude Code [#restart-claude-code]
Claude Code reads hooks when it starts. Quit every open session and start a new one.
On a Mac you can use [Pushary Isle](/docs/agents/mac-app) instead. It connects Claude Code for you and needs no Node.
## What setup changes on your computer [#what-setup-changes-on-your-computer]
| File | What changes |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `~/.claude.json` | Adds the `pushary` MCP server (`https://pushary.com/api/mcp/mcp`) with your key. The file is written readable by you only. |
| `~/.claude/settings.json` | Adds the Pushary hooks and allows `mcp__pushary__*`, so Pushary's own tools run without a prompt. |
| `~/.claude/skills/pushary/SKILL.md` | The Pushary skill, which tells Claude Code when to notify you and when to ask. |
| `~/.pushary/config.json` and your shell profile | Your API key, as `PUSHARY_API_KEY`. |
| `CLAUDE.md` in the current folder | Only if you say yes when asked. A section that tells agents in this repo to ask through Pushary. |
Setup also installs the `pushary` package globally, so each hook starts fast.
### The hooks that decide [#the-hooks-that-decide]
| Hook | What it does |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PreToolUse` | The approval gate. Runs before `Bash`, `Write`, `Edit`, `MultiEdit`, `NotebookEdit`, `PowerShell` and `Monitor`, and before Claude's own questions (`AskUserQuestion`) and plan approval (`ExitPlanMode`). |
| `PermissionRequest` | Runs when Claude Code is about to show its own permission prompt, for any tool. Your approval mode decides whether it goes to your phone. |
| `PermissionDenied` | Runs when auto mode blocks an action. Pushary asks you, and a yes lets Claude Code retry it once. |
| `Elicitation` | Sends an MCP server's request for input to your phone. |
These four wait up to 600 seconds for your answer.
Twelve more hooks only record activity for your audit log and session list: `PostToolUse`, `Stop`, `UserPromptSubmit`, `Notification`, `SessionStart`, `SessionEnd`, `StopFailure`, `SubagentStart`, `SubagentStop`, `PreCompact`, `PostCompact` and `TeammateIdle`. Each times out after 10 seconds. Claude Code builds older than 2.0, and versions setup cannot detect, get fewer of them.
`UserPromptSubmit` saves the start of your prompt as the session's task title. Set `PUSHARY_TASK_TITLES=off` to turn that off.
Each notification names its session as `Claude Code - `, so you can tell parallel sessions apart.
## Check that it works [#check-that-it-works]
```bash
npx pushary@latest doctor
```
Doctor checks your key and plan, then Claude Code's MCP server, hooks, auto-allowed tools and skill. It sends a real test push. Add `--roundtrip` to send a test question too.
Then try a real approval. A new workspace starts on **Always**, so it reaches your phone while you sit at your computer. If yours is on When away, decisions stay in Claude Code's own prompt while you are there. To test from your desk, open the Pushary app, tap **Questions & approvals** at the top of your inbox, and pick **Always**.
Ask Claude Code: `Create a file called pushary-test.txt that says hello.` Your phone buzzes with the Write request. Tap **Approve** and the file appears. Switch back to **When away** when you are done.
## What Pushary can and cannot enforce on Claude Code [#what-pushary-can-and-cannot-enforce-on-claude-code]
* **Gated before they run:** shell commands (`Bash`, `PowerShell`, `Monitor`) and file changes (`Write`, `Edit`, `MultiEdit`, `NotebookEdit`).
* **Asked in every permission mode:** Claude's questions (`AskUserQuestion`) and plan approval (`ExitPlanMode`), because they are questions, not permissions.
* **Only when Claude Code would ask:** any other tool, including reads, web fetches and other MCP servers' tools, reaches your phone only through Claude Code's own permission prompt (`PermissionRequest`).
* **Your own allow rules:** a command your Claude Code allow rules already permit runs without a Pushary question, unless a rule you wrote in Pushary covers it. Pushary reads allow rules from your user, local and managed settings. It ignores allow rules in a repo's committed `.claude/settings.json`, so a cloned repo cannot switch Pushary off.
### Claude Code's permission mode decides first [#claude-codes-permission-mode-decides-first]
| Claude Code mode | What Pushary does |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `default` | Gates as described above. |
| `acceptEdits` | Steps aside for edits, which Claude Code already accepts. Shell commands still gate. |
| `plan`, `auto`, `dontAsk` | Steps aside, because Claude Code already owns these decisions. In `auto`, a call the classifier sends to a prompt still reaches you through `PermissionRequest`, and a blocked call through `PermissionDenied`. |
| `bypassPermissions` | Keeps gating. Claude Code asks nothing, so Pushary is the only check. |
The kill switch, your deny rules and a scope you approved apply in every mode. Two environment variables change the table:
* `PUSHARY_RESPECT_PERMISSION_MODE=0` gates in every mode, whatever Claude Code's mode is.
* `PUSHARY_DEFER_BYPASS=1` steps aside in `bypassPermissions` too.
### Run --dangerously-skip-permissions and still approve from your phone [#run---dangerously-skip-permissions-and-still-approve-from-your-phone]
`claude --dangerously-skip-permissions` starts Claude Code in `bypassPermissions`. Claude Code then runs every tool without a prompt, but Pushary's hooks still run. Risky commands and edits still wait for your phone, the kill switch still blocks the next gated call, and every decision lands in your audit log. See [Claude Code dangerously skip permissions](/claude-code-dangerously-skip-permissions) for what the flag turns off.
### Choose what happens when you do not answer [#choose-what-happens-when-you-do-not-answer]
Each rule has a timeout and an **If you don't respond** action. A new workspace starts with one rule for every tool (`*`): wait 60 seconds, then **Ask in terminal**. That hands the call back to Claude Code's own prompt, so nothing runs without an answer.
Pushary also ships built-in rules, but your own `*` rule outranks them. They only apply to a workspace that has no `*` rule:
| Tool | Timeout | If you don't respond |
| --------------- | ------- | ---------------------------- |
| `Bash` | 60s | Auto deny |
| `Write` | 60s | Ask in terminal |
| `Edit` | 45s | Ask in terminal |
| `Read` | 0s | Auto approve, without asking |
| Everything else | 60s | Ask in terminal |
A rule you write for one tool, such as `Bash`, beats your `*` rule. The other choices are **Auto approve**, **Auto deny** and **Wait for phone**. Change them at [pushary.com/dashboard/agent/policies](https://pushary.com/dashboard/agent/policies).
## Set it up by hand [#set-it-up-by-hand]
Use setup if you can. It writes up to 16 hook events (fewer on Claude Code builds older than 2.0) and pins each command to its full path. The minimum for approvals and activity looks like this.
Install the hook commands globally, unless `which pushary-hook` already prints a path. An existing install under `@pushary/agent-hooks` provides the same commands, and npm refuses to install over them.
```bash
npm install -g pushary
```
Add the MCP server to `~/.claude.json`:
```json
{
"mcpServers": {
"pushary": {
"type": "http",
"url": "https://pushary.com/api/mcp/mcp",
"headers": {
"Authorization": "Bearer pk_xxx.xxx"
}
}
}
}
```
Add the hooks and the tool permission to `~/.claude/settings.json`:
```json
{
"permissions": {
"allow": ["mcp__pushary__*"]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Write|Edit|MultiEdit|NotebookEdit|PowerShell|Monitor|AskUserQuestion|ExitPlanMode",
"hooks": [{ "type": "command", "command": "pushary-hook", "timeout": 600 }]
}
],
"PermissionRequest": [
{
"hooks": [{ "type": "command", "command": "pushary-permission-hook", "timeout": 600 }]
}
],
"PostToolUse": [
{
"matcher": "Bash|Write|Edit|MultiEdit|NotebookEdit|PowerShell|Monitor|TodoWrite|Task|WebFetch|WebSearch",
"hooks": [{ "type": "command", "command": "pushary-post-hook", "timeout": 10 }]
}
],
"Stop": [
{
"hooks": [{ "type": "command", "command": "pushary-stop-hook", "timeout": 10 }]
}
]
}
}
```
Save your key in your shell profile:
```bash
export PUSHARY_API_KEY="pk_xxx.xxx"
```
Restart Claude Code, then run `npx pushary@latest doctor`. It reports any hook event that is missing.
The Pushary skill is optional. Setup installs it for you. By hand:
```bash
npx skills add Pushary/pushary-skill
```
## Verify a real approval [#verify-a-real-approval]
A test push, or `npx pushary@latest doctor --roundtrip`, proves that delivery works and that your answer comes back. It does not run Claude Code's hooks. The Mac app's phone test does not either. This check proves a real approval reached Claude Code.
### Restart Claude Code with your normal permissions [#restart-claude-code-with-your-normal-permissions]
Finish sign-in and checkout, connect Claude Code, then restart it so it reads the installed hooks. If your current mode auto-allows the action, or keeps approval in the terminal, the check cannot prove a phone answer was used.
### Make a new target path and approve one Write [#make-a-new-target-path-and-approve-one-write]
In an empty scratch folder, in the terminal where you will run the check:
```bash
TARGET="$PWD/pushary-activation-$(node -p 'require("node:crypto").randomUUID()').txt"
printf '%s\n' "$TARGET"
```
Ask Claude Code: **"Use the Write tool to create this exact absolute path, containing `Pushary activation check`. Do not run other commands or answer any approval yourself."** Paste the printed path. Approve the Write yourself on your chosen surface. Check that the file appears with that text.
### Read the evidence [#read-the-evidence]
```bash
npx pushary@latest doctor --activation claude_code --activation-target "$TARGET" --no-push --json
```
It reads the Write decision for that exact path from the last 10 minutes, on this machine and workspace. Save the decision, session and tool-call IDs, the answer source and transport, with `claude --version`, `pushary --version`, and the Mac app version if you use it. The evidence excludes credentials and file contents.
### Repeat with a denial [#repeat-with-a-denial]
Make another new target and repeat, denying the request this time. Check that the file was not created.
### Restart and run a negative control [#restart-and-run-a-negative-control]
Restart the agent and repeat with a new target. For the negative control, remove only the Pushary hooks in an isolated test profile and use another new target. A test push may still pass, but that attempt must not produce matching hook evidence. Restore the test profile afterwards.
Never reuse a target. Its old approval does not test your current configuration.
| Result | What it means |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `approved_executed` | You approved, and Claude Code reported the Write completed after your answer. |
| `denied_observed` | You denied, and Claude Code reported the denial. Some host paths send no denial event, and those stay `unverified`. A missing completion event alone never proves a denial. |
| `unexpected_execution` | Claude Code reported the Write completed after a denial. Investigate it. |
| `tool_failed` | Claude Code reported a tool error. |
| `unverified` | No proof either way: an unknown answer source, a synthetic question, a duplicate decision, a reused target, missing hooks, stale evidence, or client clocks that are missing or disagree. |
The check never creates a question or changes permissions. Queued events keep their original runtime timestamp. Results are what the agent reported at runtime, not independently attested execution. A pass covers that one invocation, not every agent or future request.
If no approval appeared, check Claude Code's permission mode and your Pushary approval mode before you repair anything. If the CLI owns your hooks, run `npx pushary@latest doctor`. If the Mac app owns them, use its agent setup and recovery controls. Keep the existing owner.
To cancel a test, cancel it in the agent or on its approval surface. The hook owns its own deadline. Stopping the read-only check does not cancel a pending agent request.
## Troubleshooting [#troubleshooting]
### Why did my phone not buzz? [#why-did-my-phone-not-buzz]
* You did not restart Claude Code after setup. Hooks load at start.
* You are at your computer in When away mode, so the decision stays in Claude Code's prompt. To test, set **Questions & approvals** to **Always** in the app.
* Claude Code is in `plan`, `auto`, `dontAsk` or `acceptEdits` mode, which Pushary respects. See the table above.
* Your Claude Code allow rules already permit the command.
* Run `npx pushary@latest doctor` to check the key, the plan and each hook.
### `which pushary-hook` prints nothing [#which-pushary-hook-prints-nothing]
Run `npx pushary@latest setup` again. It installs the hook commands globally.
### Setup says Pushary for Mac handles Claude Code's approvals [#setup-says-pushary-for-mac-handles-claude-codes-approvals]
The Mac app already owns Claude Code's hooks. Setup reuses the app's key and keeps the app's hooks. To have the CLI own them instead, run `npx pushary@latest setup --take-over-hooks`.
### More help [#more-help]
* [Claude Code notifications not working](/claude-code-notifications-not-working): fixes for hooks and alerts that stay silent.
* [Claude Code hooks explained](/blog/claude-code-hooks-explained): every hook event and what it can return.
* [Control Claude Code from your phone](/control-claude-code-from-phone): approve, answer or stop a session away from the terminal.
## Next steps [#next-steps]
---
# Claude.ai, Claude Desktop, and Cowork
Source: https://pushary.com/docs/agents/guides/claude-desktop
Last updated: 2026-10-08
> Get Claude Desktop notifications on your phone. Paste one Pushary link as a custom connector in claude.ai, Claude Desktop or Cowork, then sign in.
Pushary sends Claude Desktop notifications to your phone. You add one link as a custom connector in claude.ai, the Claude Desktop app, Cowork or the Claude mobile apps, then sign in to Pushary. Claude can then tell you when a task finishes and ask you questions you answer from your lock screen. There is nothing to install and no config file to edit.
This is the connector URL. It holds no password or key, because Claude signs you in:
```text
https://pushary.com/api/mcp/mcp
```
Claude chooses when to call Pushary. The connector does not catch Claude's own permission prompts, so it cannot hold an action until you approve it. To approve risky commands before they run, use an agent with hooks, such as [Claude Code](/docs/agents/guides/claude-code). [Supported agents](/docs/agents/supported-agents) shows which agents Pushary can enforce.
## Set up Claude Desktop notifications [#set-up-claude-desktop-notifications]
A connector is how Claude plugs in an outside service over MCP (Model Context Protocol, the standard way AI apps call outside tools). The same connector works in Chat on the web, desktop and mobile, and in Cowork.
### Copy the connector URL [#copy-the-connector-url]
Copy `https://pushary.com/api/mcp/mcp` from the block above. Pushary also shows it in [Settings > Connections](https://pushary.com/dashboard/agent/settings), on the **Claude and ChatGPT** card, with the account you need to sign in with.
### Open Connectors in Claude [#open-connectors-in-claude]
In Claude, open **Customize**, then **Connectors**. In some Claude apps it sits under **Settings** instead. Click **Add custom connector**.
### Paste the URL and add it [#paste-the-url-and-add-it]
Paste the URL into the **Remote MCP server URL** field. Leave the OAuth Client ID and Secret fields empty. Claude registers itself with Pushary, and those fields are only for sign-in servers that cannot do that. Name it `Pushary` and click **Add**.
### Sign in to Pushary [#sign-in-to-pushary]
Sign in with the Pushary account that owns your workspace. Claude often asks you to sign in the first time it calls a Pushary tool, not the moment you add the connector.
### Send a test question [#send-a-test-question]
Turn on Pushary in a conversation and paste this prompt:
```text
Use Pushary ask_user to ask “Did this setup question reach you?” with type “confirm”. Use agentName “Claude Cowork - Setup check” in Cowork or “Claude Chat - Setup check” in Chat. Wait for my answer and tell me which answer you received. Do not run a command or change any files.
```
The question shows on your phone, or in the Mac notch if Pushary Isle is running. Answer it. Claude tells you which answer it received.
**Sign in with the Pushary account that owns your workspace.** The URL carries no credential, so the account you pick at the sign-in screen is the one the connector speaks for. If your browser is signed in to more than one Pushary account, the screen accepts the wrong one too. Every tool call then fails with a generic authorization error. The **Claude and ChatGPT** card in Settings > Connections names the account to use.
Until Claude makes its first call, the card says **Not used yet** or **Waiting for the first handshake**. That is expected. After the test, it shows **Connected by sign-in as** and your email.
## Use it in Cowork [#use-it-in-cowork]
Connectors belong to your Claude account. The connector you added above is also available to Cowork tasks in the desktop app.
### Open a Cowork session [#open-a-cowork-session]
Start or open a Cowork session in the Claude desktop app.
### Open Connectors [#open-connectors]
Open **Customize** (or the **+** icon in the chat bar), then **Connectors**.
### Turn on Pushary [#turn-on-pushary]
Turn on **Pushary** for the session.
### Give Cowork standing instructions [#give-cowork-standing-instructions]
Cowork runs long tasks, often while you are away. The connector helps most when Claude uses it without being asked. In the Claude app, open **Settings**, then **Cowork**, and paste this block into your standing instructions.
Put this guidance in Cowork's standing instructions or in a Pushary plugin. Do not rely on memory files in a repository for it.
```text
Pushary is connected as a custom connector. Use it proactively to reach me on my phone or Mac notch.
- When you need a decision or clarifying answer, call ask_user. Use type "confirm", "select", or "input" as appropriate. Ask once on one channel.
- Before a risky or irreversible action outside my existing authorization, call ask_user with type "confirm" and wait for approval. Silence is never consent.
- When a task finishes, call send_notification with context.type "task_complete" and a short summary. For an error you cannot resolve, use context.type "error"; use "info" for other updates.
- Only keep a reply channel open when a specific unresolved decision is required: call send_notification with context.askQuestion, then poll the returned linkedCorrelationId with wait_for_answer. Do not ask for optional feedback just to keep a finished turn alive.
- Follow handoffAction when present, otherwise nextAction. While pending, keep waiting with the same correlationId. Only for an explicit handoff, cancel_question before asking in the current chat. If cancellation returns handoffAction "stop", stop. If cancellation returns false, poll once for 1 second and honor the answer that won the race. Expired, cancelled, missing, and unavailable questions are not new timeouts.
- Pass agentName as "Claude Cowork - " on every call, and reuse one opaque sessionId per task so parallel tasks stay separate. Never include credentials in these identifiers.
- Cancel a question when it is no longer needed. Once this turn ends, reopen Cowork for follow-up work; this connector cannot restart it.
- If Pushary tools are missing or disabled, report that once and continue within my authorization. Do not claim a notification or approval was delivered.
```
The same block has a copy button in Settings > Connections, under **Cowork instructions and setup test** on the **Claude and ChatGPT** card. With the agentName in place, Cowork sessions show up in your Activity feed and Fleet view as **Claude Cowork**, grouped by task.
Cowork may ask you to approve connector tool calls inside the session. Depending on your version, it may not offer "Allow always" for custom connectors. If Pushary calls keep prompting you, keep the standing instructions to the moments that matter: risky steps and finished tasks.
### Long, unattended sessions [#long-unattended-sessions]
One `wait_for_answer` call waits about 55 seconds. A question stays answerable for 10 minutes. Cowork sessions can run much longer than that while you are away. Two patterns cover it:
* **A decision Claude needs soon:** call `ask_user`, then keep calling `wait_for_answer` with the same correlation ID while pending. Only move it to the current chat for an explicit handoff, cancelling the live question first.
* **A decision that can wait:** call `send_notification` with `context.askQuestion`, then poll the returned `linkedCorrelationId` while the task is active. For updates that need no answer, call `send_notification` without a question.
### What your phone can do with a Cowork session [#what-your-phone-can-do-with-a-cowork-session]
The connector is an outbound channel. Cowork reaches your phone when it decides to, and you answer from the lock screen. Nothing flows back into a Cowork session on its own, because the connector has no command consumer or task-launch API.
You can:
* Answer any question Cowork asked: confirm, select or free text.
* Answer later from the notification page, for a question sent with `context.askQuestion`.
You cannot:
* **Reply to a finished task.** Once Cowork's turn ends, nothing in that session is listening. To leave room for a reply, Cowork has to keep a question open before it stops: `send_notification` with `context.type` set to `task_complete` and `context.askQuestion` set to an `input` question, then `wait_for_answer` on the returned `linkedCorrelationId`. Do this only for a specific unresolved decision, not for optional feedback.
* **Start a new Cowork task from your phone.** Messaging an agent and starting one are features of the Pushary CLI, which picks them up on your own computer. The connector has nothing that picks up queued commands, whether the Cowork task runs locally or in the cloud. The Pushary phone app hides both controls on a Cowork session, so nothing gets queued that would never arrive.
To message or start an agent from your phone, use an agent the CLI sets up on your computer with `npx pushary@latest setup`. Starting a new task from the phone works with Claude Code, Codex and Gemini CLI.
## Tools Claude gets [#tools-claude-gets]
The connector gives Claude five tools: `send_notification`, `ask_user`, `propose_scope`, `wait_for_answer` and `cancel_question`. See the [tools reference](/docs/agents/reference/tools). Those five are all Claude sees on a connector. The session, subscriber, campaign and flow tools need an API key and are not offered here.
A question blocks for up to about 55 seconds per call. If you need longer, Claude can check again with `wait_for_answer` and the same question id. Questions stay open for 10 minutes.
### Prompts to try [#prompts-to-try]
**Get pinged when a long task finishes:**
```text
When you finish this task, use Pushary to send me a notification with a short
summary of what changed.
```
**Ask before something irreversible:**
```text
Before you do anything destructive or irreversible, use Pushary to ask me to
confirm, and wait for my answer before continuing.
```
## Use a key link if Claude cannot sign in [#use-a-key-link-if-claude-cannot-sign-in]
Settings > Connections still offers a link with a key in it, behind **Client cannot sign in? Use a key link instead**. Click **Get a key link** to create one. It is for a client that cannot complete a sign-in. claude.ai rejects this kind of link, so use the plain URL there.
A key link expires after 90 days. The card warns you in the last 14 days. Treat it like a password: anyone with it can send you notifications until you revoke it.
## Disconnect or move the connector [#disconnect-or-move-the-connector]
To remove access, delete Pushary under **Connectors** in Claude. The URL stays the same if you add it again later.
To move a sign-in connection to a different workspace, click the trash button next to **Connected by sign-in as** in Settings > Connections and confirm **Detach from this workspace**. Claude stays signed in and picks a workspace on its next request.
For a key link, open **Client cannot sign in? Use a key link instead**. Click **Generate new key link** to replace it, or click the trash button and confirm **Revoke link**. The old link stops working right away. Paste the new link into Claude.
## Install with the Pushary plugin [#install-with-the-pushary-plugin]
The [Pushary for Claude plugin](https://github.com/Pushary/claude-plugin) bundles the shared skill and the same sign-in connector for Claude Chat and Claude Code. In Chat, open **Customize > Plugins > Add > Add marketplace**, add the plugin's GitHub URL and install **Pushary for Claude**. Then connect Pushary from the plugin's **Connectors** tab.
In Claude Code, run these two commands, then use `/mcp` to check the sign-in:
```text
/plugin marketplace add Pushary/claude-plugin
/plugin install pushary@pushary-claude-plugin
```
This plugin gives questions and notifications. Approval hooks for Claude Code come from the separate [Claude Code integration](https://github.com/Pushary/pushary-skill).
For Cowork, the [Pushary for Cowork plugin](https://github.com/Pushary/cowork-plugin) is still available under its original repository and install names. Add that repository as the marketplace, install **Pushary for Cowork**, and connect Pushary from its **Connectors** tab. Both plugins use the same connector and shared skill. Pick one per app, or you get duplicate connectors.
If your Claude version does not add the connector from the plugin, add the URL above by hand.
### Get the setup text from the CLI [#get-the-setup-text-from-the-cli]
```bash
npx pushary@latest cowork
```
This prints the connector URL, the same steps and the test prompt. `npx pushary@latest setup --agents cowork` does the same, without writing CLI hooks or saving a key. Add `--instructions` to print the standing instructions, or `--skill` to print the skill file to upload (for example `npx pushary@latest cowork --skill > SKILL.md`). `doctor` does not check a connector that lives in your Claude account.
Anthropic documents [Cowork plugin hooks](https://support.claude.com/en/articles/13837440-use-plugins-in-claude). This connector does not install or check native permission hooks. Local and cloud tasks can have different capabilities. Neither a skill nor a working MCP connection proves that Claude's own approval prompts are intercepted.
## Why did the test question not arrive? [#why-did-the-test-question-not-arrive]
Check these in order:
1. You signed in with the Pushary account named on the **Claude and ChatGPT** card.
2. Pushary is turned on in this conversation or Cowork session.
3. You allowed the tool call if Claude asked.
4. Your approval mode in Pushary sends questions where you expect them. In In agent mode, nothing goes to your phone.
The test passes when Claude reports the answer you gave. A printed setup link does not count.
## Next steps [#next-steps]
---
# Codex setup
Source: https://pushary.com/docs/agents/guides/codex
Last updated: 2026-10-13
> Set up Codex notifications and phone approvals in one command. Native Codex hooks gate shell commands, apply_patch edits and MCP calls before they run.
Pushary connects Codex to your phone through native Codex hooks (small programs Codex runs around each tool call) and the Pushary MCP server. Codex shell commands, `apply_patch` edits and MCP calls go through your Pushary policy, and the ones that need a person wait for your answer. It works with the Codex CLI and with the Codex inside the ChatGPT desktop app.
## Set it up [#set-it-up]
```bash
npx pushary@latest setup
```
On Windows PowerShell, run `npx.cmd pushary@latest setup`. The CLI needs Node.js 20.17+, 22.13+ or 23.5+.
### Scan the QR code with your phone [#scan-the-qr-code-with-your-phone]
Setup shows a QR code first. Scan it with your phone's camera or the Pushary app. Sign in or create an account, check that the code on your phone matches the one in your terminal, then tap **Approve** on **Connect this computer?** and allow notifications.
New accounts start a 3-day free trial, then pay $9.99 a month. A card is needed to start the trial. If this computer already has a Pushary key, setup reuses it and skips this step.
### Pick Codex [#pick-codex]
Setup asks **Which agents do you use?** with the agents it found already selected. Keep Codex selected. To skip the question, run `npx pushary@latest setup --agents codex`.
### Trust the hooks, if setup asks [#trust-the-hooks-if-setup-asks]
Codex runs a hook only after you trust it. Setup writes the trust entries for you and asks Codex to confirm them, so there is usually nothing to do.
If setup prints **One step left, and it happens inside Codex**, open Codex, type `/hooks` and trust the Pushary hook commands. In the ChatGPT desktop app, open Settings, then **Hooks**. Until you do, Codex runs without phone approvals. Pushary updates keep the same trust, so Codex does not ask again.
### Answer the test question [#answer-the-test-question]
Setup sends a test push, then a test question. Answer it on your phone. The terminal says **You're set** once your answer arrives.
### Restart Codex [#restart-codex]
Codex reads hooks when a session starts. Start a new session.
Native hooks need Codex 0.122 or newer. On an older Codex, setup installs the legacy notify handler instead, which sends notifications only. Upgrade Codex and run setup again to get approvals.
On a Mac you can use [Pushary Isle](/docs/agents/mac-app) instead. It connects Codex for you and needs no Node.
## When the Codex app asks to use an app on your Mac [#when-the-codex-app-asks-to-use-an-app-on-your-mac]
When Codex wants to control an app on your Mac, the Codex app shows a card such as "Allow Computer Use to use Calculator?". Hooks never see that card. An experimental switch sends it to Pushary as well.
With the Pushary Mac app, version 0.1.101 or later, the switch is in Settings, under Agents: "Codex app", "Ask me when Codex wants to use an app". It is on unless you turn it off. The app turns it on once you are signed in on a paid plan and it finds the Codex app, and repairs it after a Codex update. Quit Codex and open it again after it changes.
The Mac app's login item runs Pushary's own program, so macOS treats it as part of Pushary. If macOS does not start it when you log in, for example because it is turned off under Login Items in System Settings, Pushary leaves it that way and the row in Settings says so.
Without the Mac app, turn it on once from a terminal:
```bash
pushary codex --app-cards on
```
Then quit Codex and open it again. From then on the Codex app starts its engine through a small Pushary pass-through every time you open it, including after a restart. `pushary codex --app-cards status` says whether it is on, and `pushary codex --app-cards off` turns it off. It uses the CLI's own account, so run `pushary setup` first. The switch in the Mac app and the command are the same switch: either one shows what the other did, and the Mac app does not turn back on what you turned off with the command.
To try it for one launch without changing anything that lasts, quit Codex and run `pushary codex --desktop`. Keep that terminal open; closing Codex ends it.
You get one question: "Allow Codex to use Calculator for this conversation?" Yes allows that app for that conversation only, and No denies it. If you answer the card in Codex first, the Pushary question is withdrawn. "Always allow" is only ever offered in Codex. The question follows your delivery mode like any other: in Terminal mode nothing is sent and the card waits in Codex. Pushary waits ten minutes for an answer, then withdraws the question and leaves the card in Codex.
From CLI 1.9.59, the same switch also carries three questions from Codex's browser: access to a site, a download and an upload. Each names the site. A download or upload question also carries the address up to the end of its path. Nothing after a `?` or `#` is in that question, and the address ends in … when something was left out. The path itself is sent, so a link that keeps a token in its path sends it. Codex shows the whole address. For a download or an upload, Yes allows that one transfer. For a site, Yes allows it for the rest of the conversation, or for the turn if Codex is set to ask each turn, and No blocks it for the conversation, as Codex's own buttons do. "Always allow" and "Allow for all sites" stay in Codex, and so do the other Browser use questions, such as Developer Tools access, verification codes and sign-in. In the Mac app this needs the build that follows 0.1.102.
Pushary only asks when the card comes from Codex's own Computer Use or its own browser and says exactly what Codex shows. A card from any other tool, or one worded differently, stays in Codex.
Commands, file edits and Codex's own questions are not touched by this launch. The hooks above keep handling them. If Pushary cannot be reached or this computer is signed out, the card stays in Codex.
### What the switch changes on your computer [#what-the-switch-changes-on-your-computer]
| What | Where |
| ----------------------------------------------------- | ---------------------------------------------------------- |
| A launcher script | `~/.pushary/codex-app/codex` |
| A login item that applies the setting after a restart | `~/Library/LaunchAgents/com.pushary.codex-app-cards.plist` |
| The setting itself, for your login session | `CODEX_CLI_PATH`, set with `launchctl setenv` |
`pushary codex --app-cards off` removes the login item and the setting, and replaces the launcher with one that only runs Codex's engine. The file stays on purpose: while the switch is on, the Codex app records the launcher's path in its own settings (`~/.codex/config.toml`) each time it starts, and a Codex that is still open keeps starting its engine through it. After `off`, nothing of Pushary sits in between. `pushary clean` removes the launcher as well, so quit Codex and open it again after a clean. If another program already set `CODEX_CLI_PATH`, Pushary leaves it alone and says so.
The launcher is built to step aside. It hands every engine command except the app's own protocol straight to Codex's engine. For that one protocol it starts Pushary and checks that Pushary started the engine; if Pushary is missing or broken, or the launcher cannot write beside itself, the launcher starts the engine itself and Codex runs without the cards. If the launcher is stopped, Pushary stops the engine within a few seconds, as Codex expects. If an update moves the engine, the launcher turns its own setting off and says so; open Codex again and it starts normally. `pushary codex --app-cards status` says when the switch is on but Pushary or the engine is no longer where the launcher expects.
Running `pushary setup` again while the switch is on is safe: setup looks through the launcher to Codex's own engine.
This is experimental and not enabled by ordinary setup. It relies on a launch setting of the Codex app that OpenAI does not document, and was checked against Codex app 26.1002 (engine 0.162).
## What setup changes on your computer [#what-setup-changes-on-your-computer]
| File | What changes |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `~/.codex/config.toml` | Adds `[mcp_servers.pushary]` with your key in an `Authorization` header and `default_tools_approval_mode = "approve"`, so Pushary's own tools run without a prompt. Adds trust entries for the Pushary hooks. Removes the old `pushary-codex` notify handler. |
| `~/.codex/hooks.json` | Adds 11 hook events. Every one runs the same command, `pushary-codex-hook`, so there is one command to review. |
| `~/.codex/AGENTS.md` | Adds a Pushary block that tells Codex to ask through Pushary instead of the terminal. |
| `~/.codex/skills/pushary/SKILL.md` | The Pushary skill. |
| `~/.pushary/config.json` and your shell profile | Your API key, as `PUSHARY_API_KEY`. |
| `AGENTS.md` in the current folder | Only if you say yes when asked. |
If you set `CODEX_HOME`, these files go there instead of `~/.codex`. The key sits in `config.toml` itself, so Codex reaches the MCP server even when it starts from a shell that never loaded your profile.
### What each hook does [#what-each-hook-does]
| Event | What happens |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `PermissionRequest` | Runs when Codex is about to ask you in the terminal: a sandbox escalation, a forced `rm`, an MCP tool not marked read-only, or a patch outside the writable folders. Your approval mode decides whether the question goes to your phone. Yes runs it, No denies it. If your rule says Ask in terminal on timeout, it falls back to Codex's own prompt. |
| `PreToolUse` | The enforcement backstop. Runs before every shell command, `apply_patch` call, MCP call and Codex question. The kill switch and always-deny rules block the call here, even when Codex would not have asked. See below for when it asks you. |
| `PostToolUse` | Records `tool_complete` or `tool_error` and clears pending phone questions. |
| `UserPromptSubmit` | Saves your prompt as the session's task title. Set `PUSHARY_TASK_TITLES=off` to turn it off. |
| `Stop` | Marks the session idle and cancels pending phone questions. |
| `SessionStart`, `SessionEnd` | Show the session in your session list from its first second to its end. |
| `SubagentStart`, `SubagentStop`, `PreCompact`, `PostCompact` | Record subagents and context compaction. They decide nothing. |
`PermissionRequest` and `PreToolUse` wait up to 180 seconds for your answer.
### When PreToolUse asks you [#when-pretooluse-asks-you]
`PreToolUse` fires on every call, so it asks only about the calls that matter. With the CLI's hooks, that means:
* **An edit outside a scope you agreed.** Codex applies edits inside the project without asking, so Pushary asks in Always and When away, and refuses in Alert only and In agent.
* **Full access.** When Codex's approval policy is `never`, Codex asks nothing itself. From `pushary` 1.9.17, every call your policy gates then goes to your phone, even while you sit at the keyboard, and waits for your policy's timeout. A call nobody answers is refused with "No response within timeout". Only in Always can a rule that approves on timeout let it run after the full wait. In Alert only and In agent, a gated call still runs, and so does any call made while your plan has lapsed or when the hook fails.
* **Always.** Pushary also asks about a destructive or networked command, a patch that deletes a file, and any call that a rule you wrote or a preset you picked covers.
* **When away.** Pushary asks about a destructive command (such as `rm -rf`, a force push, `git reset --hard`, `git clean` or dropping a table) and a patch that deletes a file, because Codex can run those without asking. With the Mac app it shows in the notch while you are at your Mac and goes to your phone when you are not. With the CLI it goes to your phone when you are away from the keyboard. A call nobody answers is refused.
* **Codex's own questions.** When Codex asks you something with `request_user_input`, Pushary shows the question in the notch or on your phone, in every mode except Alert only and In agent. A question with choices shows its choices. A question with no choices shows a text field, and what you type goes back word for word, unless it is only the word "defer", which hands the question back to Codex. Your answer goes back to Codex in place of its card. A question with one choice, more than four, or a secret stays in Codex, and so does any question nobody answers, a blank answer, or a question Pushary cannot reach. On a Mac wired by the Pushary app, several questions asked together stay in Codex when one of them has no choices, until the app is on a version that shows them.
Everything else is left to Codex, which raises `PermissionRequest` when it wants a person. The Mac app's hooks ask about the same calls.
## Check that it works [#check-that-it-works]
```bash
npx pushary@latest doctor
```
Doctor checks your key and plan, then Codex's MCP server, hooks, hook trust, skill and `AGENTS.md` block. It sends a real test push. Add `--roundtrip` to send a test question too.
Then try a real approval. A new workspace starts on **Always**, so it reaches your phone while you sit at your computer. If yours is on When away, decisions stay in Codex's own prompt while you are there. To test from your desk, open the Pushary app, tap **Questions & approvals** at the top of your inbox, and pick **Always**.
Ask Codex to run a command that needs approval, for example `curl -I https://example.com`. Your phone buzzes. Tap **Approve** and Codex runs it. Switch back to **When away** when you are done.
## What Pushary can and cannot enforce on Codex [#what-pushary-can-and-cannot-enforce-on-codex]
* **Matched:** shell commands (`Bash`), `apply_patch` edits and MCP calls (`mcp__*`). An `apply_patch` call maps to your `Edit` rules, and a multi-file patch is checked against every path it touches.
* **Not matched:** other Codex tools.
* **Not visible to hooks:** Codex sends no `PreToolUse` event for input written into a shell session that is already running.
* **Fails open:** if the hook command crashes, Codex does not block the call.
* **Trust first:** Codex never runs an untrusted hook.
Treat enforcement on Codex as a guardrail, not a security boundary.
### Codex's permission mode decides first [#codexs-permission-mode-decides-first]
Codex reports the same permission modes as Claude Code (`default`, `acceptEdits`, `plan`, `dontAsk`, `bypassPermissions`), and Pushary applies the same rule. It steps aside in `plan` and `dontAsk`, and keeps gating in `default`, `acceptEdits` and `bypassPermissions`. In `acceptEdits`, edits still gate, because Codex sends them as `apply_patch`. The kill switch, your deny rules and a scope you approved apply in every mode. Set `PUSHARY_RESPECT_PERMISSION_MODE=0` to gate in every mode.
When Codex's auto-reviewer approves calls for you, Pushary checks the kill switch and always-deny rules, then lets the reviewer decide. If the reviewer denies a shell command, the `Stop` hook can ask you about it. A yes tells Codex to run the same command again, and that retry is allowed.
## Set it up by hand [#set-it-up-by-hand]
Use setup if you can. It writes the trust entries, so you skip the `/hooks` step.
### MCP server [#mcp-server]
Add this to `~/.codex/config.toml`:
```toml
[mcp_servers.pushary]
url = "https://pushary.com/api/mcp/mcp"
default_tools_approval_mode = "approve"
[mcp_servers.pushary.http_headers]
Authorization = "Bearer pk_xxx.xxx"
```
Or register it with the Codex CLI. This form reads the key from the environment of the shell that starts Codex, so it fails with 401 when that shell never loaded your profile:
```bash
codex mcp add pushary --url https://pushary.com/api/mcp/mcp --bearer-token-env-var PUSHARY_API_KEY
```
Save your key in your shell profile (`~/.zshrc` or `~/.bashrc`):
```bash
export PUSHARY_API_KEY="pk_xxx.xxx"
```
### Hooks [#hooks]
Install the Pushary CLI globally, unless `which pushary-codex-hook` already prints a path. An existing install under `@pushary/agent-hooks` provides the same commands, and npm refuses to install over them.
```bash
npm install -g pushary
```
Add the entries to `~/.codex/hooks.json`. Replace `/path/to/pushary-codex-hook` with the output of `which pushary-codex-hook`, and use the same path in every entry:
```json
{
"hooks": {
"PermissionRequest": [
{ "hooks": [{ "type": "command", "command": "/path/to/pushary-codex-hook", "timeout": 180, "statusMessage": "Waiting for your phone" }] }
],
"PreToolUse": [
{ "matcher": "Bash|apply_patch|mcp__.*|request_user_input|request_user_input_async", "hooks": [{ "type": "command", "command": "/path/to/pushary-codex-hook", "timeout": 180, "statusMessage": "Checking Pushary policy" }] }
],
"PostToolUse": [
{ "matcher": "Bash|apply_patch|mcp__.*", "hooks": [{ "type": "command", "command": "/path/to/pushary-codex-hook", "timeout": 10 }] }
],
"UserPromptSubmit": [
{ "hooks": [{ "type": "command", "command": "/path/to/pushary-codex-hook", "timeout": 10 }] }
],
"Stop": [
{ "hooks": [{ "type": "command", "command": "/path/to/pushary-codex-hook --hold-budget=180", "timeout": 180 }] }
],
"SessionStart": [
{ "matcher": "startup|resume|clear", "hooks": [{ "type": "command", "command": "/path/to/pushary-codex-hook", "timeout": 10 }] }
],
"SessionEnd": [
{ "hooks": [{ "type": "command", "command": "/path/to/pushary-codex-hook", "timeout": 3 }] }
],
"SubagentStart": [
{ "hooks": [{ "type": "command", "command": "/path/to/pushary-codex-hook", "timeout": 5 }] }
],
"SubagentStop": [
{ "hooks": [{ "type": "command", "command": "/path/to/pushary-codex-hook", "timeout": 5 }] }
],
"PreCompact": [
{ "matcher": "manual|auto", "hooks": [{ "type": "command", "command": "/path/to/pushary-codex-hook", "timeout": 5 }] }
],
"PostCompact": [
{ "matcher": "manual|auto", "hooks": [{ "type": "command", "command": "/path/to/pushary-codex-hook", "timeout": 5 }] }
]
}
}
```
Codex caps `SessionEnd` at 3 seconds, which is why that entry is shorter. Open Codex, type `/hooks` and trust the Pushary entries. In CI, where nobody can answer the trust prompt, Codex supports `--dangerously-bypass-hook-trust`. Use it only there.
## Legacy notify handler [#legacy-notify-handler]
Codex versions before 0.122 have no hooks. On those, setup adds the `pushary-codex` notify handler to `~/.codex/config.toml`. It reports turn completions only, because Codex's notify setting fires for nothing else. It has no policy engine, no kill switch and no session tracking. Setup removes it once you upgrade Codex and run setup again.
## Troubleshooting [#troubleshooting]
### Approvals never reach my phone [#approvals-never-reach-my-phone]
* Codex does not trust the hooks yet. Type `/hooks` in Codex and trust the Pushary entries.
* Your Codex is older than 0.122, so setup installed only the notify handler. Upgrade Codex and run setup again.
* You are at your computer in When away mode, so the decision stays in Codex's prompt. To test, set **Questions & approvals** to **Always** in the app.
* Run `npx pushary@latest doctor`. It reports whether Codex lists and trusts the Pushary hooks.
### Setup says your Codex install is missing the binary it runs [#setup-says-your-codex-install-is-missing-the-binary-it-runs]
A Pushary setup before 0.71.0 asked Codex for its version. On macOS that hands the binary to XProtect, which can quarantine the Codex CLI. Setup no longer runs Codex. Reinstall it:
```bash
npm install -g @openai/codex
```
### The Pushary MCP server answers 401 [#the-pushary-mcp-server-answers-401]
You probably added it with `--bearer-token-env-var`, and Codex started from a shell without `PUSHARY_API_KEY`. Run setup again. It writes the key into `config.toml`.
### Setup says Pushary for Mac handles Codex's approvals [#setup-says-pushary-for-mac-handles-codexs-approvals]
The Mac app already owns Codex's hooks. Setup reuses the app's key and keeps the app's hooks. To have the CLI own them instead, run `npx pushary@latest setup --take-over-hooks`.
### More help [#more-help]
* [Codex hooks](/codex-hooks): the events, the trust hash, and how hooks meet the auto-reviewer.
* [Codex dangerously skip permissions](/codex-dangerously-skip-permissions): the flags and config keys that turn approvals off, and a safer pattern.
* [Control Codex from your phone](/control-codex-from-phone): approve, answer or stop a Codex session away from the terminal.
## Next steps [#next-steps]
---
# Cursor setup
Source: https://pushary.com/docs/agents/guides/cursor
Last updated: 2026-10-12
> Set up enforced Cursor approvals on your phone. The Pushary plugin stops shell commands, MCP calls and file edits in Cursor until your policy allows them.
Pushary adds enforced Cursor approvals. Its plugin registers Cursor hooks (small programs Cursor runs before an action) that hold a shell command, an MCP call, or a Write, Delete or Edit tool call until your Pushary policy allows it, and send the risky ones to your phone. The agent does not choose whether to ask. Plain MCP, without the plugin, only gives the agent tools it may never call.
## Set it up [#set-it-up]
```bash
npx pushary@latest setup
```
On Windows PowerShell, run `npx.cmd pushary@latest setup`. The CLI needs Node.js 20.17+, 22.13+ or 23.5+.
### Scan the QR code with your phone [#scan-the-qr-code-with-your-phone]
Setup shows a QR code first. Scan it with your phone's camera or the Pushary app. Sign in or create an account, check that the code on your phone matches the one in your terminal, then tap **Approve** on **Connect this computer?** and allow notifications.
New accounts start a 3-day free trial, then pay $9.99 a month. A card is needed to start the trial. If this computer already has a Pushary key, setup reuses it and skips this step.
### Pick Cursor [#pick-cursor]
Setup asks **Which agents do you use?** with the agents it found already selected. Keep Cursor selected. To skip the question, run `npx pushary@latest setup --agents cursor`.
### Answer the test question [#answer-the-test-question]
Setup sends a test push, then a test question. Answer it on your phone. The terminal says **You're set** once your answer arrives.
### Fully quit and reopen Cursor [#fully-quit-and-reopen-cursor]
Cursor loads hooks when it starts. A **Developer: Reload Window** is not always enough.
On a Mac you can use [Pushary Isle](/docs/agents/mac-app) instead. Connecting your agents there registers the gate in `~/.cursor/hooks.json`, links your key in `~/.cursor/mcp.json`, and writes the always-on rule that tells Cursor to ask through Pushary. The CLI is still what installs the full plugin, including the skill.
## What setup changes on your computer [#what-setup-changes-on-your-computer]
| File | What changes |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `~/.cursor/plugins/local/pushary/` | The Pushary plugin: the MCP tools (`mcp.json`, with your key), the always-on rule (`rules/pushary.mdc`) and the skill. |
| `~/.cursor/hooks.json` | The approval gate for `beforeShellExecution`, `beforeMCPExecution` and `preToolUse` (`Write`, `Delete`, `Edit`), plus activity hooks for prompts, tool results, subagents and sessions. Every entry runs `pushary-gate.mjs` from the plugin. |
| `~/.pushary/config.json` and your shell profile | Your API key, as `PUSHARY_API_KEY`. |
The user-level `~/.cursor/hooks.json` is the reliable place for the gate. Setup also strips the gate from the plugin's own copy, because some Cursor versions run a local plugin's bundled hooks too, which would fire each approval twice.
## Check that it works [#check-that-it-works]
```bash
npx pushary@latest doctor
```
Doctor checks your key and plan, that the gate is registered in `~/.cursor/hooks.json`, that its script path resolves, and that the MCP server has your key. It sends a real test push. Add `--roundtrip` to send a test question too.
Then try a real approval. A new workspace starts on **Always**, so it reaches your phone while you sit at your computer. If yours is on When away, decisions stay in Cursor while you are there. To test from your desk, open the Pushary app, tap **Questions & approvals** at the top of your inbox, and pick **Always**.
Ask Cursor's agent to run `touch pushary-test.txt`. Your phone buzzes. Tap **Approve** and the command runs. Switch back to **When away** when you are done.
## What Pushary can and cannot enforce on Cursor [#what-pushary-can-and-cannot-enforce-on-cursor]
* **Gated before they run:** shell commands, MCP calls, and `Write`, `Delete` and `Edit` tool calls. Your approval rules decide which of them need your answer. Ordinary safe actions can proceed.
* **Not gated:** file reads. Cursor's read hook can only answer allow or deny, so a gate there would have to allow every read while Pushary is down.
* **Wait time:** the gate waits up to 45 seconds for your phone, inside Cursor's 60-second hook limit.
* **When Pushary hands a call back:** shell commands and MCP calls go to Cursor's own prompt. File edits have no Cursor prompt, so they are refused instead.
* **When Pushary is unavailable:** the same rule applies. A missing key, a missing gate script or no Node sends shell and MCP calls to Cursor's prompt and refuses file edits. On Windows, a missing gate script or no Node makes the hook itself fail, and Cursor blocks the call. A required approval is never allowed silently.
### Plugin or plain MCP [#plugin-or-plain-mcp]
| | Plugin | Plain MCP |
| ---------------------------------------------------------------------------------------------- | ------ | --------- |
| Send notifications | Yes | Yes |
| Ask questions, wait for answers | Yes | Yes |
| Enforced approval before risky shell commands, MCP calls, and Write, Delete or Edit tool calls | Yes | No |
| Works without installing anything locally | No | Yes |
The plugin bundles the same MCP tools and adds the gate.
## Use plain MCP instead [#use-plain-mcp-instead]
If you cannot install the plugin, add Pushary as an MCP server in `~/.cursor/mcp.json`, or in `.cursor/mcp.json` inside one project:
```json
{
"mcpServers": {
"pushary": {
"url": "https://pushary.com/api/mcp/mcp",
"headers": {
"Authorization": "Bearer pk_xxx.xxx"
}
}
}
}
```
Replace `pk_xxx.xxx` with your API key. Your agent gets the notification and question tools, so it can ping your phone and wait for an answer. Nothing is intercepted, though. The agent must choose to call the tools. If it never asks, you are never asked, even right before a dangerous command.
The plugin includes the Pushary skill. With plain MCP, install it so your agent knows when to notify you:
```bash
npx skills add Pushary/pushary-skill
```
## Troubleshooting [#troubleshooting]
### The gate never fires [#the-gate-never-fires]
Run `npx pushary@latest doctor`. It checks that the hook is registered and that its script path resolves. Then fully quit and reopen Cursor.
### I installed the plugin from the marketplace and nothing is gated [#i-installed-the-plugin-from-the-marketplace-and-nothing-is-gated]
The plugin on its own gives you the MCP tools, the rule and the skill. It does not link your API key or register the gate reliably across platforms. Run `npx pushary@latest setup` once to do both.
### Cursor asks me twice for one action [#cursor-asks-me-twice-for-one-action]
Your plugin copy still carries its own gate. Run setup again. It removes the gate from the plugin copy and keeps the one in `~/.cursor/hooks.json`.
### On Windows I get Cursor's own prompt and no push [#on-windows-i-get-cursors-own-prompt-and-no-push]
Cursor on Windows sends the hook a payload that starts with a byte order mark. Old gate versions could not read it and fell back to Cursor's prompt. The current gate reads it. If you patched the gate by hand, run setup again to get the shipped version.
### I want to see what the hook received [#i-want-to-see-what-the-hook-received]
Open Cursor's hooks output channel. It shows each hook call and its answer.
### Setup says Pushary for Mac handles Cursor's approvals [#setup-says-pushary-for-mac-handles-cursors-approvals]
The Mac app already owns Cursor's hooks. Setup reuses the app's key and keeps the app's hooks. To have the CLI own them instead, run `npx pushary@latest setup --take-over-hooks`.
## Next steps [#next-steps]
---
# fx setup
Source: https://pushary.com/docs/agents/guides/fx
Last updated: 2026-10-08
> Connect fx, the Vercel Labs agent, to Pushary with one command, so fx can send you notifications and ask you questions on your phone.
Pushary connects to [fx](https://fx.sh) through MCP (Model Context Protocol, the standard way AI apps call outside tools). One setup command gives fx's agent Pushary's tools and turns off fx's own terminal question tool. fx's agent then sends you notifications and asks its questions on your phone, or in the Mac notch while Pushary Isle runs.
```bash
npx pushary@latest setup --agents fx
```
fx has no hooks that other tools can register. So Pushary cannot answer fx's own approval prompts, and it cannot stop an fx tool call. fx's agent asks through Pushary when it decides to, which makes fx a cooperative agent. [Supported agents](/docs/agents/supported-agents) explains which agents Pushary can enforce.
## Set up fx [#set-up-fx]
The CLI needs Node.js 20.17+, 22.13+ or 23.5+. If this computer has no Pushary key yet, setup signs you in and saves one.
### Run fx once [#run-fx-once]
Setup skips fx when `~/.fx` does not exist, so start fx at least once first. fx does not run on Windows, and setup skips it there.
### Run setup [#run-setup]
Run the command above. Or run `npx pushary@latest setup` and pick fx in the list. Setup ticks fx for you when it finds fx's own files in `~/.fx`: `settings.json`, `sessions` or `history.jsonl`.
When it works, setup prints **Turning off fx's terminal question tool, so its questions go through Pushary**.
### Reload fx [#reload-fx]
Restart fx, or run `/mcp reload` in a running session.
### Check it [#check-it]
```bash
npx pushary@latest doctor
```
Doctor prints **fx: Pushary MCP server configured** and **fx: questions go through Pushary**. Then ask fx's agent to send you a test question. It shows on your phone, marked as fx.
## Set up fx with the Mac app [#set-up-fx-with-the-mac-app]
The Pushary Mac app (Pushary Isle) sets up fx without Node or the CLI. With **Set up new agents automatically** on, it connects fx when it finds fx's files in `~/.fx`. fx is also listed under Agents in Settings. Its switch connects or disconnects fx, and **Repair** puts back anything that is missing.
The Mac app writes the same three files setup writes. Its `pushary` server in `~/.fx/mcp.json` runs `~/.pushary/bin/pushary-mcp`, which starts the Mac app's signed helper. The helper uses the key this Mac signed in with, so no key is written to fx's files. If the Pushary CLI already set up fx, the Mac app keeps the CLI's `pushary` server as it is.
Questions fx's agent asks through Pushary reach the Mac notch, marked as fx, and your phone. As with setup, restart fx or run `/mcp reload` after the Mac app connects it.
## What setup changes in \~/.fx [#what-setup-changes-in-fx]
Setup installs the Pushary CLI globally, then changes three files:
| File | What setup adds |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `~/.fx/mcp.json` | A `pushary` server. It runs `pushary-mcp`, a local bridge that forwards fx's MCP calls to Pushary, with `operation_timeout_ms` set to 75000 (longer than the 55 seconds a question can block) |
| `~/.fx/settings.json` | Two permission rules. `"mcp_pushary_*": "allow"` lets fx run Pushary's tools without asking. `"ask_user_question": "deny"` turns off fx's own terminal question tool, so its agent asks through Pushary |
| `~/.fx/AGENTS.md` | A block between `` and ``. It tells fx's agent when to ask through Pushary and how to load Pushary's tools |
Setup adds both rules after your own rules. If you already have a rule for `mcp_pushary_*` or `ask_user_question`, setup keeps yours and tells you.
No key is written to fx's files. The bridge reads your key from `PUSHARY_API_KEY` when it is set, and from `~/.pushary/config.json` otherwise. When you switch to a new key, fx picks it up from there, so you do not run fx setup again.
## Pushary's tools in fx [#pusharys-tools-in-fx]
fx names them:
| Tool | What it does |
| ------------------------------- | --------------------------------------------------------------------- |
| `mcp_pushary_send_notification` | Send a push notification, with optional rich context |
| `mcp_pushary_schedule_reminder` | Schedule, list or cancel a personal reminder that survives fx exiting |
| `mcp_pushary_ask_user` | Ask a question: yes/no, multiple choice, or free text |
| `mcp_pushary_wait_for_answer` | Wait for your answer |
| `mcp_pushary_cancel_question` | Cancel a pending question |
| `mcp_pushary_propose_scope` | Propose the boundary of a run for you to approve |
fx does not load these tools at the start of a turn. Its agent loads each one by exact name with `mcp_select_tool`, for example `{"name": "mcp_pushary_ask_user"}`. The block in `~/.fx/AGENTS.md` tells it to.
For example, ask fx “Remind me in 30 minutes to check the deploy.” fx loads `mcp_pushary_schedule_reminder`, saves the reminder on Pushary, and confirms its time. Calendar times need your timezone. One-time reminders support the next 30 days; delivery can be about a minute late. Your connected phone receives it even when fx is closed. Pushary respects stop/mute and current account access. This does not run future fx tasks.
## What Pushary cannot do in fx [#what-pushary-cannot-do-in-fx]
* **fx's approval prompts stay in the terminal.** fx cannot hand them to another tool. Only questions fx's agent asks through `mcp_pushary_ask_user` reach your phone.
* **Full-access mode keeps fx's question tool on.** fx's full-access mode (`yolo`, `full-access` or `full access`, set in settings, in a workspace, or with `FX_PERMISSION_MODE`) ignores permission rules. So the `ask_user_question` rule does nothing there, and the agent is only told to ask through Pushary. Setup and `doctor` tell you when this applies. Leave full-access mode with `/permissions` to route questions for certain.
* **Workspace rules replace the global ones.** An fx workspace with its own `permission` rules ignores the rules setup added. Setup tells you how many there are. Add `"mcp_pushary_*": "allow"` and `"ask_user_question": "deny"` to each one.
## When setup leaves fx's files alone [#when-setup-leaves-fxs-files-alone]
Setup does not edit `~/.fx/mcp.json` or `~/.fx/settings.json` when fx itself would reject the file or refuse to edit it. That covers:
* comments, a trailing comma or a byte-order mark
* a repeated key
* servers under a key fx does not read
* a file larger than fx reads (1 MiB for `mcp.json`, 64 KiB for `settings.json`)
It also leaves `settings.json` alone when fx would ignore it, for example because of an unknown permission action or permission mode.
Setup names the file and the reason. Fix the file, then retry only fx:
```bash
npx pushary@latest setup --agents fx
```
If `~/.fx/AGENTS.md` is a symlink to a file outside `~/.fx`, fx ignores it. Setup writes no instructions there and tells you to replace the link with a file.
## Why can fx's questions still stop in the terminal? [#why-can-fxs-questions-still-stop-in-the-terminal]
Run `npx pushary@latest doctor`. It checks the `pushary` server in `~/.fx/mcp.json` and that its bridge is installed. It warns when Pushary's tools are not auto-allowed, and when fx's questions can still stop in the terminal. The usual causes are full-access mode, a workspace with its own rules, or your own `ask_user_question` rule. Doctor names which one.
## Remove Pushary from fx [#remove-pushary-from-fx]
```bash
npx pushary@latest disconnect fx
```
This removes the `pushary` server, the `mcp_pushary_*` rule setup added, the `ask_user_question` rule (only if setup added it and you have not changed it since), and the block in `~/.fx/AGENTS.md`. In the Mac app, turning fx off under Agents in Settings removes the same things.
`npx pushary@latest clean` removes Pushary from every agent, fx included. See the [CLI reference](/docs/agents/reference/cli#clean).
## Next steps [#next-steps]
---
# Gemini CLI setup
Source: https://pushary.com/docs/agents/guides/gemini-cli
Last updated: 2026-10-12
> Set up Gemini CLI approvals on your phone with native hooks. Gate shell commands, file edits and MCP calls, and see what changes under gemini --yolo.
Pushary puts Gemini CLI's risky tool calls on your phone. Native Gemini hooks (small programs Gemini runs around each tool call) check your Pushary policy before each shell command, file write, edit and MCP call, and the ones that need a person wait for your answer. Gemini CLI has no `--dangerously-skip-permissions`. Its `--yolo` flag approves everything, and a deny from your phone still blocks the call.
On 18 June 2026 Google stopped serving Gemini CLI to Google AI Pro, Ultra and free accounts and moved them to Antigravity CLI. Gemini Code Assist Standard and Enterprise licences keep Gemini CLI. This guide covers Gemini CLI. For Antigravity CLI, see the [Antigravity CLI guide](/docs/agents/guides/antigravity-cli).
## Set it up [#set-it-up]
```bash
npx pushary@latest setup
```
On Windows PowerShell, run `npx.cmd pushary@latest setup`. The CLI needs Node.js 20.17+, 22.13+ or 23.5+.
### Scan the QR code with your phone [#scan-the-qr-code-with-your-phone]
Setup shows a QR code first. Scan it with your phone's camera or the Pushary app. Sign in or create an account, check that the code on your phone matches the one in your terminal, then tap **Approve** on **Connect this computer?** and allow notifications.
New accounts start a 3-day free trial, then pay $9.99 a month. A card is needed to start the trial. If this computer already has a Pushary key, setup reuses it and skips this step.
### Pick Gemini CLI [#pick-gemini-cli]
Setup asks **Which agents do you use?** with the agents it found already selected. Keep Gemini CLI selected. To skip the question, run `npx pushary@latest setup --agents gemini_cli`. If the `gemini` command is not installed, setup skips it.
### Answer the test question [#answer-the-test-question]
Setup sends a test push, then a test question. Answer it on your phone. The terminal says **You're set** once your answer arrives.
### Restart Gemini CLI [#restart-gemini-cli]
Gemini loads hooks when it starts. Gemini does not make you trust a hook first, so approvals work as soon as you restart.
On a Mac you can use [Pushary Isle](/docs/agents/mac-app) instead. It connects Gemini CLI for you and needs no Node.
## What setup changes on your computer [#what-setup-changes-on-your-computer]
| File | What changes |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `~/.gemini/settings.json` | Adds the `pushary` MCP server (`httpUrl`, your key in an `Authorization` header, and `trust: true` so Pushary's own tools run without a prompt). Adds six hook events. |
| `~/.gemini/GEMINI.md` | Adds a Pushary block that tells Gemini to ask through Pushary instead of the terminal. |
| `~/.pushary/config.json` and your shell profile | Your API key, as `PUSHARY_API_KEY`. |
Setup also installs the `pushary` package globally, so each hook starts fast.
### What each hook does [#what-each-hook-does]
| Event | What happens |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `BeforeTool` | The approval gate. Runs before `run_shell_command`, `write_file`, `replace` and MCP tools. The kill switch and always-deny rules block here. Other calls follow your approval mode. Waits up to 180 seconds. |
| `AfterTool` | Records `tool_complete` or `tool_error` and clears pending phone questions. |
| `BeforeAgent` | Saves your prompt as the session's task title. Set `PUSHARY_TASK_TITLES=off` to turn it off. |
| `AfterAgent` | Marks the turn finished. |
| `SessionStart` | Shows the session in your session list from its first second. |
| `SessionEnd` | Marks the session idle and cancels pending phone questions. |
## Check that it works [#check-that-it-works]
```bash
npx pushary@latest doctor
```
Doctor checks your key and plan, then Gemini's MCP server, hooks and `GEMINI.md` block. It sends a real test push. Add `--roundtrip` to send a test question too.
Then try a real approval. A new workspace starts on **Always**, so it reaches your phone while you sit at your computer. If yours is on When away, decisions are left to Gemini's own prompt while you are there. To test from your desk, open the Pushary app, tap **Questions & approvals** at the top of your inbox, and pick **Always**.
Ask Gemini: `Create a file called pushary-test.txt that says hello.` Your phone buzzes with the write request. Tap **Approve** and the file appears. Switch back to **When away** when you are done.
## What Pushary can and cannot enforce on Gemini CLI [#what-pushary-can-and-cannot-enforce-on-gemini-cli]
Gemini's tools have different names from Claude Code's, so Pushary maps them onto your existing rules:
| Gemini tool | Pushary rule |
| ------------------- | ----------------------- |
| `run_shell_command` | `Bash` |
| `write_file` | `Write` |
| `replace` | `Edit` |
| MCP tools | `mcp____` |
One rule works across every agent. A rule you set for `Bash` also gates `run_shell_command`. File reads and other Gemini tools are not gated.
Gemini's hook protocol has no "ask" answer. When Pushary leaves a call to Gemini, it prints nothing and Gemini's own confirmation runs. That happens:
* in In agent and Alert only modes,
* in When away, when you are at your computer, when nobody answers your phone in time, or when Pushary cannot be reached,
* in Always, when no phone is connected, when Pushary hands the call back to your keyboard, or when nobody answers and your rule says **Ask in terminal**.
In Always, a question Pushary could not send at all is refused.
## Gemini CLI --yolo and dangerously skip permissions [#gemini-cli---yolo-and-dangerously-skip-permissions]
Gemini CLI has no `--dangerously-skip-permissions` flag. The equivalent is `gemini --yolo` (short form `-y`, or `--approval-mode yolo`), which approves every tool call with no prompt. Ctrl+Y toggles it inside a session. Use it only in a throwaway or sandboxed environment.
Pushary does not make yolo safe. Every call Pushary leaves to Gemini runs with no prompt under yolo. That includes a question nobody answered when the rule says **Ask in terminal**, a call handed back to your keyboard, a call made while your plan has lapsed, and a call where the hook itself failed.
To answer fewer prompts in the terminal and still approve risky calls on your phone, stay in Gemini's `default` or `auto_edit` mode and set your Pushary approval mode to **Always**. Gated calls go to your phone. Anything Pushary hands back falls to Gemini's own approval mode, so a shell command still gets a prompt instead of running unchecked. Under `auto_edit`, an edit Pushary hands back runs without a prompt, so use `default` if every hand-back should prompt.
See [Gemini dangerously skip permissions](/gemini-dangerously-skip-permissions) for the approval modes and Antigravity CLI's flag.
## Set it up by hand [#set-it-up-by-hand]
Use setup if you can. It pins each hook command to its full path.
Install the Pushary CLI globally, unless `which pushary-gemini-hook` already prints a path. An existing install under `@pushary/agent-hooks` provides the same commands, and npm refuses to install over them.
```bash
npm install -g pushary
```
Add the MCP server to `~/.gemini/settings.json`. Replace the token with your API key:
```json
{
"mcpServers": {
"pushary": {
"httpUrl": "https://pushary.com/api/mcp/mcp",
"headers": { "Authorization": "Bearer pk_xxx.xxx" },
"trust": true
}
}
}
```
Add the hooks under the same top-level object. Replace `/path/to/pushary-gemini-hook` with the output of `which pushary-gemini-hook`, and use the same path in every entry. Gemini counts hook timeouts in milliseconds.
```json
{
"hooks": {
"BeforeTool": [
{
"matcher": "run_shell_command|write_file|replace|mcp_.*",
"hooks": [{ "name": "pushary", "type": "command", "command": "/path/to/pushary-gemini-hook", "timeout": 180000 }]
}
],
"AfterTool": [
{
"matcher": "run_shell_command|write_file|replace|mcp_.*",
"hooks": [{ "name": "pushary", "type": "command", "command": "/path/to/pushary-gemini-hook", "timeout": 10000 }]
}
],
"BeforeAgent": [
{ "hooks": [{ "name": "pushary", "type": "command", "command": "/path/to/pushary-gemini-hook", "timeout": 10000 }] }
],
"AfterAgent": [
{ "hooks": [{ "name": "pushary", "type": "command", "command": "/path/to/pushary-gemini-hook", "timeout": 10000 }] }
],
"SessionStart": [
{ "hooks": [{ "name": "pushary", "type": "command", "command": "/path/to/pushary-gemini-hook", "timeout": 10000 }] }
],
"SessionEnd": [
{ "hooks": [{ "name": "pushary", "type": "command", "command": "/path/to/pushary-gemini-hook", "timeout": 10000 }] }
]
}
}
```
Save your key in your shell profile so the hook can read it:
```bash
export PUSHARY_API_KEY="pk_xxx.xxx"
```
Restart Gemini CLI, then run `npx pushary@latest doctor`.
## Troubleshooting [#troubleshooting]
### Gemini still asks in the terminal [#gemini-still-asks-in-the-terminal]
That is expected when Pushary leaves the call to Gemini, for example at your desk in When away mode. See the list under [What Pushary can and cannot enforce on Gemini CLI](#what-pushary-can-and-cannot-enforce-on-gemini-cli).
### The hooks never run [#the-hooks-never-run]
* Restart Gemini CLI. It reads `~/.gemini/settings.json` at start.
* Check that `~/.gemini/settings.json` has a `hooks.BeforeTool` entry that runs `pushary-gemini-hook`.
* Run `npx pushary@latest doctor`. It lists any hook event that is missing.
### Gemini CLI stopped working with my Google account [#gemini-cli-stopped-working-with-my-google-account]
Google moved Google AI Pro, Ultra and free accounts to Antigravity CLI. Set up [Antigravity CLI](/docs/agents/guides/antigravity-cli) instead.
### Setup says Pushary for Mac handles Gemini CLI's approvals [#setup-says-pushary-for-mac-handles-gemini-clis-approvals]
The Mac app already owns Gemini's hooks. Setup reuses the app's key and keeps the app's hooks. To have the CLI own them instead, run `npx pushary@latest setup --take-over-hooks`.
## Next steps [#next-steps]
---
# Hermes Agent setup
Source: https://pushary.com/docs/agents/guides/hermes
Last updated: 2026-10-08
> Set up Hermes Agent notifications and phone approvals in one command. The Pushary plugin sends Hermes's dangerous-command approvals and questions to your phone.
Pushary sends Hermes Agent notifications and approvals to your phone. A Python plugin adds Pushary tools to Hermes, makes Pushary the transport for Hermes's dangerous-command approvals, and routes Hermes's questions to your phone. You get a push when a tool fails, and you can approve or deny a flagged command from your lock screen while Hermes runs.
## Set it up [#set-it-up]
```bash
npx pushary@latest setup
```
On Windows PowerShell, run `npx.cmd pushary@latest setup`. The CLI needs Node.js 20.17+, 22.13+ or 23.5+.
### Scan the QR code with your phone [#scan-the-qr-code-with-your-phone]
Setup shows a QR code first. Scan it with your phone's camera or the Pushary app. Sign in or create an account, check that the code on your phone matches the one in your terminal, then tap **Approve** on **Connect this computer?** and allow notifications.
New accounts start a 3-day free trial, then pay $9.99 a month. A card is needed to start the trial. If this computer already has a Pushary key, setup reuses it and skips this step.
### Pick Hermes [#pick-hermes]
Setup asks **Which agents do you use?** with the agents it found already selected. Keep Hermes selected. To skip the question, run `npx pushary@latest setup --agents hermes`.
Setup finds the Python that Hermes runs in, installs the `hermes-plugin-pushary` package there with pip, and edits `~/.hermes/config.yaml`. It never runs the `hermes` command itself.
### Answer the test question [#answer-the-test-question]
Setup sends a test push, then a test question. Answer it on your phone. The terminal says **You're set** once your answer arrives.
### Start a new Hermes session [#start-a-new-hermes-session]
Hermes loads plugins when it starts.
On a Mac you can use [Pushary Isle](/docs/agents/mac-app) instead. Connecting your agents there installs the plugin into Hermes's own Python, selects Pushary as the approval transport, and saves your key in `~/.pushary/config.json`, where the plugin reads it. It needs Hermes installed with its Python environment in place.
## Install from the Hermes plugin catalog [#install-from-the-hermes-plugin-catalog]
Pushary is listed in the [Hermes Agent plugin catalog](https://github.com/NousResearch/hermes-agent/blob/main/plugin-catalog/pushary.yaml), so Hermes can install it for you:
```bash
hermes plugins install pushary
```
The catalog installs the release Nous reviewed, pinned to a commit. On Hermes v2026.9.11, run `hermes update` before installing: that release can read the catalog but lacks the redaction function the pinned plugin needs, so its approval cards would arrive without the command. Then set your API key in your shell profile and select Pushary as the approval transport, as shown in [Set it up by hand](#set-it-up-by-hand):
```bash
export PUSHARY_API_KEY="pk_xxx.xxx"
```
Do not run a second installer over the catalog copy.
## What setup changes on your computer [#what-setup-changes-on-your-computer]
| File | What changes |
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Hermes's Python environment | Installs `hermes-plugin-pushary`. Setup looks for the Python named in the `hermes` launcher, then in `~/.hermes/hermes-agent/venv`. |
| `~/.hermes/config.yaml` | Enables the plugin, selects Pushary as the approval transport with Hermes's own prompt as the fallback, and turns off Hermes's terminal `clarify` tool so questions go through `pushary_ask`. |
| `~/.pushary/setup/hermes-config.json` | A record of what `config.yaml` had before, so `npx pushary@latest clean` can put it back. |
| `~/.pushary/config.json` and your shell profile | Your API key. The plugin reads `PUSHARY_API_KEY` first, then `~/.pushary/config.json`. |
The settings setup writes to `~/.hermes/config.yaml`:
```yaml
plugins:
enabled:
- pushary
security:
approval:
transport: pushary
transport_fallback: builtin
agent:
disabled_toolsets:
- clarify
```
## Check that it works [#check-that-it-works]
```bash
npx pushary@latest doctor
```
Doctor checks your key and plan, that the plugin is installed and enabled, that approvals go through Pushary, and that questions go through Pushary. It sends a real test push. Add `--roundtrip` to send a test question too.
Then ask Hermes to do something its dangerous-command check flags, such as deleting a scratch folder. Your phone buzzes with Hermes's four choices.
## Approve Hermes's dangerous commands from your phone [#approve-hermess-dangerous-commands-from-your-phone]
Hermes already detects dangerous commands and asks a person before it runs them. With `security.approval.transport: pushary`, that question goes to your phone instead of a terminal nobody is watching. You get the same four choices Hermes offers in the terminal:
| Choice | Effect |
| ---------------------- | ------------------------------------------------------ |
| Allow once | Runs this command. |
| Allow for this session | Runs it, and stops asking for the rest of the session. |
| Always allow | Runs it, and writes a standing allow rule. |
| Deny | Blocks it. The agent is told you did not consent. |
How long your phone has depends on your approval mode. In Always, it is Hermes's own `approvals.timeout` (300 seconds by default). In When away, your phone has the push window, then Hermes's own prompt takes over.
`transport_fallback: builtin` makes the transport safe to leave on. If your key is missing, your mode is In agent, or no device can be reached, Pushary steps aside (withdrawing any question it already sent) and Hermes asks in its own prompt. If cancellation says stop, or Pushary cannot confirm the withdrawal, the command is denied. A network failure while Pushary waits never reopens approval silently. Remove the `transport_fallback` line and an unreachable Pushary becomes a denial instead.
### Gate named tools with PUSHARY\_GATE\_TOOLS [#gate-named-tools-with-pushary_gate_tools]
To gate specific tools rather than Hermes's dangerous-command set, list them in `PUSHARY_GATE_TOOLS`. The check runs in the plugin's blocking `pre_tool_call` hook.
```bash
export PUSHARY_GATE_TOOLS="terminal,write_file"
export PUSHARY_GATE_TIMEOUT_MS=25000
```
| Outcome | Result |
| -------------------------------------------- | ----------------------------------------------- |
| You approve | The tool runs. |
| You deny | The tool is blocked, and the agent is told why. |
| No answer in the window | The tool is blocked. |
| Pushary unreachable, or no API key | The tool is blocked. |
| No phone, browser or Slack channel connected | The tool is blocked, with that as the reason. |
Hermes abandons any `pre_tool_call` callback that outlives `plugins.hook_callback_timeout` (30 seconds by default). The plugin keeps its wait inside that limit. `PUSHARY_GATE_TIMEOUT_MS` defaults to 55000 and is capped by it. Raise `plugins.hook_callback_timeout` to give your phone longer, up to the plugin's own limit of 55 seconds.
When Pushary is also the selected transport, the transport wins. The hook hands the call to Hermes's own approval gate, which gets the full 300-second window and the four choices. **Always allow** on such a call creates a standing rule for the whole tool name, including future calls with different arguments. Choose **Allow once** when you mean only the call on screen.
### Hermes approvals off (dangerously skip permissions) [#hermes-approvals-off-dangerously-skip-permissions]
Hermes's `approvals.mode: off` turns off its native approval enforcement. Hermes then raises no dangerous-command approvals, so the Pushary transport has nothing to send. Plain MCP, the plugin's question tools and the skill only make questions cooperative: the agent decides whether to ask, so they are not an enforced gate. `PUSHARY_GATE_TOOLS` with no transport selected asks Pushary directly from the plugin's own hook. With the transport selected, it hands the call to Hermes's approval gate instead.
## Hermes notifications and tools [#hermes-notifications-and-tools]
The plugin registers 7 native Hermes tools:
| Tool | What it does |
| ----------------------- | ------------------------------------------------------------------ |
| `pushary_notify` | Send a push with optional rich context. |
| `pushary_ask` | Ask a question: yes or no, multiple choice, or free text. |
| `pushary_wait` | Poll once for the answer to a question created with `wait=false`. |
| `pushary_cancel` | Cancel a pending question. |
| `pushary_propose_scope` | Ask you to agree a file boundary or record a promise before a run. |
| `pushary_enroll` | Connect one of your application's end users (Partner plan). |
| `pushary_ask_end_user` | Ask an enrolled end user (Partner plan). |
Automatic notifications:
* **Tool errors:** a push when any tool returns an error, at most 3 per session.
* **Session end:** set `PUSHARY_AUTO_NOTIFY_SESSION_END=1` to get a summary with tool and error counts when a session ends. Completed sessions use task-completion routing.
Hermes redacts gated argument summaries, targets and automatic error text before they are shortened or sent. If the host redactor is missing or fails, that detail is left out.
For questions, `pushary_ask` does its first wait and one server-directed poll per call; use pushary\_wait with the same id while pending. Follow `answered`, `status` and `handoffAction` (or `nextAction`). Do not create a duplicate question or cancel a pending one because a poll ended. Cancel a live question before moving it to the current chat, stop if cancellation says `stop`, and if cancellation loses a race, poll once for one second and honor the winning answer. Silence is never approval.
### Environment variables [#environment-variables]
| Variable | What it does |
| --------------------------------- | ----------------------------------------------------------------------------------------- |
| `PUSHARY_API_KEY` | Your API key. Overrides the key in `~/.pushary/config.json`. |
| `PUSHARY_CONFIG_FILE` | Read the key from a different file than `~/.pushary/config.json`. |
| `PUSHARY_AGENT_NAME` | Label shown in notifications. Defaults to `Hermes`. Example: `"Hermes - daily-briefing"`. |
| `PUSHARY_AUTO_NOTIFY_SESSION_END` | Set to `1` to get notified when sessions end. |
| `PUSHARY_GATE_TOOLS` | Comma-separated tool names to gate in `pre_tool_call`. |
| `PUSHARY_GATE_TIMEOUT_MS` | How long the per-tool gate waits, in milliseconds. Defaults to 55000. |
### When to use Pushary and when to use Hermes's chat platforms [#when-to-use-pushary-and-when-to-use-hermess-chat-platforms]
Hermes can also talk to you through chat platforms such as Telegram, Discord and Slack. Use Pushary when you have no active chat open, when a quick decision should reach your lock screen, when a background task or cron job finishes, or when Hermes flags a dangerous command. Use the active chat platform when you are already in the conversation and the question is part of it.
## Set it up by hand [#set-it-up-by-hand]
Install the plugin into the Python that Hermes runs in, not your system Python:
```bash
~/.hermes/hermes-agent/venv/bin/python -m pip install hermes-plugin-pushary
```
Add the settings from [What setup changes on your computer](#what-setup-changes-on-your-computer) to `~/.hermes/config.yaml`. Current Hermes also accepts `hermes plugins enable pushary` for the first part.
Save your key in your shell profile:
```bash
export PUSHARY_API_KEY="pk_xxx.xxx"
```
Start a new Hermes session, then run `npx pushary@latest doctor`.
### Use MCP instead of the plugin [#use-mcp-instead-of-the-plugin]
You can connect Hermes to the Pushary MCP server instead. Add this to `~/.hermes/config.yaml`:
```yaml
mcp_servers:
pushary:
url: https://pushary.com/api/mcp/mcp
headers:
Authorization: "Bearer ${PUSHARY_API_KEY}"
```
Then install the Pushary skill, so Hermes knows when to notify you and how to choose between Pushary and the active chat platform:
```bash
hermes skills tap add Pushary/pushary-skill
hermes skills install pushary
```
The MCP route gives Hermes the agent tools under their MCP names (`send_notification`, `ask_user`, `wait_for_answer`, `cancel_question` and `propose_scope`). It has none of the plugin's automatic notifications and no approval transport.
## Turn it off [#turn-it-off]
There is no `pushary disconnect` for Hermes. Edit `~/.hermes/config.yaml` instead:
* To send approvals back to the terminal, remove `security.approval.transport`.
* To bring back terminal questions, remove `clarify` from `agent.disabled_toolsets`.
`npx pushary@latest clean` removes Pushary from this computer, for every agent, and puts the Hermes settings back from its record. Add `--dry-run` to see what it would change first.
## Troubleshooting [#troubleshooting]
### Setup says the Hermes CLI was not found [#setup-says-the-hermes-cli-was-not-found]
Setup looks for `hermes` on your PATH. Install Hermes, check that `hermes` runs in a new terminal, then run setup again.
### Setup could not locate the Python that Hermes runs in [#setup-could-not-locate-the-python-that-hermes-runs-in]
Install the plugin by hand with Hermes's own Python, as shown in [Set it up by hand](#set-it-up-by-hand).
### Setup installed the plugin but could not edit Hermes's config [#setup-installed-the-plugin-but-could-not-edit-hermess-config]
Add the settings from [What setup changes on your computer](#what-setup-changes-on-your-computer) to `~/.hermes/config.yaml` yourself.
### Doctor says approvals stay in the terminal [#doctor-says-approvals-stay-in-the-terminal]
`security.approval.transport` is not `pushary` in `~/.hermes/config.yaml`. Run `npx pushary@latest setup --agents hermes` again, or set it by hand.
## Next steps [#next-steps]
---
# Langdock
Source: https://pushary.com/docs/agents/guides/langdock
Last updated: 2026-10-04
> Connect Pushary to Langdock as a remote MCP integration, so Langdock chats, agents and workflows can notify you and ask you questions on your phone.
Langdock can use Pushary in chats, agents and workflows through a remote MCP integration. MCP (Model Context Protocol) is the standard way AI apps call outside tools. Once connected, a Langdock agent can send you a push notification and ask you a yes/no, choice or free-text question that you answer on your phone.
This is the server URL you enter in Langdock:
```text
https://pushary.com/api/mcp/mcp
```
Langdock's agent chooses when to send a notification or ask a question. This connection does not hold a separate action in your workflow until you approve it.
## Connect Pushary [#connect-pushary]
You need Langdock's **Create integrations** permission. If **Create** is missing, ask a workspace admin to create the connection or give you that permission.
### Create an MCP integration [#create-an-mcp-integration]
Open **Plugins** in Langdock, then select **Create > Integration > MCP Integration**.
### Enter the server URL [#enter-the-server-url]
Enter `https://pushary.com/api/mcp/mcp` as the server URL.
### Choose OAuth and connect [#choose-oauth-and-connect]
Choose **OAuth 2.0 (Dynamic Client Registration)**, then **Create and connect**. Langdock registers itself with Pushary, so you need no client ID or secret.
### Sign in to Pushary [#sign-in-to-pushary]
Sign in to the Pushary account that owns your devices.
### Pick the tools [#pick-the-tools]
Select **List Server Features**. Keep `send_notification`, `ask_user`, `wait_for_answer` and `cancel_question` turned on. Langdock may select every feature it finds. Turn off `propose_scope`, because its file-path check does not apply to Langdock chats. The five documentation resources are optional. Save the integration.
### Turn it on in a chat [#turn-it-on-in-a-chat]
In a new Langdock chat, open **Plugins** and select **Pushary**.
### Send a test [#send-a-test]
Ask Langdock to send a test notification to your phone. Then ask it a harmless yes/no question through `ask_user`, and answer it on your phone. Langdock shows your answer.
## Connect with an API key instead [#connect-with-an-api-key-instead]
If your workspace cannot complete the OAuth setup, choose **API Key** with the **Authorization: Bearer** header type. Create a key in [Settings > API keys](https://pushary.com/dashboard/agent/settings) and paste it into Langdock's API key field. Keep the key out of the server URL and out of shared instructions.
With an API key, Langdock may list more Pushary tools. Keep only the four tools above turned on.
Langdock can ask for your confirmation before an MCP tool runs. You can turn that on in Langdock after saving the integration. It controls the tool call inside Langdock. It does not prove that an unrelated business action waited for your approval.
## Why does the Pushary tool fail in Langdock? [#why-does-the-pushary-tool-fail-in-langdock]
If the tool shows up but a call fails, check three things:
1. You signed in with the Pushary account that owns your phone.
2. The chat uses the right Langdock connection.
3. The four tools are turned on in the integration.
See [Langdock's MCP setup guide](https://docs.langdock.com/en/using-langdock/guides/integrations/mcp/mcp) for its current screens.
## Next steps [#next-steps]
---
# Langflow
Source: https://pushary.com/docs/agents/guides/langflow
Last updated: 2026-10-04
> Connect a Langflow agent to Pushary with the MCP Tools component, so it can send you updates and ask you questions you answer on your phone.
Langflow agents can use Pushary through Langflow's MCP Tools component. MCP (Model Context Protocol) is the standard way AI apps call outside tools. You store your Pushary key as a Langflow credential, add Pushary as an MCP server, and connect it to an Agent. The agent can then send you updates and ask you questions on your phone.
This is the server URL you enter in Langflow:
```text
https://pushary.com/api/mcp/mcp
```
The agent chooses when to call a tool. This connection does not hold a separate action in a flow until you approve it.
## Connect Pushary [#connect-pushary]
You need a Pushary account with a connected phone and an API key from [Settings > API keys](https://pushary.com/dashboard/agent/settings). Use the key for the account that owns your phone. Do not put the key in a flow, a prompt or the server URL.
### Store your key as a credential [#store-your-key-as-a-credential]
In Langflow, open **Settings > Global Variables > Add New**. Name the variable `PUSHARY_BEARER` and choose **Credential**. Set its value to `Bearer ` followed by your key, then save:
```text
Bearer pk_xxx.xxx
```
### Add the MCP server [#add-the-mcp-server]
Open **Settings > MCP Servers > Add MCP Server** and choose **Streamable HTTP/SSE**. Name the server `pushary` and set the URL to `https://pushary.com/api/mcp/mcp`.
### Add the header [#add-the-header]
Under **Headers**, set the key to `Authorization` and select `PUSHARY_BEARER` as its value. Save the server. Langflow lists the Pushary tools.
### Build the flow [#build-the-flow]
In a flow, add **Chat Input**, **Agent** and **Chat Output**. Choose a model provider for the Agent.
### Give the Agent the Pushary tools [#give-the-agent-the-pushary-tools]
Drag `pushary` from the **MCP** sidebar onto the canvas. Turn on **Tool Mode** on the MCP Tools component, and connect its **Toolset** output to the Agent's **Tools** input.
### Tell the Agent when to ask [#tell-the-agent-when-to-ask]
In the Agent's instructions, say when to use `ask_user` and `send_notification`. For a yes/no question, use `ask_user` with `type: "confirm"` and wait for the answer. Treat no answer, a decline or an expired question as no approval.
### Test it in the Playground [#test-it-in-the-playground]
Ask the agent to send a harmless test notification. Then ask it to send a yes/no question through `ask_user`, and answer it on your phone. The answer shows in Langflow.
An Agent instruction guides the agent and enforces nothing. If an approval must protect a deployment, a payment or another business action, bind the exact action and recipient in your application, and verify the Pushary decision before you run that action. Partner workflows that address customers use the Partner server API and customer IDs instead of this personal MCP connection.
## Why does the test fail? [#why-does-the-test-fail]
* **The result shows `sent: 0` or a `no_recipients` status.** No phone is connected. Connect one in Pushary under **Settings > Connections**.
* **Langflow lists the tools, but a call returns `401 Unauthorized`.** Check that `PUSHARY_BEARER` holds your current key with the `Bearer ` prefix. Langflow can list the tools without a valid key, so a tool list alone does not prove the key works.
See [Langflow's MCP client guide](https://docs.langflow.org/mcp-client) for its current editor controls.
## Next steps [#next-steps]
---
# Lovable
Source: https://pushary.com/docs/agents/guides/lovable
Last updated: 2026-10-06
> Connect Pushary to Lovable as a personal MCP connector, so the Lovable agent pings your phone when a build finishes or it needs your input.
[Lovable](https://lovable.dev) supports custom MCP servers as personal connectors on its paid plans. MCP (Model Context Protocol) is the standard way AI apps call outside tools. Add Pushary as a connector, and the Lovable agent can send you a push notification when a build finishes and ask you questions you answer from your lock screen.
This is the server URL you paste into Lovable:
```text
https://pushary.com/api/mcp/mcp
```
The Lovable agent decides when to ask. Pushary cannot hold a change in Lovable until you approve it. For enforced approvals (an action blocked until you say yes), use an agent with hooks. [Supported agents](/docs/agents/supported-agents) shows which ones Pushary can enforce.
## Connect Pushary in Lovable [#connect-pushary-in-lovable]
### Get an API key [#get-an-api-key]
Create one in [Settings > API keys](https://pushary.com/dashboard/agent/settings). Pushary shows it once, so copy it right away. See [Get your API key](/docs/agents/api-key).
### Open personal connectors [#open-personal-connectors]
In Lovable, go to **Settings > Connectors > Personal connectors** and click **New MCP server**.
### Name the server and paste the URL [#name-the-server-and-paste-the-url]
Set **Server name** to `Pushary`. Set **Server URL** to `https://pushary.com/api/mcp/mcp`.
### Add your key [#add-your-key]
Under **Authentication**, select **Bearer token** and paste your API key (`pk_xxx.xxx`). Click **Add server**.
### Send a test notification [#send-a-test-notification]
In a Lovable chat, ask: "Use Pushary to send me a test notification." Your phone shows it.
## Make Lovable notify you on its own [#make-lovable-notify-you-on-its-own]
The connector gives Lovable the tools. To get Lovable to use them without being asked, give it the Pushary instructions. Agents on your own computer install these with `npx skills add Pushary/pushary-skill`. Lovable is a hosted builder, so you paste the same guidance into Lovable's **Knowledge** (**Settings > Knowledge**) instead:
```text
# Pushary notifications
You have a Pushary MCP connector with tools: send_notification, ask_user,
wait_for_answer, cancel_question. Use them proactively, do not wait to be asked.
- When you finish a task that took several steps, call send_notification with a
short title and a one-line summary. Pass agentName "Lovable - {project}".
- Before a destructive or irreversible change (deleting data or files, dropping or
altering a table, deploying), call ask_user with type "confirm" and wait for the
answer. If the answer is no, do not proceed.
- If no answer comes back, follow handoffAction when present, otherwise nextAction:
keep waiting with the same correlationId while pending. Only for an explicit handoff, call cancel_question
and ask me in the chat. If cancellation returns handoffAction "stop", stop. If
cancellation returns false, poll once for 1 second and honor the answer that won
the race. Silence is never a yes.
- When a choice has 2 to 6 known options, use ask_user with type "select".
- Keep titles under 60 characters and bodies under 200. Max 3 notifications per task.
```
Lovable now notifies you when it finishes and checks in before risky steps, without a reminder in each prompt.
## Prompts to try [#prompts-to-try]
You can also ask for it in any prompt.
**Notify when a build finishes:**
```text
After you finish implementing this feature, send me a Pushary notification with
title "Lovable build done" and a one-line summary of what changed.
```
**Ask before a risky change:**
```text
Before you delete or rename any database table, use Pushary ask_user to confirm
with me, and wait for my answer before continuing.
```
A question blocks for up to about 55 seconds per call. If you need longer, the agent can check again with `wait_for_answer` and the same question id. Questions stay open for 10 minutes.
## Tools Lovable gets [#tools-lovable-gets]
The main tools are `send_notification`, `ask_user`, `wait_for_answer`, `cancel_question` and `list_sessions`. The full list depends on your key. See the [tools reference](/docs/agents/reference/tools).
## Next steps [#next-steps]
---
# Windsurf and other MCP agents
Source: https://pushary.com/docs/agents/guides/windsurf-other
Last updated: 2026-10-07
> Connect Windsurf or any MCP client to Pushary with one config block, so your agent can send push notifications and ask you questions on your phone.
Windsurf and other MCP clients connect to Pushary with one config block: the server URL and your API key. MCP (Model Context Protocol) is the standard way AI apps call outside tools. Your agent then gets Pushary's notification and question tools, so it can tell you when a task finishes and ask you questions you answer on your phone.
```json
{
"mcpServers": {
"pushary": {
"url": "https://pushary.com/api/mcp/mcp",
"headers": {
"Authorization": "Bearer pk_xxx.xxx"
}
}
}
}
```
The agent chooses when to ask. Pushary cannot hold an action in these clients until you approve it. For enforced approvals, use an agent with hooks, such as Claude Code or Codex. [Supported agents](/docs/agents/supported-agents) shows which ones Pushary can enforce.
## Connect over MCP [#connect-over-mcp]
### Get an API key [#get-an-api-key]
Create one in [Settings > API keys](https://pushary.com/dashboard/agent/settings). It looks like `pk_` followed by 24 hex characters, a dot, then a 48 hex character secret. Pushary shows it once, so copy it right away. See [Get your API key](/docs/agents/api-key).
### Add the config block [#add-the-config-block]
Paste the block above into your client's MCP configuration file. Each client keeps it in a different place. Replace `pk_xxx.xxx` with your key.
### Check the connection [#check-the-connection]
Open your client's MCP settings. Pushary shows as connected, with its tools listed.
### Send a test notification [#send-a-test-notification]
Ask your agent: "Use Pushary to send me a test notification." Your phone shows it.
Prefer the terminal? `npx pushary@latest setup --agents custom` signs you in, saves a key, and prints the server URL and header to use.
## Clients that only support stdio [#clients-that-only-support-stdio]
Some clients cannot connect to an HTTP MCP server and only start local commands (stdio). For those, use the `mcp-remote` bridge, which runs locally and forwards to Pushary:
```json
{
"mcpServers": {
"pushary": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://pushary.com/api/mcp/mcp",
"--header",
"Authorization:Bearer pk_xxx.xxx"
]
}
}
}
```
## Teach your agent when to ask [#teach-your-agent-when-to-ask]
The tools alone do not tell your agent when to use them. Install the Pushary skill, a short instruction file, so your agent sends notifications and asks questions at the right moments:
```bash
npx skills add Pushary/pushary-skill
```
## Tools your agent gets [#tools-your-agent-gets]
The main tools are `send_notification`, `ask_user`, `wait_for_answer` and `cancel_question`. The full list depends on your key. See the [tools reference](/docs/agents/reference/tools).
A question blocks for up to about 55 seconds per call. While pending, the agent keeps checking with `wait_for_answer` and the same question id. Ordinary full-access MCP questions without a host deadline stay open until answered or cancelled. This preserves the question; it does not restart a closed agent.
## Tools that are not MCP clients [#tools-that-are-not-mcp-clients]
n8n, Zapier, Make and plain scripts can use the REST endpoint instead, at `https://pushary.com/api/v1/server`. See [Connect any agent](/docs/agents/connect-any-agent).
## Next steps [#next-steps]
---
# How it works
Source: https://pushary.com/docs/agents/how-it-works
Last updated: 2026-10-07
> How Pushary works for AI agents. A hosted MCP server your agent calls, hooks that hold risky actions until you approve, API key auth, limits and tools.
Pushary runs a hosted [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server. MCP is the standard way an AI agent calls outside tools. Your agent connects over HTTPS and gets tools such as `send_notification` and `ask_user`. Pushary delivers each call to your phone app, browser or Mac. For agents with hooks, Pushary also holds a risky action until you approve it.
Set it up with one command. It connects your agents, saves your API key and pairs your phone:
```bash
npx pushary@latest setup
```
## Architecture [#architecture]
```text
AI agent (Claude Code, Codex, Cursor, ChatGPT, ...)
|
| MCP over HTTPS (Streamable HTTP)
v
https://pushary.com/api/mcp/mcp
|
+--> Pushary phone app (iOS and Android)
+--> Push notifications in your browser
+--> Mac app (Pushary Isle)
+--> Slack approvals, if you connected Slack
```
Your answer travels back the same way. Answer on your phone and the agent's waiting `ask_user` call returns it, for example `"value": "yes"`.
The agent tools (`ask_user`, `send_notification` and the rest) run on the Pushary server and share one question store with the REST endpoint `POST /api/v1/server/ask`. The push platform tools (subscribers, campaigns, templates, flows) call the REST API at `https://pushary.com/api/v1/server`.
Agents reach Pushary in two ways.
### MCP tools: the agent decides to ask [#mcp-tools-the-agent-decides-to-ask]
The agent calls `ask_user` when it needs a decision, and `send_notification` when work finishes or fails. Every MCP client can do this, but the agent chooses when. This is the cooperative path.
### Hooks: Pushary steps in before an action [#hooks-pushary-steps-in-before-an-action]
For agents with hooks, such as Claude Code, Codex, Gemini CLI and Cursor, the setup command installs Pushary's hook. A hook is a small program the agent runs before each tool call:
```text
Agent is about to run a tool (for example Bash: git push --force)
-> the Pushary hook reads the tool call on stdin
-> fetches your permission policy from /api/mcp/policy
-> if the policy needs you, asks your phone and waits
-> returns allow or deny, or hands the call to the agent's own prompt
```
If Pushary cannot be reached, the hook gives no opinion and the agent's own prompt decides. What happens when you do not answer depends on your [policy](/docs/agents/policies). By default the agent's own prompt takes over, as described in [When you don't answer](/docs/agents/unanswered). The [supported agents](/docs/agents/supported-agents) table lists which agents have hooks.
## Connect an MCP client [#connect-an-mcp-client]
### Streamable HTTP (recommended) [#streamable-http-recommended]
Most MCP clients accept a URL and a header:
```json
{
"pushary": {
"url": "https://pushary.com/api/mcp/mcp",
"headers": {
"Authorization": "Bearer pk_xxx.xxx"
}
}
}
```
### stdio via mcp-remote [#stdio-via-mcp-remote]
For a client that only supports stdio, use the `mcp-remote` bridge:
```json
{
"pushary": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://pushary.com/api/mcp/mcp",
"--header",
"Authorization:Bearer pk_xxx.xxx"
]
}
}
```
### Claude and ChatGPT [#claude-and-chatgpt]
Claude.ai, Claude Desktop and ChatGPT cannot send a header. You add Pushary there as a custom connector and sign in to your Pushary account instead. Follow the [Claude setup guide](/docs/agents/guides/claude-desktop) or the [ChatGPT setup guide](/docs/agents/guides/chatgpt).
### Add the Pushary skill [#add-the-pushary-skill]
The skill teaches your agent when to notify you and when to ask:
```bash
npx skills add pushary/pushary-skill
```
It works with Claude Code, Codex, Cursor, Windsurf and the other agents listed on [skills.sh](https://skills.sh). It includes the tool documentation and guidance on when a notification is worth sending.
## Authentication [#authentication]
Every request sends your API key as a Bearer token:
```text
Authorization: Bearer pk_xxx.xxx
```
A key is `pk_` plus 24 hex characters, a dot, then a 48 hex character secret. It belongs to one workspace's agent site, and Pushary shows it only once, when it is created. See [Get your API key](/docs/agents/api-key).
* A missing, wrong or revoked key gets HTTP 401, and every tool call fails.
* A workspace with no active plan or trial gets HTTP 403 with `"code": "subscription_required"`. `ask_user`, `send_notification` and `propose_scope` stop working. `wait_for_answer` and `cancel_question` still work, so an agent can close out a question it already asked.
## How Pushary tells sessions apart [#how-pushary-tells-sessions-apart]
When you run several agents at once, Pushary keeps them apart. The hook sends two fields with every call:
* `sessionId`: one per agent window, taken from the agent.
* `machineId`: a random ID the CLI creates once and stores in `~/.pushary/machine-id`.
The pair identifies each session. Two Claude Code windows in the same repo show up as two sessions, and a blocked approval points at the exact one that raised it. See the [Multi-Agent Control Panel](/docs/agents/control-panel).
## Task titles and privacy [#task-titles-and-privacy]
For Claude Code, setup also registers a UserPromptSubmit hook. When you submit a prompt, the hook sends its first 120 characters to Pushary as the session's task title. The dashboard then shows what each session is working on instead of a bare session ID. Each new prompt replaces the title.
The CLI hook sends no other prompt text unencrypted. Approval requests do carry the command or diff you are asked to approve, with secrets redacted.
Sessions that Pushary launches, such as one you start from your phone, sync a transcript by default. It is redacted on your machine and encrypted there, for your phone and for Pushary's documented compliance recovery, before upload.
### Turn off task titles [#turn-off-task-titles]
Set this where your agent runs:
```bash
export PUSHARY_TASK_TITLES=off
```
With titles off, the hook still reports that a prompt was submitted, so the session shows as active. It sends no prompt text at all.
### Turn off transcript sync [#turn-off-transcript-sync]
Run this on the agent machine:
```bash
npx pushary@latest setup --transcripts off
```
Setting `PUSHARY_TRANSCRIPTS=off` in the agent's environment does the same.
## Rate limits [#rate-limits]
Questions and notifications from the agent tools have their own limits. A burst of parallel agents still gets through, and one runaway agent is still capped:
* **3** pending questions per agent session
* **25** pending questions per site
* **10** notifications per 5 minutes per site
A question frees its slot when it is answered, cancelled or expires. Ordinary full-access MCP questions without a host deadline stay answerable and keep their slots until answered or cancelled. Restricted connectors, REST asks and native permission hooks retain their existing deadlines.
Connector keys get their own counters, so a leaked connector link cannot use up the limit your CLI agents rely on. REST calls to `/api/v1/server` also have a per-plan request limit, for example 120 requests a minute on the Agent plan. See [Pricing and limits](/docs/pricing-and-limits).
## Which tools your key gets [#which-tools-your-key-gets]
A connector key gets the five agent tools. A key from setup or phone pairing adds `list_sessions`. A key you create in **Settings > API keys** also gets the push platform tools. The Partner and Enterprise plans add durable decisions for your own end users.
| Category | Tools | What they do |
| --------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
| **Notifications** | `send_notification` | Send a push notification, with optional rich context |
| **Human in the loop** | `ask_user`, `propose_scope`, `wait_for_answer`, `cancel_question` | Ask a question, agree a run's scope, wait for the answer, retract a question |
| **Sessions** | `list_sessions` | Read the live list of agent sessions (not connector keys) |
| **Subscribers** | `list_subscribers`, `get_subscriber`, `count_subscribers` | Look up browser subscribers |
| **Campaigns** | `list_campaigns`, `create_campaign`, `get_campaign`, `send_campaign`, `pause_campaign`, `get_campaign_stats` | Create and manage campaigns |
| **Templates** | `list_templates`, `create_template` | Reusable notification templates |
| **Flows** | `list_flows`, `create_flow`, `activate_flow`, `pause_flow` | Automation flows, such as a welcome series |
| **Partner decisions** | `enroll_end_user`, `create_decision`, `get_decision`, `cancel_decision` | Connect an end user and collect a durable decision |
## Next steps [#next-steps]
---
# Human in the loop
Source: https://pushary.com/docs/agents/human-in-the-loop
Last updated: 2026-10-07
> Add human in the loop approvals to your AI agent with Pushary's ask_user MCP tool. Ask for a yes or no, a choice or text, and handle unanswered questions.
Human in the loop means your AI agent stops and asks you before it continues. With Pushary, the agent calls the `ask_user` MCP tool, you answer on your phone, and the agent reads your answer from the same call. This page covers an agent asking **you**, the workspace owner. If your product needs approval from **your customers**, use the [Partner integration](/docs/agents/embed) and its durable Decisions API.
Connect your agent and your phone first:
```bash
npx pushary@latest setup
```
Setup installs the MCP connection and agent instructions, plus hooks where the agent supports them. MCP gives the agent tools it can call. Approval before other tool calls is enforced only through the agent's hook integration and your [permission policies](/docs/agents/policies).
## Ask a question and read the answer [#ask-a-question-and-read-the-answer]
`ask_user` **waits by default**. It can return the answer in the first call, with no second call needed.
```json
{
"tool": "ask_user",
"arguments": {
"question": "Deploy this release to staging?",
"type": "confirm",
"agentName": "Codex - my-project",
"toolName": "Bash",
"toolTarget": "deploy staging"
}
}
```
An answered question returns:
```json
{
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"question": "Deploy this release to staging?",
"type": "confirm",
"status": "answered",
"answered": true,
"value": "yes"
}
```
For a confirm question, proceed only on `answered: true` with `value: "yes"`. `answered: true` with `value: "no"` is a refusal. An unanswered question never becomes an approval.
| Question type | You send | The answer |
| ------------- | ---------------------------------------- | ------------------------ |
| `confirm` | A yes or no question | `"yes"` or `"no"` |
| `select` | A question and 2 to 6 `options` | The chosen option string |
| `input` | A question and an optional `placeholder` | The text the user typed |
Use `context` for background, `agentName` to name the project, and `toolName` and `toolTarget` for a tool approval. Ask only when you need the user's input. A notification on its own does not approve an action.
## Handle a question nobody answered [#handle-a-question-nobody-answered]
Read `handoffAction` first when it is present. Otherwise follow `nextAction`. An unanswered result can mean a live timeout, a delivery mode handoff, or a final state.
1. If the result has `nextAction: "wait_for_answer"`, keep polling with the same
`correlationId` and `timeoutMs: 55000`.
2. Only for an explicit handoff, cancel the phone question before asking in the
current chat.
3. If cancellation returns `handoffAction: "stop"`, stop. Otherwise, if
cancellation returns false, poll once for 1 second and honor any answer that
won the race.
4. Ask in the current client only after the phone question is safely closed and
no answer won the race.
```json
{
"tool": "wait_for_answer",
"arguments": {
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timeoutMs": 55000
}
}
```
Only `pending` is a live unanswered state. `cancelled`, `expired`, `missing` and `unavailable` are not timeouts. Never turn a `stop` directive into another approval request.
While the agent waits, show the returned `answerUrl` so the user can open the question in the dashboard. For the full handoff contract, see [When you don't answer](/docs/agents/unanswered).
## Where you answer: delivery modes [#where-you-answer-delivery-modes]
| Mode | What happens |
| -------------------------- | -------------------------------------------------------------------------------------------- |
| When away (`push_first`) | Uses presence to decide whether to send the phone push, and waits for the push-first window. |
| Always (`push_only`) | Sends to the phone and waits for the policy timeout. |
| Alert only (`notify_only`) | Sends an alert only. The approval belongs in the current client. |
| In agent (`terminal_only`) | Keeps the decision in the current client, with no phone push. |
In Claude Code, a command your own Claude Code allow rules already cover is left to Claude Code in every mode. The exceptions are a command that a Pushary rule or preset you chose names, and one that looks risky. Pushary records no decision for it.
Always read the returned directives. One call waits at most 55 seconds. Ordinary full-access MCP questions without a host deadline stay answerable until answered or cancelled, with one phone reminder after ten minutes when delivery settings allow it. Restricted connectors and native permission hooks retain their existing deadlines. The end of a wait is not consent.
Set `wait: false` only if your code will collect the answer later. The [tools reference](/docs/agents/reference/tools#ask_user) lists every parameter.
## Cancel a question you no longer need [#cancel-a-question-you-no-longer-need]
Cancel when the action is abandoned or replaced, or the user answers in chat:
```json
{
"tool": "cancel_question",
"arguments": {
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"handoff": true
}
}
```
Use `handoff: true` when you move the same question into the current client. Apply the race handling above before you act on a chat answer.
## Agree on a run's scope [#agree-on-a-runs-scope]
For a run that touches several files, `propose_scope` records the allowed paths, the off-limits paths and the definition of done under a session ID. Only `ratified: true` creates the contract on the server.
Scope **narrows** the file boundary. It does not approve risky actions inside it. Hook enforcement needs a supported agent with `@pushary/agent-hooks` 0.59.0 or later. Shell commands stay governed by your permission policy. See [propose\_scope](/docs/agents/reference/tools#propose_scope).
## Next steps [#next-steps]
---
# Kill switch
Source: https://pushary.com/docs/agents/kill-switch
Last updated: 2026-10-07
> Use the Pushary kill switch to stop a runaway AI agent from your phone or the dashboard. Every tool call in that session is denied for one hour.
The kill switch stops one agent session, or all of them, in the middle of a run. You tap Stop in the Pushary phone app, on a question's web page, or in the dashboard. From then on the agent's hook denies every tool call that session tries, including calls your policy would auto-approve. The stop clears on its own after one hour.
## Stop an agent [#stop-an-agent]
### From the phone app [#from-the-phone-app]
Open the session and tap **Stop**, then confirm **Stop this agent?**. To halt every session at once, open the **Fleet** tab and tap **Stop all agents**. If the agent's machine can be reached, the app also ends the agent's process.
### From a question in your browser [#from-a-question-in-your-browser]
When an agent asks you something, the question's web page shows **Stop this agent** under the question. Tap it to halt that session and dismiss the question. No laptop needed.
### From the dashboard [#from-the-dashboard]
Open **Dashboard > Agent** at [pushary.com/dashboard/agent](https://pushary.com/dashboard/agent), find the session in the [blocked queue](/docs/agents/control-panel#the-blocked-queue), and click **Stop**. The dashboard says "Stopped. The agent's next tool call will be denied."
The agent's next tool call fails with this reason:
```text
Stopped by user, this agent was halted from Pushary
```
Every plan can stop an agent, from the dashboard, the phone app or a question's web page. Read-only teammates cannot stop an agent.
## What a stop does [#what-a-stop-does]
* **Denies every tool call in that session.** Each call checks the kill switch first, in every approval mode and every Claude Code permission mode, including plan and auto.
* **Cancels the session's open questions**, so a stale approval does not linger on your phone.
* **Ends the process when it can.** From the phone app, Pushary asks the machine to end the agent's process. That needs the machine online, a CLI new enough to do it, and a session that reported its process id. Otherwise the agent is only blocked, and it keeps running and spending tokens until you stop it from your terminal.
* **Leaves other sessions alone.** A stop is scoped to one session id, so your other agents keep working. **Stop all agents** is the site-wide version and halts every session.
* **Writes an audit entry.** A stop from the dashboard or a question's web page is recorded as **Agent stopped** in your [audit log](/docs/agents/audit-log).
## How the kill switch works [#how-the-kill-switch-works]
The stop is a small marker in Redis, keyed by your site and the session id. The hook checks it on every tool call, before anything else:
```text
tool call → stopped? deny
→ deny rule? deny
→ outside an agreed scope? ask you
→ your policy allow, ask your phone, or the agent's prompt
```
Because the stop is checked before the [auto-approve shortcut](/docs/agents/policies#the-auto-approve-shortcut), a stopped session is denied even for tools you always allow.
## When does the stop end? [#when-does-the-stop-end]
A stop expires one hour after you set it, so a forgotten stop never blocks a session for good. To get going sooner, start a new agent session: a new session has a new id, so a single-session stop does not apply to it. After **Stop all agents**, every session on the site stays stopped until the hour is up.
Pushary for Mac shows **Undo** after you stop a session from the notch. Undo lifts only the stop you just made. It is not offered when the session was already stopped from somewhere else, or when Pushary asked the machine to end the process, because a process cannot be started again. If another person stops the same session, or **Stop all agents** is on, Undo says the stop is still in force. Two stops on one session last as long as the longer of the two.
The hook reads the stop on every tool call, with a 3 second timeout. If it cannot reach Pushary, it keeps using a stop it saw in the last 60 seconds. After that it cannot know, so it auto-approves nothing except read-only commands it knows are safe, and hands other calls to the agent's own prompt. The kill switch is a remote control, not a sandbox. For a block that holds offline, add a deny rule to your [policy](/docs/agents/policies): the hook keeps a cached copy of your policy for up to 12 hours.
No. It denies the next tool call and everything after it. A tool already running when you tap Stop finishes. Ending the process from the phone app is the only way to cut that short.
A deny policy is a standing rule for one tool or command, for every agent on your site. The kill switch is a stop for one session right now, across all tools, including the ones you normally auto-approve. Use Stop when a run has gone wrong. Use a policy for "never let any agent do this."
No. Stop is a person's action, from the phone app, the dashboard or a signed question page. The read-only [`list_sessions`](/docs/agents/reference/tools#list_sessions) tool can see sessions but cannot steer them.
## Next steps [#next-steps]
---
# Mac app (Pushary Isle)
Source: https://pushary.com/docs/agents/mac-app
Last updated: 2026-10-08
> Install Pushary Isle, the Mac app. See your agents' questions in the notch, answer with one key, and let the same question reach your phone when you step away.
Pushary Isle is the Pushary app for Mac. When an AI agent such as Claude Code, Codex or Cursor stops to ask you something, the question shows up in your Mac's notch. You answer with one key and the agent keeps going. When you step away, the same question reaches your phone instead.
It is included in every agent plan. See [pricing](/pricing) for the plans, or the [Mac app page](/mac-notch-app) for a quick look.
## What you need [#what-you-need]
* macOS 14 or later, on Apple silicon or Intel.
* A Pushary account. New accounts start with a 3-day free trial.
* No notch? That is fine. On a Mac without one, Isle sits at the top of the screen.
## Install [#install]
Download the app and drag it to Applications:
```text
https://github.com/Pushary/pushary-mac/releases/latest/download/Pushary.dmg
```
Or install it with Homebrew:
```bash
brew install --cask pushary/tap/pushary
```
Both give you the same signed and notarised app. It checks for updates on its own.
## First launch [#first-launch]
1. **Sign in.** Use your Pushary account, or create one.
2. **Start your trial.** Three days free, then $9.99 a month. You finish checkout in your browser. If your account already has a plan or a trial, this step is skipped.
3. **Connect your agents.** Isle finds the agents on your Mac and sets them up for you. You do not edit any config files. If Isle cannot set one up, it shows you one command to run.
4. **Pair your phone (optional).** Isle shows a code. Scan it with your phone's camera. If you do not have the Pushary phone app yet, the link takes you to the store. You can skip this and pair later in Settings, under **Account**.
## Agents it connects [#agents-it-connects]
Isle sets up these agents on its own:
* Claude Code
* Codex
* Cursor
* Gemini CLI
* Antigravity CLI
* VS Code
* OpenCode
* Qwen Code, CodeBuddy, Qoder, Droid and Devin CLI
* Hermes, when Hermes and its Python environment are already installed
* fx
Other agents connected through Pushary, for example over [MCP](/docs/agents/connect-any-agent), also show up in the notch.
## Answer a question [#answer-a-question]
When an agent needs you, the notch opens on its own and shows the question, what the agent wants to run, and why. It stays closed if you are already looking at that agent's terminal.
A notch that opens on its own does not take the keyboard, so you keep typing where you were. These shortcuts work from any app:
| Shortcut | What it does |
| -------- | --------------------------------------- |
| `⌃⌘Y` | Approve |
| `⌃⌘N` | Deny |
| `⌃⌘P` | Open the notch and give it the keyboard |
| `⌃⌘⎋` | Close the notch |
You can change them in Settings, under **Shortcuts**.
Once the notch has the keyboard (click it, or press `⌃⌘P`), you can answer every kind of question:
| Question | How you answer |
| ------------------------- | ------------------------------------------------------------------------------------ |
| Yes or no | `⌘Y` approves. `⌘N` denies. |
| Deny and explain | Click **Deny with a reason** and type it. The agent reads your reason. |
| Pick an option | `⌘1` to `⌘9` picks the option with that number. |
| Type a reply | Type in the box and send. |
| Several questions at once | Answer each one, move between them with the arrow keys, then `⌘↩` sends all of them. |
Y or N on its own does nothing. Every key that answers a question needs ⌘, so typing in another app never answers a question by accident.
## See every session [#see-every-session]
Open the notch to see all your agent sessions in one list. Each row shows the project, what the agent is doing, and whether it needs you. The most urgent session sits at the top.
Need more detail before you answer? Open the session's terminal from the notch, and Isle brings that terminal to the front. To land on the exact tab, turn on **Jump to the Exact Terminal Tab** in Settings, under **General**. It works in Terminal and iTerm2, and in Ghostty 1.3 or later by project folder. macOS asks for Automation permission the first time.
## Step away from your Mac [#step-away-from-your-mac]
Isle and your phone share the same questions. Answer on one, and the question closes on the other.
Where approvals go is set by your approval mode. You can change it in the notch, under **Approvals**:
| Mode | In the notch | On your phone |
| -------------- | ------------------------- | --------------------------------------------------- |
| **When away** | The card shows here. | Quiet while you are at this Mac. |
| **Always** | The card shows here. | Gets every approval too. |
| **Alert only** | No card opens on its own. | Gets an alert. You decide in the agent. |
| **In agent** | No card opens on its own. | No approval push. The agent asks in its own prompt. |
See [Policies](/docs/agents/policies) for what each mode does across all your devices.
Agents that run on your Mac need your Mac awake and online. If you quit Isle, the agent asks Pushary directly. In **When away** and **Always**, the approval goes to your paired phone. With no phone paired, the agent asks in its own prompt. Cursor file edits have no prompt of their own, so keep Isle open if you rely on them.
## What it sends [#what-it-sends]
To show your sessions and keep your Mac and phone in sync, Pushary receives your agents' activity: the sessions, the tools they run, the project folder, and the start of your prompt as a task title. It also receives each question, the context around it, and your answer. That context can include a command, a file path, or text your agent wrote.
## Next steps [#next-steps]
---
# Permission policies
Source: https://pushary.com/docs/agents/policies
Last updated: 2026-10-12
> Set a permission policy for each agent tool. Run it silently, ask your phone, leave it to the agent's own prompt, or block it outright.
A permission policy decides what happens when your AI agent is about to run a tool, such as a shell command or a file edit. Pushary can run the call silently, ask your phone, leave it to the agent's own prompt, or block it. Each rule matches a tool pattern like `Bash`, `Bash(git status)` or `Bash(npm test:*)`, with a `*` rule as the fallback.
Policies are enforced by the hooks that `npx pushary@latest setup` installs. A hook is a small program the agent runs before each tool call. Agents connected over MCP only (Windsurf, Claude Desktop, Cowork, ChatGPT, or Cursor without its hooks) can send notifications and ask questions, but their tool calls are not gated. [Supported agents](/docs/agents/supported-agents) lists which agents enforce.
## Change a policy in the dashboard [#change-a-policy-in-the-dashboard]
### Open the policy editor [#open-the-policy-editor]
Go to [pushary.com/dashboard/agent/policies](https://pushary.com/dashboard/agent/policies).
### Pick a preset, or edit single rules [#pick-a-preset-or-edit-single-rules]
**Permission presets** set a whole agent at once: Cautious, Balanced, Autonomous or Custom. For one tool, open **Advanced: individual tool rules** and click **Edit** on a row.
### Set where you decide and what happens on a timeout [#set-where-you-decide-and-what-happens-on-a-timeout]
Choose **Where to decide** (the mode), **Timeout** (0 to 300 seconds) and **If you don't respond** (Auto approve, Auto deny, Ask in terminal, or Wait for phone). **Push window** applies only to When away.
### Add a rule for a specific tool or command [#add-a-rule-for-a-specific-tool-or-command]
Type a pattern such as `Bash`, `Bash(git status)`, `Bash(npm test:*)` or `Edit(src/**)`, choose **All repos** or one repository, and click **Add a rule**. The `*` row is the catch-all and cannot be removed.
The running agent picks up the change on its next tool call.
## The four approval modes [#the-four-approval-modes]
Every rule has a mode. The mode decides where the approval happens. New workspaces start on **Always**.
| Label | Mode id | Where you are asked |
| ---------- | --------------- | ------------------------------------------------------------------------------------------------------------- |
| When away | `push_first` | On your phone when you are away from the agent's machine. In the agent, or on the Mac app, when you are there |
| Always | `push_only` | On your phone for every approval, even while you are at the machine |
| Alert only | `notify_only` | In the agent's own prompt. Your phone gets an alert with no approve or deny |
| In agent | `terminal_only` | In the agent's own prompt. No approval alert |
### push\_first [#push_first]
Shown as **When away**. Pushary asks your phone only when it cannot place you at the agent's machine. Two signals place you there:
* The Pushary Mac app on that machine reports you are there: the screen is unlocked and awake, you used the keyboard or mouse in the last minute, and no quiet scene is on.
* A CLI agent hook on that machine recorded a prompt in the last five minutes and tool activity in the last minute.
Editor plugins report no presence of their own. Your phone is asked when neither signal holds, or when the question carries no machine id, as with Cowork and Claude Desktop.
While you are at the Mac, the decision stays on the Mac however long it waits. Time alone never sends it to your phone. Leave the Mac and the decision moves to your phone. When a CLI or editor agent skips your phone because you are at the keyboard, the decision goes straight to the agent's own prompt.
Some callers have no prompt of their own. They ask your phone every time, as under Always, and wait up to `timeoutSeconds`:
* a Cursor file edit, or a session started from your phone, from `pushary` 1.9.13
* Antigravity CLI, from 1.9.14
* a Codex edit outside a scope you agreed, from 1.9.15
* Codex set to never ask for approval (full access), from 1.9.17
In Codex full access, a call nobody answers is refused with "No response within timeout", even when the rule approves on timeout. Older versions refuse a Cursor file edit and tell you to switch to Always.
Anywhere else, when your phone is asked, the hook waits up to `pushFirstSeconds`. **Yes** runs the call, **No** denies it, and no answer hands the decision back to the agent's own prompt.
### push\_only [#push_only]
Shown as **Always**, and where a new workspace starts. Pushary sends the push for every approval, even while you are at the machine. The agent waits until you answer or `timeoutSeconds` runs out, then the rule's timeout action applies. On a Mac with the Pushary app, the card also shows there and the first answer wins. Use it for destructive operations where the agent should wait for you.
### terminal\_only [#terminal_only]
Shown as **In agent**. Pushary sends no approval alert, and no approval card opens on its own on the Mac (Pushary for Mac 0.1.35 or later). The agent's own permission flow decides. A host with no permission prompt of its own refuses the action, as described under Alert only. Task updates are a separate setting and still arrive if you turned them on. Use it for tools you always want to approve at the keyboard.
### notify\_only [#notify_only]
Shown as **Alert only**. Your phone gets a one-way alert titled **Agent needs approval**, with the tool description as the body. There is no approve or deny on the phone, and no approval card opens on its own on the Mac (Pushary for Mac 0.1.35 or later). You decide in the agent's own prompt, and the agent never waits on Pushary.
A host with no permission prompt of its own refuses the action. A Cursor file edit is denied, and from Cursor plugin 0.2.3 or `@pushary/agent-hooks` 0.95.7 the denial says why. The unattended Gemini bridge also refuses with an explanation from 0.95.7. Earlier versions pass the call to Gemini, which refuses it without saying why. Use Alert only when you want to know about approvals without a remote gate.
If the hook cannot reach Pushary, it returns no opinion, so the agent's own permission flow decides as if the hook were not installed. An offline agent cannot reach your phone anyway, so you approve at the keyboard. A host with no prompt of its own, such as a Cursor file hook, refuses the action instead.
## Policy fields [#policy-fields]
### The auto-approve shortcut [#the-auto-approve-shortcut]
A rule with `timeoutSeconds: 0` and `timeoutAction: "approve"` runs the call silently, in any mode. There is no push and no terminal prompt. This keeps read-only tools quiet, and it is what [Teach-on-Tap](#teach-on-tap-always-allow-this-tool) writes when you tap always allow.
The mirror also holds. `timeoutSeconds: 0` with `timeoutAction: "deny"` refuses the call at once, with no push and no prompt. Pair it with a pattern like `Bash(rm -rf:*)` to block a command family.
Read-only shell commands such as `ls` and `git status` run without asking under a bare `Bash` or `*` rule. A deny still covers them: under a `Bash` deny or a `*` deny they are denied too. The reverse also holds. A command Pushary rates destructive, such as `rm -rf`, never auto-approves through a bare `Bash`, `PowerShell` or `Monitor` rule or a `*` rule. In PowerShell that includes `Remove-Item -Recurse` under any of its aliases, and `cmd /c rd /s`. Only an exact or prefix rule you wrote yourself can approve it.
The [kill switch](/docs/agents/kill-switch) overrides the auto-approve shortcut. A stopped session is denied before any policy is read, so even silently approved tools stop.
## Pattern syntax [#pattern-syntax]
The `tool` field accepts four forms, modeled on Claude Code's own permission rules:
| Pattern | Matches | Example |
| ----------------- | ---------------------------------------------------------------------------------------- | ------------------ |
| `Tool` | Every call to that tool | `Bash` |
| `Tool(exact arg)` | Calls whose argument equals the text exactly | `Bash(git status)` |
| `Tool(prefix:*)` | Calls whose argument starts with the prefix | `Bash(npm test:*)` |
| `Tool(glob)` | Calls whose argument matches the glob. `*` stays inside one folder, `**` crosses folders | `Edit(src/**)` |
The argument depends on the tool:
| Tools | Argument |
| ------------------------------------ | --------------- |
| `Bash`, `PowerShell`, `Monitor` | `command` |
| `Read`, `Edit`, `Write`, `MultiEdit` | `file_path` |
| `NotebookEdit` | `notebook_path` |
| `LS` | `path` |
Other tools have no argument to match. The dashboard refuses an argument pattern for them, so write the bare tool name. A path is matched as written and also relative to the agent's working folder, so `Edit(src/**)` covers files under `src` in the current project.
Shell commands are split at `&&`, `||`, `;` and `|`, and each part is checked:
* A rule that approves needs every part to match. `Bash(npm test:*)` does not auto-approve `npm test && rm -rf dist`.
* A rule that denies or asks needs only one part to match. `Bash(git push:*)` catches `cd repo && git push`.
Argument patterns need `@pushary/agent-hooks` 0.13.0 or later, and path globs need 0.55.0 or later. On older versions such a rule never matches, and the bare-tool and `*` rules still apply.
### Which rule wins when several match [#which-rule-wins-when-several-match]
1. Your own rules come before preset rules, and preset rules come before Pushary's built-in defaults. One exception: a preset's rule for a specific tool beats your own `*` rule.
2. Within one layer, the most specific match wins: an exact argument, then a prefix or glob, then the bare tool name.
3. On a tie, a rule for one session or one repository beats a workspace-wide rule, then the longer pattern wins.
`*` is a literal catch-all, not a glob. It applies only when no other rule in its layer matches.
## Default policies [#default-policies]
A new workspace starts with one rule of its own, the catch-all `*`: Always, a 60 second timeout, and ask in the terminal if nobody answers. The rule also carries a 20 second push window, which applies once you switch it to When away. Because your own `*` rule outranks the built-in rules, it governs every tool until you add a specific rule or pick a preset.
A site with no `*` rule of its own falls back to these built-in rules:
| Tool | Timeout | If no response | Mode | Push window |
| ------- | ------- | --------------- | --------------- | ----------- |
| `Bash` | 60s | Auto deny | `push_first` | 20s |
| `Write` | 60s | Ask in terminal | `push_first` | 20s |
| `Edit` | 45s | Ask in terminal | `push_first` | 20s |
| `Read` | 0s | Auto approve | `terminal_only` | not used |
| `*` | 60s | Ask in terminal | `push_first` | 20s |
Built-in `Read` uses the auto-approve shortcut, so file reads never interrupt you. Built-in `Bash` denies on timeout: if you do not approve a shell command, it does not run.
The hook caches your policy in your temp folder as `pushary-policy-.json` for 60 seconds. Every change you save in the dashboard bumps a policy version that the hook checks on each tool call, so a running agent uses the new rule on its next call without a restart.
## Permission presets [#permission-presets]
A preset sets five capabilities for one agent, or for all agents: Read files, Edit and write files, Run shell commands, Reach the network, and Delete or force push.
| Preset | What the agent does on its own |
| ---------- | --------------------------------------------------------------------- |
| Cautious | Reads. Asks before editing, running commands, or reaching the network |
| Balanced | Reads and edits. Asks before running commands or reaching the network |
| Autonomous | Works on its own. Asks only for network and destructive actions |
| Custom | Your own mix, set per capability |
Destructive actions always ask, whatever you choose. An agent-specific preset takes effect once that agent's CLI is up to date. The All agents preset applies everywhere at once. A rule you write under **Advanced** can override a preset.
## Set the mode from the command line [#set-the-mode-from-the-command-line]
`pushary mode` changes the mode for the whole site:
```bash
npx pushary@latest mode push_only --for 30m # Always, for the next 30 minutes
npx pushary@latest mode status # show the current override
npx pushary@latest mode clear # back to your per-tool rules
```
With `--for`, the mode is a temporary override that wins over every rule's own mode until it expires. Durations are `m` or `h`, up to 24 hours. Without `--for`, the command changes the mode of your `*` rule and stays until you change it. `updates-only` is accepted as another name for `notify_only`.
`pushary wait ` sets the push window of your `*` rule (0 to 300). `pushary wait clear` puts it back to 20 seconds.
Changing the mode or the push window needs a key created in the dashboard. Keys from `pushary login`, phone pairing and the Mac app run approvals and notifications, not account settings, so the command tells you to change it in the app or at [pushary.com/dashboard/agent/settings](https://pushary.com/dashboard/agent/settings) instead. `pushary mode status` works with any key.
## Teach-on-Tap: "always allow this tool" [#teach-on-tap-always-allow-this-tool]
When a question is about a specific tool, the decision page shows **Always allow ``**. Tapping it approves the current call and saves a rule so you are not asked again.
The rule is as narrow as Pushary can make it:
* For a shell command, it covers the command's first words. Approving `npm test -- --watch` writes `Bash(npm test:*)`.
* For `Edit`, `Write` and `MultiEdit`, it covers the whole tool.
Pushary does not save the rule for a command it rates destructive or high consequence, such as `rm -rf` or `git push`, or for one that hides what it runs, such as `bash -c` or `eval`. The current call is still approved. Under the hood the rule is `timeoutSeconds: 0`, `timeoutAction: "approve"`, `mode: "push_only"`, `pushFirstSeconds: 0`, which trips the [auto-approve shortcut](#the-auto-approve-shortcut).
The rule applies to every agent on your site, in every project. Once you always allow `Bash(npm test:*)`, every agent runs `npm test` without asking. Remove it any time in the policy editor.
An always-allow for all of `Bash` still asks when the command shows a real consequence: a force push, `rm -r`, `git clean -f`, throwing away uncommitted changes, publishing or deploying, a production deploy or migration, a SQL client that writes, `gh pr merge`, writing to a `.env` or secrets file, or running downloaded code such as `curl ... | sh`. Everything else runs, including commits, tests, `npx`, inline scripts, `git push`, `git push --dry-run`, `git clean -n` and a plain `rm` or `git rm` of a file. To let one of the held commands run without asking, write a specific rule for it, such as `Bash(docker push:*)`.
An always-allow for all of `PowerShell` or all of `Monitor` asks about the same consequences. A `Monitor` command is judged exactly as the same `Bash` command would be. In PowerShell the held commands also include their PowerShell spellings: a recursive `Get-ChildItem` piped into `Remove-Item`, `irm ... | iex` or a downloaded script then run, `Set-Content`, `Out-File`, `Copy-Item` or `Remove-Item` on a `.env` file, `Stop-Service` and other service changes, `Format-Volume`, `Clear-Disk` and scheduled task changes. A plain `Remove-Item file.txt`, `git push`, `Stop-Process`, `-WhatIf` previews and every read such as `Get-ChildItem` still run.
## Rule suggestions from Policy Autopilot [#rule-suggestions-from-policy-autopilot]
Pushary watches how you answer. After you answer the same tool pattern at least 5 times in 30 days, almost always the same way, it suggests an auto-approve or always-deny rule. Suggestions never apply themselves. Accept them under **Policy Autopilot** in the policy editor, which needs a paid plan, or from the terminal:
```bash
npx pushary@latest suggestions
npx pushary@latest suggestions accept
```
Like `pushary mode`, the suggestions command needs a key created in the dashboard.
Approval routing sends a tool's approvals to a role or a named teammate. It is on the same page and needs the Team plan or above.
The hook ranks every rule against the call using the [pattern syntax](#pattern-syntax): exact argument first, then the longest matching prefix or glob, then the bare tool name. If nothing matches it falls back to the `*` rule, and finally to the site default.
`pushFirstSeconds` is how long When away (`push_first`) waits for your phone before a CLI or editor agent hands the decision back to its own prompt. An agent with no prompt of its own waits `timeoutSeconds` instead. `timeoutSeconds` is how long Always (`push_only`) waits before it applies `timeoutAction`. Neither timer sends your phone a decision from a Mac you are using.
No. Policies are enforced by agent hooks, which setup installs for Claude Code, Codex, Gemini CLI, Cursor, VS Code, OpenCode and the other agents marked enforced in [Supported agents](/docs/agents/supported-agents). Cursor connected over MCP alone, like any MCP-only client, can still send notifications and ask questions, but its tool calls are not gated.
## Next steps [#next-steps]
---
# Receiving notifications
Source: https://pushary.com/docs/agents/receiving-notifications
Last updated: 2026-10-08
> Get your AI agent's notifications and approvals on your phone. Use the Pushary app to approve from the lock screen, or any browser with no install.
Your agent's notifications and questions reach you in the Pushary phone app, the Mac app, or a browser. The phone app is the best choice: you tap Approve or Deny right on the lock screen, without opening anything. If you do not want an app, a phone or computer browser can get the same push notifications from your personal channel page.
## Use the Pushary phone app (recommended) [#use-the-pushary-phone-app-recommended]
### Install the app and sign in [#install-the-app-and-sign-in]
Get it from the [App Store](https://apps.apple.com/us/app/pushary/id6785677563) or [Google Play](https://play.google.com/store/apps/details?id=com.pushary.app). Sign in with your Pushary account, or create one.
### Connect it to your computer [#connect-it-to-your-computer]
Run setup on the computer where your agent runs:
```bash
npx pushary@latest setup
```
Then tap **Scan QR code** in the app and scan the code in the terminal. Already set up that computer before? Run `npx pushary@latest setup --connect app` to get a QR code. To connect another computer later, open **Settings** in the app and tap **Scan to connect**.
### Allow notifications [#allow-notifications]
Tap **Allow** when the app asks. Without this, your phone cannot buzz.
### Answer from the lock screen [#answer-from-the-lock-screen]
An approval arrives with three buttons:
* **Approve.** On iPhone, your phone must be unlocked first. On Android, if you turn on **Require unlock to approve** in the app's Settings, Approve opens the app and asks for your fingerprint or face.
* **Deny.** One tap, on both platforms. Denying is always safe.
* **Deny with note.** Type a reason. Your agent reads it.
If a request looks unusual, Pushary leaves out the Approve button and shows only Deny. You cannot approve it from your phone. Deny it there, or approve it in the agent on its own machine.
Questions with options or a text reply open in the app.
## Use your Mac [#use-your-mac]
[Pushary Isle](/docs/agents/mac-app), the Mac app, shows questions in your Mac's notch. You answer with one key. Your Mac and your phone share the same questions, so an answer on one closes it on the other.
## Use a browser (no app) [#use-a-browser-no-app]
Any browser that supports push notifications can subscribe to your personal channel page:
```text
https://your-handle.pushary.com
```
`your-handle` is the handle you picked when you signed up. Then follow the steps for your device.
iPhone only shows push notifications from a web page after you add it to your Home Screen. This is an iOS rule.
### Open your channel page in Safari [#open-your-channel-page-in-safari]
Open `https://your-handle.pushary.com` in **Safari**.
### Add it to your Home Screen [#add-it-to-your-home-screen]
Tap **Share** (the square with an arrow), scroll down, tap **Add to Home Screen**, then **Add**.
### Open it from the Home Screen [#open-it-from-the-home-screen]
Tap the new icon. The page opens without the Safari address bar.
### Allow notifications [#allow-notifications-1]
Tap **Allow** when asked. No prompt? Open **Settings > Notifications** on your iPhone and turn on notifications for the entry with your handle.
To answer a question on iPhone, open the icon on your Home Screen and use the inbox. A web page cannot put Approve and Deny buttons on the iPhone lock screen. For that, use the phone app.
### Open your channel page [#open-your-channel-page]
Open `https://your-handle.pushary.com` in **Chrome**, **Edge** or **Firefox**.
### Allow notifications [#allow-notifications-2]
Tap **Allow** when the browser asks.
You can also add the page to your Home Screen (Chrome menu, then **Add to Home screen**).
### Open your channel page [#open-your-channel-page-1]
Open `https://your-handle.pushary.com` in Chrome, Edge, Firefox or another Chromium browser.
### Allow notifications [#allow-notifications-3]
Click **Allow** when the browser asks. On a Mac, also turn on notifications for your browser in **System Settings > Notifications**.
Safari on a Mac needs the page added to the Dock first. Chrome or Edge is simpler.
## Use several devices [#use-several-devices]
Connect as many phones and computers as you like. Every notification goes to all of them. A common setup is your phone for when you are away, and your Mac or browser for when you are at your desk in another app.
## Check that it works [#check-that-it-works]
Run this on your computer:
```bash
npx pushary@latest doctor
```
It sends a real test notification to your phone. You can also ask your agent:
```text
Send me a test notification via Pushary.
```
## Stop notifications on a device [#stop-notifications-on-a-device]
* **Phone app:** turn off notifications for Pushary in your phone's settings, or sign out of the app.
* **Browser:** block notifications for your channel page in that browser's site settings.
## Delivery reliability [#delivery-reliability]
* **Delivery receipts.** When a device shows a notification, it reports back. Pushary can then tell a push that arrived from one that was only sent.
* **No buzz while you watch.** In the **When away** approval mode, Pushary skips the phone when it can tell you are at the agent's computer: you are typing in its terminal, or the Mac app sees you at that Mac. To get every approval on your phone, tap **Questions & approvals** at the top of the app's inbox and pick **Always**. See [Policies](/docs/agents/policies).
* **Email fallback.** If nobody acknowledges an approval push within a short window, Pushary can email the workspace owner, so a blocked agent does not wait on a push that never arrived. This fallback is not switched on for every workspace, so do not rely on it alone.
## Why is my phone not buzzing? [#why-is-my-phone-not-buzzing]
### iPhone [#iphone]
* Check **Settings > Notifications > Pushary** and make sure notifications are on.
* Browser channel only: open the page from the **Home Screen icon**, not from Safari. If it still fails, remove the icon and add it again.
### Android [#android]
* Check that notifications are allowed for the Pushary app, or for your channel page in the browser's site settings.
* Battery saver and Do Not Disturb can hold back notifications. Turn them off or exempt Pushary.
### Desktop [#desktop]
* Check your browser has notification permission for your channel page.
* On a Mac: **System Settings > Notifications**, find your browser, turn on **Allow Notifications**.
* On Windows: **Settings > System > Notifications**, find your browser, turn it on.
### Notifications arrive late [#notifications-arrive-late]
* Check the phone's internet connection.
* Low power mode can slow background delivery. Turn it off and try again.
## Next steps [#next-steps]
---
# Pushary REST API reference
Source: https://pushary.com/docs/agents/reference/api
Last updated: 2026-10-08
> Call the Pushary REST API with curl. Ask a person a question and wait for the answer, send a push notification, and generate a client from the OpenAPI spec.
The Pushary REST API lets any program ask a person a question on their phone, wait for the answer, and send push notifications, using plain HTTP and a Bearer API key. Use it from n8n, Zapier, a shell script or your own backend when an MCP connection is not an option. The full API is published as an OpenAPI 3.1 document you can feed to a client generator.
## Your first request [#your-first-request]
### Create a key in the dashboard [#create-a-key-in-the-dashboard]
Open **Settings > API keys** at [pushary.com/dashboard/agent/settings](https://pushary.com/dashboard/agent/settings), click **Create Key**, and copy the key. It looks like `pk_xxx.xxx` and is shown only once. Export it:
```bash
export PUSHARY_API_KEY="pk_xxx.xxx"
```
Already ran `npx pushary@latest setup`? The key it saved works for `/ask` and `/send` too. The subscriber list, campaign, template and flow endpoints need a key created here.
### Ask a question and wait for the answer [#ask-a-question-and-wait-for-the-answer]
```bash
curl -X POST https://pushary.com/api/v1/server/ask \
-H "Authorization: Bearer $PUSHARY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"question":"Deploy the new build to production?","agentName":"Deploy bot","timeoutSeconds":50}'
```
Your phone shows a yes or no question while the call waits. Tap an answer and the call returns:
```json
{
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"question": "Deploy the new build to production?",
"type": "confirm",
"status": "answered",
"answered": true,
"value": "yes"
}
```
The question goes to the Pushary phone app, to browsers subscribed to your site, and to Slack if you connected it. The response also carries `liveUrl`, `pollUrl`, `expiresInSeconds` (600) and `mode`, your approval mode.
### Send a notification [#send-a-notification]
```bash
curl -X POST https://pushary.com/api/v1/server/send \
-H "Authorization: Bearer $PUSHARY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Build finished","body":"All 214 tests passed on main."}'
```
A success returns `"success": true`, `"status": "queued"` and the `queued` count.
`POST /api/v1/server/send` delivers to browser push subscribers only. If your site has none, it returns 400 `No subscribers found matching criteria`, even with the phone app connected. It also needs a paid plan with API access. To notify the phone app, use the MCP [`send_notification`](/docs/agents/reference/tools#mcp-push-notifications) tool, or ask with `POST /ask`.
### Ask parameters [#ask-parameters]
| Field | What it does |
| -------------------------------------- | ---------------------------------------------------------------- |
| `question` | Required. Up to 8000 characters |
| `type` | `confirm` (default), `select` or `input` |
| `options` | At least 2 choices, required for `select` |
| `context`, `placeholder`, `agentName` | Background text, the input hint, and the label shown on the push |
| `wait` | `true` by default. `false` returns `"status": "pending"` at once |
| `timeoutSeconds` | How long the call waits, 1 to 55 seconds. Default 30 |
| `callbackUrl` | HTTPS webhook that receives the answer |
| `subscriberIds`, `externalIds`, `tags` | Send only to matching browser subscribers |
No answer in time returns `"status": "timeout"` and a hint. The question stays open for 10 minutes. Read it again with `GET /api/v1/server/ask/{correlationId}?wait=30` (up to 55 seconds), or withdraw it with `DELETE /api/v1/server/ask/{correlationId}`. In In agent mode the question is stored but no push is sent.
## Where the API lives [#where-the-api-lives]
| What | Where |
| ------------------------------ | ---------------------------------------------------------------------------- |
| OpenAPI specification | [`https://pushary.com/openapi.json`](https://pushary.com/openapi.json) |
| Same document, YAML media type | [`https://pushary.com/openapi.yaml`](https://pushary.com/openapi.yaml) |
| Base URL | `https://pushary.com/api/v1/server` |
| MCP server | `https://pushary.com/api/mcp/mcp` (Streamable HTTP) |
| MCP manifest | [`https://pushary.com/.well-known/mcp`](https://pushary.com/.well-known/mcp) |
| Site index for agents | [`https://pushary.com/llms.txt`](https://pushary.com/llms.txt) |
```bash
curl -s https://pushary.com/openapi.json | jq '.info.title, (.paths | keys | length)'
```
## Authentication [#authentication]
Every endpoint takes a Pushary API key as a Bearer token:
```bash
curl https://pushary.com/api/v1/server/identity \
-H "Authorization: Bearer pk_xxx.xxx"
```
What a key can call depends on where it came from:
| Key | Can call |
| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Created with **Create Key** in the dashboard | Every endpoint your plan includes |
| Made by `npx pushary@latest setup`, `pushary login`, phone pairing or the Mac app | Runtime calls such as `identity`, `site`, `channels`, `machines`, `send`, `ask` and Partner decisions. Not campaigns, templates, flows or the subscriber list |
| Claude or ChatGPT connector | `send` and `identity` only |
A setup or pairing key that calls an endpoint outside its list gets a 403 with the message `This key cannot access this endpoint.` For a Partner integration, create a server-side key in the dashboard. See [Get your API key](/docs/agents/api-key) for where keys come from and how to revoke them.
The MCP endpoint also accepts OAuth 2.0. Discovery starts at [`/.well-known/oauth-protected-resource`](https://pushary.com/.well-known/oauth-protected-resource), which names the authorization server.
## Errors [#errors]
Errors are JSON with an `error` message. Shared authentication and validation errors also carry diagnostic fields, as below. Some endpoint errors return fewer fields. Branch on the HTTP status and on `code` when present, not on the readable message.
```json
{
"error": "Unauthorized",
"code": "unauthorized",
"status": 401,
"message": "Invalid or missing API key. Use Authorization: Bearer pk_xxx.sk_xxx",
"hint": "Create a key with `npx pushary@latest setup`, or in Dashboard > Agent > Settings.",
"documentation": "https://pushary.com/docs/agents/api-key"
}
```
That `message` is quoted exactly as the server sends it. It shows an old key format: a real key has no `sk_` part and looks like `pk_xxx.xxx`.
A request to a path with no endpoint behind it returns the same shape with `code: "not_found"` and an `openapi` field pointing back at the specification. Shared `code` values are `unauthorized`, `forbidden`, `invalid_request`, `not_found` and `method_not_allowed`.
| Status | When |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 401 | Missing or invalid key |
| 403 | The key cannot call this endpoint, or the workspace has no active plan (`code: "subscription_required"`, with an `upgradeUrl`) |
| 403 `partner_decision_required` | A Partner workspace called `/ask`. Use `POST /api/v1/server/decisions` instead |
| 429 | Rate limit. The body has `error`, `resetIn` and `limit`, and the `Retry-After` and `X-RateLimit-*` headers say when to try again. `/ask` past 25 pending questions returns only `error`, with no `Retry-After` |
## Function calling [#function-calling]
Published operations carry an `operationId` and request and response schemas. Use them to build tool definitions for an OpenAPI-aware agent framework. The example below extracts an operation index; map each operation's schemas separately for your framework.
```ts
const spec = await fetch('https://pushary.com/openapi.json').then((r) => r.json())
const tools = Object.entries(spec.paths).flatMap(([path, item]) =>
Object.entries(item)
.filter(([method]) => ['get', 'post', 'patch', 'delete'].includes(method))
.map(([method, op]: [string, any]) => ({
name: op.operationId,
description: op.description,
path,
method,
})),
)
```
## SDKs [#sdks]
Use a server SDK for Partner integrations. The `ask` helper creates a decision and polls for an answer within a bounded wait. Persist decision IDs in your workflow, supply a stable idempotency key across retries, and handle transient HTTP failures in your application. SDK requests do not retry on their own.
```bash
npm install @pushary/server
```
```bash
pip install pushary
```
See [Framework adapters](/docs/agents/adapters) for the Vercel AI SDK, LangGraph, CrewAI, Mastra, Eve and the OpenAI Agents SDK.
## What the API covers [#what-the-api-covers]
| Group | Operations |
| ------------- | ---------------------------------------------------------------------------------------------------------------- |
| Decisions | Create a decision (asynchronous by default), read it, record an answer, cancel it, and manage the webhook secret |
| Authorization | Evaluate an action against the workspace policy: `allow`, `deny` or `requires_human` |
| Enrollment | Connect one of your own end users to phone approvals with a single-use link |
| Keys | Issue and revoke per-end-user API keys for multi-tenant agent runtimes |
| Notifications | Send a push notification, read one back |
| Subscribers | List, read, update, delete and count the people a site can notify |
| Campaigns | Create, schedule, send, pause, resume and read stats |
| Templates | Reusable notification content |
| Flows | Event-triggered notification automations |
| Account | Identify the calling key, read the site, count reachable channels, list agent machines |
`POST /ask` and `GET` or `DELETE /ask/{correlationId}` are live but not yet listed in the OpenAPI document.
## Markdown for agents [#markdown-for-agents]
Any documentation or blog URL serves raw Markdown two ways: append `.md`, or send an `Accept: text/markdown` header.
```bash
curl -H "Accept: text/markdown" https://pushary.com/docs/agents/reference/api
curl https://pushary.com/docs/agents/reference/api.md
```
## Next steps [#next-steps]
---
# CLI changelog
Source: https://pushary.com/docs/agents/reference/cli-changelog
Last updated: 2026-10-04
> What changed in each release of the Pushary CLI, newest first
This is the changelog that ships inside the CLI package on npm, published as [`pushary`](https://www.npmjs.com/package/pushary) and as `@pushary/agent-hooks`. Both names carry the same build. A change on the server or in Pushary for Mac can change what your agents see without a CLI release. Those changes are in the [changelog](/docs/changelog) and the [Pushary for Mac release notes](https://github.com/Pushary/pushary-mac/releases).
# Changelog [#changelog]
## 1.9.59 [#1959]
* Codex app cards now also carry three questions from Codex's browser: access to a site, a download and an upload. They used to show only in Codex, because the browser raises them itself and no hook sees them. With the switch on, each reaches Pushary as a yes or no question that names the site, and the first answer wins. A download or upload question carries the address up to the end of its path. Nothing after a `?` or `#` is in that question, and the address ends in … when something was left out; the path itself is sent.
* For a download or an upload, Yes allows that one transfer. For a site, Yes allows the site for the rest of the conversation, or for the turn if Codex is set to ask each turn, and No blocks it for the conversation, because that is what Codex does with the answer its own Allow button sends. The question's details say which. Pushary never sends "always", "for this site" or "for all sites"; those stay in Codex.
* `pushary codex --app-cards on`, `pushary codex --desktop` and the command's help now say that the browser's questions are carried too.
* Other Browser use questions stay in Codex for now: Developer Tools access, browsing history, verification codes and sign-in.
## 1.9.58 [#1958]
* `pushary clean` no longer removes the Codex app cards launcher when it could not turn the switch off. It keeps the folder and prints the reason. Before, a failed turn-off could leave the Codex app starting through a launcher that was gone, and Codex would not start its engine until the next logout. The same now holds when the setting that starts the Codex app cannot be read while a launcher or login item is on disk: clean and `pushary codex --app-cards off` change nothing and say so, where they used to read it as not set.
* `pushary clean` leaves the launcher folder to the step that turns the switch off. Its last sweep of `~/.pushary` used to remove the folder a second time, which could delete a launcher the running Mac app had just written back.
* With the switch turned on from the command, cards keep reaching Pushary after the CLI updates itself while Codex is open. The pass-through loaded part of its code at the first card, and an update replaces those files, so the first card after an update stayed in Codex. It now loads that code when it starts.
* `pushary codex --app-cards status --json` also reports where the launcher is, whether the launcher and login item are what this version writes, and whether a switch that is off left a launch setting behind. The Pushary Mac app uses these to rewrite files an older version wrote and to repair a launcher that was deleted.
* Setup no longer says phone-start is "not running" when its background service is running and only the check fell short. That covers a service that started but was not verified in time, and an older service that could not be replaced and still runs. Setup now says the service is running but this run could not verify it, and points to `pushary doctor`. When the service could not be stopped or removed, setup says that, makes no claim about whether it runs, and no longer offers `pushary daemon install` for it.
* `pushary upgrade` and the daily automatic update no longer fail when a phone-start service that Pushary started on its own is running but not verified in time, or does not come up on a computer that had no service before. `pushary upgrade` prints the line setup prints; the daily update writes it to its log. Since 1.9.50 setup treated this as a note while the update still exited 1 and kept retrying. A service that was running before the update and did not come back still fails the update, and the update tries again to bring it back: the daily update after 5 minutes, an hour, then daily, where automatic updates run, and `pushary upgrade` each time you run it. It stops trying when the service runs again, when phone-start is turned off, or when this computer can no longer run phone-start. Where setup never recorded a phone-start choice, because the key came from `pushary login` or the Mac app, it also stops when the service is no longer installed. One case is not tried again: an upgrade started with `pushary upgrade` by an installed version older than this one, which keeps no record to try again from. `npx pushary@latest upgrade` and the daily update always keep one. Phone-start you asked for with `--control on` is retried the same way.
* `pushary upgrade` now remembers an upgrade whose last step, refreshing the agent configuration, did not finish. Running `pushary upgrade` again finishes it. This holds when the upgrade is started by this version or a later one, which `npx pushary@latest upgrade` always is. Before, the first run said to run it again and the second run answered "Already on the latest version" and did nothing. A last step that needs a change only you can make is recorded as waiting on you, so the daily update leaves it alone. `pushary doctor` shows these under "Last update check" (it said "Last automatic update check", which a manual upgrade is not), and a later upgrade that finishes replaces the record.
* When phone-start you asked for with `--control on` started but could not be verified in time, `pushary upgrade` says so and points to `pushary doctor`. It used to say the service did not start. With `--control on` on a computer that cannot run phone-start, it no longer says to run `pushary daemon install`, which could not work there.
* When an instruction file is left alone during an update, the message no longer says to run `pushary upgrade` again. That did nothing while the installed version was current. It now says the next update refreshes the file once it is fixed.
## 1.9.57 [#1957]
* Codex questions you answer by typing now reach Pushary. When Codex asks something with no suggested answers, the hook used to leave it in Codex, so it never showed in the notch or on your phone. It now arrives as a question with a text field, and what you type goes back to Codex in place of its card, word for word. One exception: an answer that is only the word "defer" hands the question back, as it does everywhere else, and Codex shows its own card.
* Several questions in one call are still asked one after the other. A typed question and a question with choices can sit in the same call.
* Nothing else has to update first. A typed question is sent as the same text question `ask_user` has always sent, so every released phone app, Mac app, Slack message and decision page shows it.
* A blank answer is not passed on. When the answer is blank, nobody answers, or you hand the question back, Codex shows its own card. A question with exactly one choice, more than four, or a secret still stays in Codex.
## 1.9.56 [#1956]
* Setup, doctor, upgrade and the background service now find Claude Code in its own install folder when the `claude` command is not on PATH or in npm's global folder: `~/.local/bin/claude` on macOS and Linux, `.local\bin\claude.exe` under the user profile on Windows. The official Windows installer leaves that folder off PATH and asks the person to add it by hand, so `setup --control on` closed with phone-start unsupported on a computer that had Claude Code installed. Phone-start counts the program only when the home folder is an absolute path and, on macOS and Linux, the file can be run.
* On such a computer a default setup now installs phone-start, as it already does where `claude` is on PATH. `pushary upgrade` does the same when the stored mode is `auto`, and on macOS the automatic update runs that step in the background. As on any computer where phone-start is on, `pushary upgrade` exits 1 when the background service does not come up. With stored mode `auto`, `pushary doctor` reports the background service as missing there until setup or upgrade has run on this version. `setup --control off` keeps phone-start off.
* A session started from the phone launches Claude Code from that folder too. Before this release the launcher looked only on PATH, so such a computer could not have started one.
* Setup reads the Claude Code version from that folder as well, so on macOS and Linux it registers the same hook events as when the folder is on PATH. On Windows the program's file name carries no version, so nothing changes there.
* When the Claude Code program's file name carries no version, Pushary reads the version from a `package.json` above the program. Only a `package.json` that names Claude Code, or one of its per-platform packages, now counts. Before this release any project's `package.json` there counted, the home folder's included. A version below 1.0.0 then left Claude Code with no Pushary hooks after setup, `pushary upgrade` or the next session start, and a version from 2.0.0 registered events an older Claude Code does not know.
* When required phone-start has no agent it can start, the closing steps say what it needs and where the commands are looked for. Setup's "Phone-start was required" line now uses the wording doctor uses. `pushary claude` no longer names PATH as the only place it looked when it finds no program.
* On Windows, setup's agent detection now names the Claude Code program as what it found. Before, it looked for a file without the `.exe` ending and could only see the `.claude` folder.
* Turning Codex app cards off now stops forwarding cards even if the installation receipt is missing or unreadable. The retained launcher recovers the engine path from its own script so an already-open Codex can keep running.
* Refreshing agent instructions preserves existing file permissions, including shared files reached through a symlink, even when the updater's umask is more restrictive.
## 1.9.55 [#1955]
* The setup test now says how to answer it. When the Pushary phone app is connected, setup prints the step for that phone: "Press and hold the notification, then tap Approve." on iPhone, "Expand the notification, then tap Approve." on Android. It then waits with "Waiting for your answer". The buttons on the notification answer the question on every version of the app; on Android the button asks you to unlock the phone first. A plain tap opens the app, and during the app's first run that showed "Connect your computer" in place of the question, so the test went unanswered.
* `pushary doctor --roundtrip` prints the same step, so the command setup points to no longer gives different advice.
* When nothing comes back, setup says to send another test and then gives the step for that phone, in that order, because the first notification has been withdrawn by then. A browser connection keeps the earlier wording.
* The test question no longer says "Tap Yes or No". The buttons are named Approve and Deny in the phone app and Yes and No elsewhere, so it now reads "Answer either way".
* A setup test stopped with Ctrl-C while it waits for the answer is now reported. Setup records the interrupt, the step it stopped at (`answer-test`), the agents it had configured and that the notification was delivered, before it exits. `--json` prints its one result object for that exit too. Before this release that exit reported nothing, so a test somebody gave up on left no record.
## 1.9.54 [#1954]
* For a switch the Pushary Mac app turns on, the login item that keeps Codex app cards on after a restart now runs Pushary's own program and not a shell line, so macOS treats it as part of Pushary and not as a separate `sh` item. A switch turned on with `pushary codex --app-cards on` keeps the login item it has, unchanged.
## 1.9.53 [#1953]
* `pushary codex --app-cards status --json` prints the switch's state as one line of JSON for a program to read: the state, the Codex app found on this Mac, whether the Mac app turned the switch on, and whether it was turned off on purpose. The Pushary Mac app uses it for its new Settings switch, which is on by default there.
* `pushary codex --app-cards off` is remembered. The Pushary Mac app turns the switch on by itself, and it leaves alone a switch you turned off with this command until you turn it on again. `pushary clean` keeps that record while the Mac app stays installed.
* The login item that keeps Codex app cards on after a restart now also checks that every file of Pushary's own program is still there. If Pushary was removed without turning the switch off, Codex starts the ordinary way from the next login. An existing login item gets the check the next time the switch is turned on.
## 1.9.52 [#1952]
* `pushary upgrade` and the daily automatic update now refresh the Pushary instructions in each agent's instruction file: Codex, Gemini CLI, OpenCode, fx, Qwen Code, CodeBuddy and Grok. Before this release only `setup` wrote them, so a machine set up before 1.9.43 kept the old text after every update and its agents were never told about reminders. In fx a plain "remind me in 2 minutes" could fall back to a sleep timer that stops when fx closes.
* The refresh only replaces a Pushary block that is already there. It never adds one to a file that has none, never creates a file, and keeps the text around the block byte for byte, Windows line endings included. The file is replaced in one step, so a full disk or a stopped update cannot leave half a file.
* It leaves a file alone and names it when the file is read-only, is not UTF-8 text, or holds Pushary markers that do not form exactly one block. The upgrade still finishes. Fix the file and run `pushary upgrade` again.
* fx is now part of the refresh. Its instructions are refreshed when the Pushary server entry in `~/.fx/mcp.json` is the CLI's. The server entry and `~/.fx/settings.json` are not touched.
* fx starts the daily update check. Each time fx starts the Pushary server, Pushary checks at most once a day for a newer version, the same way a Claude Code, Codex or Gemini session start does. A machine that only uses fx used to stay on the version it was set up with.
* Agents the Pushary Mac app owns are left alone, fx included, and reported as kept.
* `pushary upgrade` on a machine with no global install now names fx and the command that updates it.
## 1.9.51 [#1951]
* `pushary codex --desktop` now does the one thing it was built for. When the Codex app shows "Allow Computer Use to use Calculator?", you get one Pushary question, "Allow Codex to use Calculator for this conversation?". Yes allows that app for that conversation, No denies it, and answering in Codex first withdraws the question. Before this release the launch never forwarded anything: it expected the Codex app to start its engine in a way the app does not, and it did not recognise the request the app really sends.
* Pushary asks only when the card comes from Codex's own Computer Use and carries exactly the words Codex shows. A card another tool raises, or one worded differently, stays in Codex. It waits ten minutes for an answer, then withdraws the question.
* The launch no longer touches commands or file edits in the Codex app. Your hooks already handle those, so nothing is asked twice.
* The launch works with any Codex app version that keeps its engine in the known place, instead of one exact version. A request Pushary does not recognise stays in Codex.
* New, off by default: `pushary codex --app-cards on` makes the Codex app start through the same pass-through every time you open it, including after a restart, with no terminal to keep open. `pushary codex --app-cards off` undoes it and `status` says where it stands, including when Pushary or the Codex engine is no longer where the launcher expects. It writes a launcher under `~/.pushary/codex-app`, a login item, and the `CODEX_CLI_PATH` setting for your login session, and leaves that setting alone if another program already owns it. `off` replaces the launcher with one that only runs Codex's engine and leaves that file in place, because the Codex app records its path in `~/.codex/config.toml` and an open Codex keeps using it; `pushary clean` removes it, after which Codex should be reopened.
* The launcher steps aside by itself: it hands every engine command except the app's own protocol straight to Codex's engine, starts the engine itself when Pushary is missing, broken, never starts it, or the launcher cannot write beside itself, and turns its own setting off if a Codex update moves the engine. If the launcher is stopped, Pushary stops the engine within a few seconds.
* The pass-through holds at most 64 KiB of the app's input and inspects at most 256 KiB of a line from the engine, loads the code that talks to Pushary only when a card appears, does nothing while idle except check every two seconds that its launcher is still running, lets the engine finish its own shutdown when Codex quits, and keeps copying if anything in it fails. The first time Computer Use asks in a way this version does not know, it says so once in Codex's own log.
* Setup on a Mac that has the Codex app and no Codex CLI: setup never takes Pushary's own launcher for Codex, and it finds the app's engine where the current Codex app keeps it when Codex's settings file does not name one. Before, the first would have skipped native hooks and the second reported Codex as not found. Correction: this entry first said such a Mac got no hooks at all since the Codex app changed its layout. That was wrong. When Codex's settings file names the app's own engine, as it does after Codex has started normally, setup already found it and installed hooks.
* `pushary codex` is experimental and no longer appears in `pushary --help`. It still runs when typed, and `pushary codex --help` describes it.
## 1.9.50 [#1950]
* Setup no longer reports a problem when it tried to install phone-start on its own and the background service did not start. The run closes on what the approval check found, and the closing notes say phone-start is not running. `--control on` still fails the run when phone-start cannot be made ready.
## 1.9.49 [#1949]
* A program at a nested path under a system folder, such as `/opt/homebrew/bin/tools/cat` or `/usr/local/bin/x/zsh`, is no longer treated as the system tool of the same name. It asks a person where it used to run as a read, and `pushary codex` no longer reads a script out of a shell at such a path. Programs directly inside `/bin`, `/usr/bin`, `/usr/local/bin` and `/opt/homebrew/bin` are judged as before. The `pushary codex` half needs the matching server.
* `pushary codex`: a question or form Pushary leaves with the terminal, in Terminal mode or because you are at the keyboard, is no longer reported as a failure in the Codex screen. Neither is a form you answer in the terminal before the phone does.
* Recorded late, shipped in 1.9.47: `pushary clean --everything` also removes stale app registrations, leaked test preferences, the app's per-user cache folders and crash reporter entries, and `clean` and `disconnect` delete the Grok hook file once nothing else is left in it.
* Recorded late, shipped in 1.9.45: a question Codex asks in a `pushary codex` session reaches your phone as well as the terminal.
## 1.9.48 [#1948]
* When `--control on` cannot work because this computer has no `claude`, `codex` or `gemini` command on PATH, or no background service manager, setup says which one is missing instead of suggesting `pushary daemon install`.
* The setup report records which of the two was missing, so a phone-start failure on Windows can be told apart from one on a headless session.
## 1.9.47 [#1947]
* `pushary codex`: when Codex asks to approve a call Pushary already allows (your rules, a preset, or a read-only command), it is approved for you once, as the Codex hook already does. A file change is never approved this way, and neither is a request Codex flags as needing a person. The kill switch and deny rules are unchanged. This needs the matching server.
* `pushary codex`: a command Codex runs through a login shell (`/bin/zsh -lc 'git push origin main'`) is asked about and matched against your rules as the command itself, so a rule such as `Bash(git push*)` applies to it. This needs the matching server.
* `pushary codex`: an approval Pushary hands back to Codex's own prompt is no longer reported as a failure in the Codex screen.
* `pushary codex --desktop` no longer throws while stopping when the Codex engine's processes have exited but are not yet reaped, which macOS reports as a permission error.
## 1.9.46 [#1946]
* A command that feeds text to a database client or a shell through a pipe, a heredoc or a here-string is judged together with that text again. Under the Autonomous preset or a rule that allows everything, `echo "DROP TABLE users;" | psql app` asks a person, as it did before 1.9.39. A pipeline that only reads, such as `npm test | grep release`, still runs unasked.
## 1.9.45 [#1945]
* Add an opt-in experimental Codex Desktop launch with `pushary codex --desktop`. Quit Desktop first; the bridge uses its supported bundled engine and forwards supported native permissions through Pushary.
* Computer Use approvals preserve conversation scope and warnings. Native prompts remain available when forwarding is unavailable, and competing phone/local answers are resolved once.
* Desktop forwarding currently supports bundled engine 0.162.0-alpha.2 on macOS. It uses an undocumented launch override; real Calculator-to-phone acceptance is still pending. Ordinary setup does not enable it.
## 1.9.44 [#1944]
* Start a managed Codex terminal session with `pushary codex` and route supported command, file-change, and MCP permissions through Pushary. The native terminal remains available for unsupported requests.
* Preserve repository-specific deny and routing rules, cancel stale questions across delayed or lost responses, and stop owned session processes on shutdown. Existing Codex desktop chats are not attached by this feature.
## 1.9.43 [#1943]
* Agent and fx setup guidance uses Pushary’s durable personal reminders, which persist after the agent exits and deliver privately to their creator.
## 1.9.42 [#1942]
* Complete Mac cleanup removes the Partner enrollment and device-key slots introduced by the native Partner flow, alongside existing session state.
* Bundled agent guidance preserves native Mac setup, explains store-specific checkout terms and returning to the pairing link after installation or restart, and distinguishes a synthetic question test from a real hook approval check.
## 1.9.41 [#1941]
* Personal Codex sessions reject an invalid question set before asking anything on your phone. Malformed or repeated choices no longer disappear, and ordinary free-text replies, cancellation and host deadlines keep their existing behavior.
## 1.9.40 [#1940]
* Personal Codex question replies bind to the original connection and request. A saved answer is claimed once; missing delivery confirmation never triggers automatic replay.
* With the matching server, blocked-action answers and their short-lived grants commit together, and a Stop prevents later claims. Question answer time remains separate from this grant window.
* Phone, Mac and browser inboxes read every authorized question page. Saved answers and unconfirmed delivery have separate messages; neither claims an action ran.
## 1.9.39 [#1939]
* When away recognizes destructive Codex commands behind Git options and command wrappers, and recursive PowerShell deletions.
* Malformed Codex question choices stay in Codex instead of appearing in Pushary with choices silently removed.
## 1.9.38 [#1938]
* Owned Codex child questions reach the long-wait bridge instead of being asked first by a short-lived native hook. The matching Mac helper hands these questions to the CLI; kill, deny and command-permission checks still apply.
* Questions in the personal Codex child bridge keep waiting until answered or cancelled when Codex supplies no deadline. A short poll or temporary network failure no longer cancels the question.
* A cancelled request or an explicit Codex deadline still stops the wait and rejects a late answer. Native permission hooks and recovery after closing Codex keep their existing limits.
* With the updated Pushary server, ordinary MCP questions remain available after the cache clears and can receive one reminder after ten minutes. This saves the answer; it does not promise that a closed agent will restart.
## 1.9.37 [#1937]
* Codex questions reach Pushary. When Codex asks you something with its question card (`request_user_input`), the question and its choices go to your phone or the Mac notch, and your answer goes back to Codex in place of the card. A question with no choices, one choice, more than four, or a secret stays in Codex, and so does one nobody answers or Pushary cannot reach. Setup and upgrade widen the Codex `PreToolUse` matcher to include both question tools.
* When away now asks about a destructive Codex command (such as `rm -rf`, a force push, `git reset --hard` or `git clean`) or a patch that deletes a file, because Codex can run those without a prompt of its own. The question goes to your phone when you are away from the keyboard; at the keyboard Codex carries on as before. A call nobody answers is refused.
## 1.9.36 [#1936]
* Connect Grok Build to Pushary with native hooks for tool approvals, session activity and subagent activity. Setup, doctor, upgrades and disconnect use Grok's configuration directory, including a custom `GROK_HOME`.
* Grok patch approvals inspect every affected path, including move destinations, and show the patch in the approval receipt. Existing kill, deny and scope rules apply to those calls.
* Cleanup reports malformed or unreadable agent configuration and preserves the file so it can be repaired.
* Grok's question and plan dialogs stay in Grok. Use Pushary's MCP tools for phone questions. Grok itself continues after a hook crash or timeout, so Pushary hooks do not replace Grok's native permission checks.
## 1.9.35 [#1935]
* When you run setup yourself in a terminal and your phone cannot get notifications yet, it no longer stops with exit code 6. It asks you to open the Pushary app and allow notifications, waits up to 10 minutes for your phone, then sends a test notification. Press Enter to skip the wait. Runs with `--yes`, `--agents` or `--json`, and computers with the Pushary Mac app, keep the single check.
## 1.9.34 [#1934]
* Live questions keep waiting when a tool poll ends; agent instructions no longer cancel them after one retry.
* Questions preserve every point up to 8,000 characters instead of being silently cut to 500. Notification previews stay short.
## 1.9.33 [#1933]
* Session history keeps saving a conversation when the history process is stopped during that conversation's first upload. It used to make a new encryption key on restart, which the server refused, and the rest of that conversation was never saved.
* When the server cannot take history uploads, retries wait up to 12 seconds instead of repeating every 3 seconds, and go back to every 3 seconds as soon as one is accepted.
* A finished session with nothing to save, or one another Pushary process is already saving, stops being watched. Before, it held one of the 32 history slots for up to two days.
## 1.9.32 [#1932]
* A paused session keeps asking about destructive PowerShell and Monitor commands, just as it does for Bash.
* SQLite bracket identifiers cannot hide a later write behind a comment marker. MySQL and MariaDB bracket syntax asks when the server's SQL mode is unknown; PostgreSQL and GoogleSQL array reads keep running.
* SQL writes next to quoted identifiers, such as `UPDATE[users]SET[x]=2`, ask even without spaces between the tokens.
* Commands beyond the remote API inspection limit ask for approval.
## 1.9.31 [#1931]
* SQL sent through `psql`, `mysql` and `mariadb` is now read the way the database reads it. These used to run under an approve rule and now ask: a write after a dollar quote with a non-ASCII tag, such as `psql -c 'SELECT $é$ -- $é$; UPDATE users SET x=1'`; a write after a carriage return that ends a `--` comment; and a write inside a MySQL `/*! ... */` or MariaDB `/*M! ... */` comment, which those clients run.
* `SELECT ... INTO` and a `LOCK` statement now count as writes and ask, as do `EXECUTE IMMEDIATE` and `PREPARE ... FROM`.
* A command after `bash -c` and its options is now seen, so a `Bash(git push:*)` deny rule now refuses `bash -c -x 'git push'`, `bash -c -- 'git push'` and `zsh -c -x 'git push'`, which used to run.
* `env` options are read the way GNU and macOS `env` read them, abbreviations included, so the same deny rule now refuses `env --spl='git push'`. When the two versions of `env` would run different commands, Pushary asks.
* Codex: `gh api -iXDELETE` and `gh api -iX DELETE` are read as DELETE requests and ask, instead of being passed back to Codex.
* A tool name that is not one concrete tool, such as `*`, can no longer be saved as an Always allow rule.
* History capture works when Claude Code or Codex keeps its files under a custom `CLAUDE_CONFIG_DIR` or `CODEX_HOME`.
* `pushary-mcp` tells Pushary which computer is asking, so the phone's Try it step checks the computer you chose.
## 1.9.30 [#1930]
* "Always allow PowerShell" and "Always allow Monitor" now ask about the same consequences "Always allow Bash" does. A Monitor command is judged exactly as the same Bash command. In PowerShell that includes `Remove-Item -Recurse`, `irm ... | iex`, a downloaded script then run, `Set-Content .env`, `Stop-Service`, `Format-Volume` and a recursive `Get-ChildItem` piped to `Remove-Item`. A plain `Remove-Item file.txt`, `git push`, `Stop-Process`, `-WhatIf` and every read such as `Get-ChildItem` keep running.
* A `*` approve rule no longer runs a destructive PowerShell or Monitor command, such as `Remove-Item -Recurse -Force ~`, `cmd /c rd /s` or `rm -rf ~`, without asking. It asks, as it already did for Bash.
* Codex: a call nobody answers is now refused instead of passed back to Codex, which ran calls it does not ask about itself. Only a rule you set to approve on timeout lets it run.
* A call refused because your phone approvals for a session are paused now says so, and tells you to tap Approve on the notification to resume. It used to say "No response within timeout".
* Codex: a GitHub, Stripe, AWS or Twilio write behind `caffeinate`, `stdbuf` or `flock` now asks, and `gh` asks about anything that is not a known read, such as `gh extension install`, `gh ssh-key add` or `gh label clone`. `gh pr co` now runs like `gh pr checkout`.
* Codex: a patch to a GitHub workflow or secrets file now asks however its path is capitalised, so `.GitHub/Workflows/ci.yml` no longer runs unasked.
* When two of your rules hold a call equally, Pushary now uses the one that refuses it if nobody answers, whatever order the rules are in.
* Antigravity CLI: input to a running command that is not text is now refused instead of shown as blank input.
## 1.9.29 [#1929]
* Codex: answering "Handle on your machine" now always refuses the call, and tells you to ask Codex to run it again while you are at your computer, or to run it yourself. In Codex's default modes the call used to pass back to Codex, and a call Codex does not ask about itself, such as a remote API write or a file deletion, could run without anyone approving it.
* In a session started from your phone, "Handle on your machine" now refuses the call with the same reason every agent without an approval prompt of its own gives.
## 1.9.28 [#1928]
* Antigravity CLI: text agy sends to a command it left running (`manage_task` with `send_input`) now goes through Pushary and is judged as the shell command it types, so your `Bash` rules and safe read-only commands apply to it. It used to run with nobody asked, because connecting agy turns off its own prompt. Sending blank input, such as pressing Enter, asks your phone. Listing, checking and stopping agy's own background tasks run without a question.
* Antigravity CLI: a call the hook cannot read (empty or malformed input, or a tool call with no name) is refused with a reason instead of running. So is every agy call while an admission rule, a wildcard included, has switched agy's approval hook off. Pushary's own tools still run.
* `pushary upgrade` and automatic updates now also refresh the hooks of Antigravity CLI, Qwen Code, CodeBuddy, Qoder, Droid and Devin when this CLI set them up, so a connection made on an older version picks up the current hook set without running setup again. Other entries in those files are left as they are.
## 1.9.27 [#1927]
* A `Bash(...)` deny or ask rule you write in Pushary now catches the command it names however it is spelled. With a `Bash(git push:*)` deny and a rule that otherwise allows Bash, these used to run and are now refused: `git -C . push`, `git -c x=y push`, `git --no-pager push`, `git --git-dir=.git push`, `GIT_DIR=.git git push`, `FOO=1 git push`, a push behind `env`, `nohup`, `time`, `nice`, `timeout 60`, `exec`, `command`, `builtin`, `xargs`, `sudo` or `arch -arm64`, `"git" push`, `\git push`, `/usr/bin/git push`, `git push` with two spaces, `(git push)`, `{ git push; }`, `bash -c "git push"`, `sh -c 'git push'`, `eval git push`, a heredoc fed to `bash`, `find . -exec git push \;`, a push inside `if ...; then ...; fi`, and a push chained after a command Pushary could not split before, such as `x=$(git rev-parse HEAD) && git push`, `echo hi > out.txt && git push`, `npm test && git push $(git remote)`, or Claude Code's heredoc commit followed by `&& git push 2>&1 | tail -5`. An ask rule now asks for every one of these. The same holds for any rule, such as `Bash(npm publish:*)` or `Bash(rm:*)`.
* A command whose program Pushary cannot see, such as `$GIT push`, `"$(which git)" push`, `bash -c "$CMD"`, `eval "$(ssh-agent -s)"` or `cat script | bash`, is now refused when you have a deny rule for a Bash command, and asks when you have an ask rule. Every preset includes ask rules for destructive commands, so under the Autonomous preset such a command now asks instead of running.
* The read-only floor no longer approves a read that your deny rule names in another spelling. With a `Bash(cat:*)` deny, `"cat" .env`, `/bin/cat .env` and `c''at .env` were approved as reads. With `Bash(cat .env)`, so were `cat ./.env`, `cat '.env'` and `cat .env`, and with `Bash(git log:*)`, so were `git log`, `"git" log` and `git "log"`. They are now refused, and an ask rule asks for them.
* A rule that approves still covers only the command as written. `Bash(git commit:*)` does not start approving `env FOO=1 git commit` or `bash -c "git commit"`. A narrow ask rule that now catches a new spelling never weakens a broader deny: with a `Bash` deny and a `Bash(npm test:*)` ask, `FOO=1 npm test` is still refused.
* The `ask` and `deny` rules in your Claude Code settings read the same spellings. With `Bash` allowed and `Bash(git push:*)` denied there, Pushary used to step aside for `git -C . push` as a call Claude Code allows. It now handles that call under your Pushary rules.
* `pushary setup` keeps the whole pairing QR on screen for the whole wait on an 80x24 terminal. After 45 seconds the install hint, and after a minute the browser sign-in command, used to print as new lines under the code. Each one scrolled a top row of the code off the screen, so after a minute it no longer scanned. They now take turns with the minutes left on the waiting line itself. When setup's output is not a terminal, each still prints once on its own line. The install hint now says "scan the QR above" instead of "scan the code above" so it fits on one 80-column line.
* The background service now tells Pushary whether automatic updates can run on this computer, and if not, why: turned off, CI, Windows, Linux, or not a global install. It also sends the result and the time of the last update check, with no file paths or error text. Your agent overview uses it to show a computer that is stuck on an old version.
* The bundled Pushary skill now gives the When I'm out window as 20 seconds by default, which is what the built-in rules use. It said 10.
## 1.9.26 [#1926]
* `arch` is no longer approved as a read. On a Mac, `arch -arm64 ` runs the command after it, so `arch -arm64 git push --force` was approved without asking, even under a `Bash(git push:*)` deny rule. A command behind `arch` is now judged like the same command without it. Use `uname -m` to read the architecture; it is still approved as a read.
* Codex in any mode except full access now asks your phone before a command that changes something through GitHub, Stripe, AWS or Twilio, such as `gh pr comment`, `gh api -X DELETE`, `gh api ... -f field=value`, a GraphQL mutation, `stripe refunds create`, `aws lambda invoke`, `aws ses send-email`, an upload with `aws s3 cp` or `aws s3 sync`, or `twilio api:core:messages:create`. Since 1.9.15 these ran without a question whenever Codex did not raise one of its own, which is the case when its sandbox allows network access. Reads such as `gh pr view`, `gh api repos/...`, `stripe charges list`, `aws s3 ls` and `aws ec2 describe-instances` still run without asking.
* In the same modes Codex now also asks before a patch to a `.env` file, a credentials file or a GitHub Actions workflow. Codex applies an edit inside the project without asking, so these used to change with nothing shown. `.env.example` and other files under `.github` are not affected.
## 1.9.25 [#1925]
* "Always allow Bash in this workspace" and "Always allow Bash in this repository" now still ask about a command with a real consequence. That is running downloaded code (`curl ... | sh`, or a file downloaded and then run), a production deploy or migration (`vercel --prod`, `RAILS_ENV=production bin/rails db:migrate`), a schema push or reset (`prisma db push`, `prisma migrate reset`, `supabase db push`, `drizzle-kit push`), a SQL client that writes or runs a script file (`psql -c "UPDATE ..."`, `psql -f`, `psql < file`, `sqlite3 app.db < schema.sql`), `redis-cli FLUSHALL`, a cluster deploy (`kubectl apply`, `helm upgrade`), publishing (`docker push`, `npm publish`), `gh pr merge`, `gh repo delete`, `aws s3 sync --delete`, restarting, stopping or unloading a service (`systemctl restart`, `launchctl unload`), throwing away uncommitted changes (`git checkout `, `git restore`, `git branch -D`), and writing or deleting a `.env` or secrets file. These used to run without a question. They are recognised behind wrappers such as `sudo`, `env`, `timeout` and `xargs`, behind global flags such as `git -C dir`, inside `if` and `for` blocks, and with output redirected to a log file.
* Under those rules a plain `git push`, `git push -u origin HEAD`, `git push --dry-run`, `git clean -n`, and an `rm`, `git rm` or `unlink` of a file keep running. Asking about every push and every removal under a rule that says always allow is too much. A force push, `rm -r` and `git clean -f` still ask, as before, and a force push now also asks when it is written as `git -C dir push --force`, `git push origin +main`, or a push that deletes or mirrors branches on the remote (`--delete`, `:branch`, `--mirror`, `--prune`).
* The same applies to a bare `Bash` approve rule written on the Policies page and to rules saved before this release. Presets and "Allow Bash for this session" keep their own rules. To let one of the held commands run anyway, add a specific rule such as `Bash(docker push:*)`.
* Claude Code's commit form, `git commit -m "$(cat <<'EOF' ... EOF)"`, is now read as a commit when every shell reads its message as plain text, including macOS `/bin/bash` 3.2. "Allow Bash for this session" asks about fewer commits, a `Bash(git commit:*)` rule now covers them, and a `&& git push` after one is seen, also by a `Bash(git push:*)` deny rule. A message with an unmatched `)`, an apostrophe, a backslash, a backtick, or a line that starts with the closing word such as `EOF`, is not read this way and asks as before.
* Production deploys and migrations, SQL that writes, runs a script file or comes from a variable or a pipe, `mongosh --eval`, cluster deploys and schema resets (`kubectl apply`, `helm upgrade`, `prisma db push`), `pg_restore --clean`, `mongorestore --drop`, `gh pr merge`, and publishing or deploying with `docker push`, `twine upload`, `cargo publish`, `npm unpublish`, `gh release create`, `pulumi up`, `cdk deploy`, `firebase deploy`, `fly deploy` or `wrangler deploy` now also ask under "Allow Bash for this session". These checks skip a command run with `--dry-run` or `--help`, but a command that says deploy, publish or release, such as `cargo publish --dry-run` or `firebase deploy --dry-run`, still asks, as it did before this release.
## 1.9.24 [#1924]
* Under When I'm out, a call nobody answers is refused for every agent with no approval prompt of its own (Antigravity CLI, Cursor file edits, sessions started from your phone), even when your rule approves on timeout. The 1.9.17 notes said this; only Codex did it. Under Every time a rule that approves on timeout still runs the call after its full wait.
* After three approvals nobody answered in a session, an Every time call whose rule approves on timeout keeps reaching your phone instead of running at once with nothing shown. Antigravity CLI sessions are no longer paused at all: agy sends nothing that could lift the pause, so every later call was refused until a new conversation.
* "Allow for this session" is offered for an MCP tool only when its name says it reads, such as `get_issue` or `list_files`. A grant cannot see the arguments of later calls, so a tool such as `query` or `create_issue` asks every time. A session grant already given for such a tool stops applying.
* `pushary sign-out --revoke` and `pushary clean --everything` revoke only a key this CLI created by browser sign-in or pairing. The Pushary Mac app's key, a key you pasted or passed with `--key`, and a key from an `export PUSHARY_API_KEY` line are removed from this computer and not revoked, because the Mac app or another machine may still use them; the output says so and links to the dashboard to revoke it there. Since 1.9.8 `sign-out --revoke` could revoke the Mac app's own key. A key saved before 1.9.24 has no record of how it was made, so it is removed and not revoked until your next sign-in records one.
## 1.9.23 [#1923]
* A `printf` whose format has a numeric conversion, such as `%d`, `%x` or `%f`, is no longer approved as a read unless every argument is a plain number. zsh, the default shell on a Mac, evaluates those arguments as arithmetic, and an argument such as `'PWD[$(command)]'` ran the command while Pushary approved it without asking. `printf '%s\n' text`, `printf hello` and `printf '%d\n' 42` are still approved as reads.
* Codex with its approval policy set to never: answering "Handle on your machine" on your phone now refuses the call. Codex has no prompt of its own there, so the call used to run, a destructive one included. With Codex's own prompts on, the call still goes to that prompt.
* Antigravity CLI: when your Pushary subscription has ended, a call Pushary would ask about is refused with a message that says so. Pushary is agy's only gate, and the call used to run with nothing asking. Renew, or run `pushary disconnect antigravity` to give approvals back to agy.
## 1.9.22 [#1922]
* Each approval Claude Code or Codex sends through Pushary now carries the approval mode the agent reported for the session, such as `default` or `bypassPermissions`, so Pushary can tell which mode an approval was asked in. It is recorded with the approval and never used to decide one. A mode that is empty or not text is left out, and a long one is cut to 64 characters.
## 1.9.21 [#1921]
* `pushary setup` records a key as the one Pushary for Mac is signed in with only when Pushary for Mac holds that same key. It used to record any key it reused from your agent settings that way, including one left behind by an earlier CLI sign-in. `pushary clean` never revokes the Mac app's key, so it skipped that leftover key and left it working. Now it revokes it, and still leaves the Mac app's own key alone.
* For the same reason, a leftover CLI key is no longer reported as "signed out on this Mac" when it stops working.
* A key recorded by an earlier version stays recorded until setup saves a different key.
## 1.9.20 [#1920]
* `pushary setup` calls a key found in your agent settings the Pushary for Mac key only when Pushary for Mac holds that same key. A key left behind by an earlier CLI sign-in is called a key found in your agent settings, also when Pushary for Mac is installed.
* On Windows, when npm cannot replace a file during the global install (EPERM), setup says to close other terminals or editors that use Node, then run setup again. It used to suggest an npm prefix change, which does not help when another program has the file open. EACCES, and EPERM on macOS and Linux, keep the npm prefix advice.
* The pairing screen prints the fingerprint directly under the QR code, above the waiting line, so the QR code, the fingerprint and the waiting line all fit an 80x24 terminal. The fingerprint used to sit above the QR code and scroll off.
* `Wrong account? Run npx pushary@latest setup --connect app` is printed only where that command can pair: not in CI, and not with `--skip-phone` or `--connect none`. On an inactive plan it is printed once, next to the account with no plan, instead of twice.
## 1.9.19 [#1919]
* When nobody has answered the last 3 approvals in one agent session, Pushary stops sending that session's approvals to your phone. Each further call ends at once, exactly as it would have if nobody had answered it for the whole wait: under When I'm out Claude Code asks in its own prompt, a rule that refuses on timeout refuses it, a rule that approves on timeout runs it at once with nobody asked, and Codex set to never ask for approval refuses it. Nothing is sent, and the paused approval does not appear on your phone or in the Mac app. A command that deletes, rewrites history or reaches the network, such as `git push`, `rm` or `dropdb`, and any call Pushary flags as destructive, is still sent to your phone.
* Approvals go back to your phone as soon as you answer any approval in that session, send it a new prompt, or approve at your agent's own prompt a call Pushary handed there. That last one works for Claude Code and for Codex calls checked before they run, whose completion Pushary sees.
* An approval counts as unanswered only once its wait has ended with nobody answering, so several approvals open at the same time never pause the next one. Approvals you may have answered at your terminal without Pushary seeing it, in Gemini CLI, OpenCode, Cursor's shell and MCP approvals and Codex's own approval prompt, never count, so those sessions are never paused.
* Earlier CLI versions are paused only for Claude Code, and only where the call would have ended in Claude Code's prompt anyway. In a session started from your phone, CLI 1.9.6 to 1.9.12 refuse a paused call with their message that the remote session has no local approval UI. The Pushary Mac app keeps sending every approval to your phone for now.
## 1.9.18 [#1918]
*Notes corrected on October 4, 2026. The first version described only the Cursor case and left out the setup and skill text changes.*
* `pushary disconnect` leaves a config file it cannot read exactly as it is and lists it as untouched, for every agent. For Cursor it also keeps the plugin folder when the hooks file that points into it cannot be read, so Cursor's gate never points at a script that was deleted.
* After setup, the line on how to test reads "Run a command that needs approval. Away from this computer, it reaches your phone." The bundled Pushary skill now says When I'm out waits 10 seconds for your phone by default, not 45.
## 1.9.17 [#1917]
*Notes corrected on October 4, 2026. The first version said a rule that approves on timeout lets the call run under When I'm out. It does not: the call is refused.*
* When Codex runs with its approval policy set to never (full access), every call Pushary asks about goes to your phone, also while you are at the keyboard. Codex has no prompt of its own to ask you there. The wait is your policy's timeout, also under When I'm out.
* If nobody answers, the call is refused with "No response within timeout". Before, the command ran once the wait ended, a destructive one too, and at the keyboard it ran without anyone being asked. A wait cut short by Codex's hook time limit, and a question no phone could receive, are refused the same way.
* In Every time, a rule's timeout action still applies after its full wait: approve on timeout lets the call run, and deny on timeout refuses it. Under When I'm out, an unanswered call is refused whatever the rule's timeout action says.
* In Codex's other approval modes nothing changes. The call goes back to Codex, which asks in its own prompt when the call needs it.
## 1.9.16 [#1916]
* `pushary setup` names the account before it reuses a key: the key saved on this machine, `PUSHARY_API_KEY`, or a key found in your agent settings. At a terminal it asks `Continue as @workspace? (Y/n)`, and No connects a new key the way you asked: the Pushary app on your phone by default, or a browser sign-in with `--connect browser`. Without a terminal it names the account and prints `Wrong account? Run npx pushary@latest setup --connect app`. A key found in your agent settings is called the Pushary for Mac key only when Pushary for Mac is installed. A key on an inactive plan names its account and the same `--connect app` line instead of telling you to re-run setup with the saved key.
* The pairing screen prints the QR code last, directly above the waiting line, so it no longer scrolls off a 24-row terminal. Above it: what to scan, the short link, the fingerprint in plain words, the app link, and where to get the app. The browser fallback is offered once, after a minute.
* Stopping setup with SIGTERM while it waits for the phone cancels the pairing, as Ctrl-C already did. An agent whose tool call times out no longer leaves a live QR behind.
* Setup saves a paired key as soon as the key is checked, before it asks which agents to set up. Stopping at the agent list keeps the key, and the next run does not draw a new QR.
* When the phone answers the test question, setup ends with `You're set. Your phone answered in 3s.` and one line asking you to restart open agent sessions. The two "Agent hook activation remains unverified" lines are gone. A test nobody answered now shows `! No answer yet` instead of a tick.
* Setup says `Signed in to @workspace (Agent plan)` instead of `Key verified (workspace · plan agent)`, and names the agents Pushary for Mac handles in plain words.
* The transcript notice is one line: transcripts sync redacted and encrypted, your phone can decrypt them, authorized Pushary compliance admins can recover them for documented legal obligations, and the command that turns sync off. Transcript sync is still on by default.
* The two questions after setup wires your agents say plainly what they change: a Pushary section in this folder's `CLAUDE.md` or `AGENTS.md`, and a `claude` alias in your shell profile. Both still default to No.
* On Windows every printed `npx pushary@latest` command reads `npx.cmd pushary@latest`, which PowerShell runs without the execution policy blocking it.
* When npm cannot install globally (EACCES or EPERM), setup says to use nvm or set an npm prefix, instead of offering a retry that fails the same way.
* Setup prints one line before it installs the phone-start background service, with `--control off` to turn it off.
* When Pushary for Mac is signed into a different account, setup names both accounts and the two ways out: sign Pushary for Mac into this setup's account, or take over the hooks with `--take-over-hooks`.
* An unreachable phone is pointed at `npx pushary@latest setup --connect app` to pair the Pushary app, not at web push.
* On an unsupported Node, `pushary` says which releases it needs, which one you have, and where to get Node.js.
## 1.9.15 [#1915]
*Notes corrected on October 4, 2026. "Allow for this session" for an MCP tool also reaches Claude Code questions; the first version said Codex only.*
* Codex in Every time asks your phone only about the calls that matter. Codex raises its own approval when a command leaves its sandbox, a forced `rm` runs, an MCP tool is not marked read-only, or a patch lands outside the project, and Pushary answers that request in your mode as before. Before a call runs, Pushary now adds a question only when the call leaves the scope you agreed, a rule you wrote, a preset you picked or an approval route covers it, the command destroys, rewrites history or reaches the network, or a patch deletes a file. Commands that only build, test or read no longer reach your phone, and an MCP tool Codex asks about itself is asked once, through Codex's own request. The kill switch and your deny rules still apply to every call.
* In Every time with full access, where Codex asks nothing itself, every call your policy gates still comes to your phone, as before.
* When I'm out is unchanged for Codex, except for scope: Codex's own approval requests go to your phone first, and nothing else is asked before a call runs.
* A Codex edit outside the scope you agreed is now asked in Every time and When I'm out, also while you are at the keyboard, because Codex has no prompt for it there. Yes lets it run. No refuses it. No answer before your policy's wait ends refuses it with the path named. In Updates and Terminal, where nobody would be asked, it is refused. A patch that leaves the scope across several files is refused with a request to split it into single-file edits, as with Claude Code. Before this release Codex applied these edits without asking.
* In Updates, Pushary sends an update before a Codex call only for the calls above. Codex's own approval requests still send one each.
* A patch whose `*** Delete File:` or `*** Update File:` header is indented, which Codex applies, is now read as the delete or edit it is, so your path rules and the delete question apply to it.
* A command that deletes through a cloud CLI subcommand spelled as one word, such as `aws dynamodb delete-table`, `aws ec2 terminate-instances`, `heroku apps:destroy` or `firebase firestore:delete`, now counts as destructive for every agent. Pushary asks about it instead of leaving it to the agent's own allowlist, and an "allow Bash for this session" grant does not cover it.
* Codex questions can offer "Allow for this session" for Bash, where risky commands still ask, as Claude Code questions already did.
* After three approvals in a row for one exact MCP tool, the question can offer "Allow for this session" for that tool, in Claude Code and in Codex. It is never offered for Pushary's own tools or for a tool whose name says it runs code, sends, merges, publishes, deploys, writes, deletes or moves money.
## 1.9.14 [#1914]
* Antigravity CLI approvals go to your phone every time. agy has no approval prompt of its own while Pushary is its only gate, so under When I'm out the phone is no longer skipped because you are at the keyboard, and the wait is your policy's timeout instead of the short push window. Before, such a call was blocked with nobody asked, or after 10 seconds when you were away. Other agents keep When I'm out as it was.
## 1.9.13 [#1913]
*Notes corrected on October 4, 2026. The last two bullets, the setup receipt and the Windows exception were missing when 1.9.13 shipped.*
* `pushary setup` makes Pushary the only gate for Antigravity CLI. When `~/.gemini/antigravity-cli/settings.json` has no `toolPermission`, setup sets it to `always-proceed`, so plain `agy` no longer asks in its own prompt and approvals from your phone take effect. A mode you chose yourself, such as `strict`, is left alone, and setup says agy will still ask in its own prompt. Setup records the change in `~/.pushary/setup/antigravity-tool-permission.json`.
* `pushary disconnect antigravity` and `pushary clean` put `toolPermission` back, but only if Pushary set it and only once no Pushary hook is left in `~/.gemini/config/hooks.json`. A value you set or changed is never touched.
* An Antigravity CLI call that Pushary cannot decide is now blocked with a reason instead of running unasked: when Pushary is signed out, cannot be reached, or cannot establish its policy. Other agents are unchanged and still fall back to their own prompts.
* If the Pushary CLI is uninstalled while agy is still connected, agy's approval hook blocks each call and says how to undo the setup, instead of exiting silently and letting the call run. On Windows the hook still exits silently in that case, and agy runs the call.
* A session started from your phone asks your phone every time under When I'm out, also when this computer looks attended, and waits for your policy's timeout. Before, when another session had prompted in the last five minutes or Pushary for Mac was attended, the phone was skipped and the call was denied with "no local approval UI". Nobody was asked.
* The Cursor plugin setup installs asks your phone about every file change under When I'm out, also while you are at the keyboard, and waits for your policy's timeout. Before, the edit was denied with "Switch to Every time" and nobody was asked, or it was denied after the 10 second push window when you were away. Shell commands and MCP calls in Cursor keep When I'm out as it was, because Cursor can prompt for those itself.
## 1.9.12 [#1912]
*Notes corrected on October 4, 2026. 1.9.12 shipped with no notes. The bullets below were added in the 1.9.13 and 1.9.15 packages, and the error text and the refused options in full were added on October 4.*
* An Antigravity CLI tool call that failed is recorded as failed in the session, where it used to show as completed. The session now shows the call's error message: up to its first 500 characters, with secrets redacted, as for other agents.
* `pushary disconnect fx` is recorded in `~/.pushary/declined-agents.json`, so Pushary for Mac versions that read that file leave fx off until you connect it again.
* `rg`, `sort`, `uniq`, `sleep`, `pgrep` and `git branch --show-current` run without asking when they only read. This applies to every agent. `rg` still asks with an option that runs another program (`--pre`, `--pre-glob`, `--hostname-bin`, `--search-zip`, `-z`). `sort` still asks with an option that writes a file or runs a program (`-o`, `--output`, `-T`, `--temporary-directory`, `--compress-program`). `uniq` still asks with a second file, which it writes.
* Codex's `wait_threads` and `send_message_to_thread` run without asking, matched by exact name. The kill switch, a deny rule and a rule you wrote for the tool still win over them.
* A deny rule now blocks read-only commands too. A bare `Bash` deny, a `*` deny or a preset that never runs the shell used to let `ls` and other reads through.
## 1.9.11 [#1911]
* `pushary setup` connects Antigravity CLI (`agy`). Pick it in setup or pass `--agents antigravity_cli`. Setup adds a hook named `pushary` to `~/.gemini/config/hooks.json` for `PreToolUse`, `PostToolUse` and `Stop`, and the Pushary MCP server to `~/.gemini/config/mcp_config.json`. Before agy runs a shell command, writes or edits a file, or calls an MCP tool, your policy decides, and the risky ones wait for your answer on your phone or your Mac. Pushary answers agy only with allow or deny: a denial, or no answer before your policy's wait ends, blocks the call with a reason the model sees.
* Approvals for Antigravity CLI take effect only when agy runs with `--dangerously-skip-permissions` or with `"toolPermission": "always-proceed"` in `~/.gemini/antigravity-cli/settings.json`, because agy ignores a hook's allow in its other modes. Setup prints this and does not change agy's mode. Pushary is then the only gate: if the hook cannot reach Pushary, agy runs the call.
* Setup skips Antigravity CLI when `agy` is not installed, even when `~/.gemini` exists for Gemini CLI.
* While agy waits for your answer, its terminal shows "If nobody answers, it does not run." instead of saying the terminal takes over, which agy has no way to do.
* `pushary disconnect antigravity` removes the `pushary` hook and the Pushary MCP server and leaves your other hooks and servers alone.
## 1.9.10 [#1910]
* `pushary setup` no longer reports Qwen Code, CodeBuddy, Qoder, Droid and Devin as an account mismatch when Pushary for Mac connected them and the CLI's key is in the same account. Setup compares the account of the key in their MCP entry for Qwen Code, CodeBuddy and Qoder, and the account of the Mac app's own key for Droid and Devin, whose hooks run through the app. They are kept and listed as kept for the Mac app. If the app is in a different account, setup reports it and changes nothing. The CLI does not copy or save the app's key.
* `pushary doctor` no longer fails "Global package installed" on a computer Pushary for Mac connects, for example when run with npx. The app runs the hooks there, so doctor prints that the global package is not installed and not needed. It still fails when the CLI has its own key or owns any agent's hooks, because those hooks run the global package.
* `pushary clean --everything` revokes the keys the CLI wrote on pushary.com before removing them: the key in `~/.pushary/config.json`, and a different key in the `export PUSHARY_API_KEY=...` line setup writes to your shell profile. It says whether each key was revoked. A key that is only in the `PUSHARY_API_KEY` environment variable is left alone and named, because Pushary did not write it and it may be a server key you use elsewhere. If pushary.com cannot be reached, the clean still finishes and tells you where to revoke the key. `--dry-run` revokes nothing and lists the keys it would revoke. The Mac app's key is never revoked, and a plain `clean`, `disconnect` and `sign-out` without `--revoke` behave as before.
* `pushary setup --take-over-hooks` takes over Droid's hooks from Pushary for Mac. It used to fail with "no Mac app wiring was found", because Droid keeps its hooks at the top of `~/.factory/hooks.json`.
## 1.9.9 [#199]
* The VS Code hooks setup writes now run the gate through the same shell guard as every other agent. Before, a missing Node or a deleted plugin folder made the VS Code hooks fail on every tool call; now they stay silent and VS Code carries on as if Pushary were not installed. Run setup again for VS Code to get the guarded hooks.
* `pushary doctor`, `pushary status` and `pushary pair` recognise a computer Pushary for Mac connects when the CLI has no key of its own. Doctor and status say the computer is connected through Pushary for Mac instead of reporting a missing key, and pair tells you to pair a phone from the app's Settings > Phone. The CLI does not use, copy or save the app's key.
* A default `pushary setup` turns on phone-start when `~/.pushary/config.json` exists but has no `controlMode`, as `pushary login` and the Mac app's Hermes connection leave it. Before, setup skipped phone-start on those machines. `--control off`, or a saved `controlMode` of `off`, still keeps it off, and upgrades still never add the background service on their own.
* `pushary setup` keeps an OpenCode plugin Pushary for Mac installed, as it already did for the other agents, and lists it as kept. It used to overwrite the app's plugin, MCP key and instructions. Pass `--take-over-hooks` if you want the CLI to own it. If the app is signed into a different account, setup reports that for OpenCode and changes nothing.
## 1.9.8 [#198]
*Notes corrected on October 4, 2026. The first three bullets were missing when 1.9.8 shipped.*
* Phone pairing is now `pushary pair` and signing out is `pushary sign-out`. `pushary connect` and `pushary logout` still work as aliases, with the same flags. Setup, status, doctor and `pushary bell` say "pair a phone".
* `pushary disconnect fx` removes what setup added for fx: the Pushary server in `~/.fx/mcp.json`, the permission rules setup recorded, that record, and the Pushary block in `AGENTS.md`. Your own entries are left alone. With nothing connected it says fx was not connected and changes nothing.
* `pushary mode` accepts `updates-only` and `updates_only` for Updates (`notify_only`). Setup and disconnect accept both agent id forms, such as `claude_code` or `claude`, and reject an unknown id with the list of valid ones.
* `pushary upgrade` and automatic updates no longer take over agents Pushary for Mac connected. They used to replace the app's hooks with the CLI's and overwrite its MCP key for Claude Code, Codex, Gemini CLI, VS Code and OpenCode. An agent the app owns is now left exactly as the app wrote it and listed as kept for the Mac app, using the same rule setup uses. Pass `--take-over-hooks` to setup if you want the CLI to own it.
* `pushary clean` keeps the Mac app's wiring for Qwen Code, CodeBuddy, Qoder, Droid and Devin while the app is installed, as it already did for the other agents. `clean --everything` still removes it.
* `pushary disconnect cursor` removes the Cursor plugin setup installed in `~/.cursor/plugins/local/pushary`, including the `mcp.json` that held your API key, and setup's leftover copies of it. The plugin is kept, and the output says so, when the gate in `~/.cursor/hooks.json` that points into it could not be removed.
* `pushary disconnect` removes the Pushary block it wrote to `AGENTS.md` (Codex, OpenCode), `GEMINI.md`, `QWEN.md` and `CODEBUDDY.md`. Only the text between the Pushary markers is removed. Skills are left in place.
* `pushary disconnect ` is no longer undone by the Mac app. It records the agent in `~/.pushary/declined-agents.json`, and Pushary for Mac versions that read that file leave the agent off until you connect it again from the app or with `pushary setup`.
* `pushary mode`, `mode clear`, `wait ` and `suggestions` no longer say "This key was rejected" when the key is fine. Keys from `pushary login`, phone pairing and the Mac app cannot change approval settings, and these commands now say so, point you to the Pushary app, your phone or the dashboard, and exit with code 2.
* `pushary sign-out --revoke` revokes those keys too, and then checks with pushary.com and tells you whether the key still works. Before, it could not revoke them and always said the key was still valid. `--json` adds `keyStillValid`.
* When the Mac app signs out of the key setup borrowed from it, the background service stops once instead of restarting every 10 seconds, and `pushary doctor`, `pushary status` and commands that report a rejected key say the key was signed out on this Mac, tell you to run `pushary login`, and name the shell profile line that still exports the old key. Pushary does not edit that line. A background service with no key at all also stops instead of restarting.
## 1.9.7 [#197]
* `pushary claude` and the `pushary daemon` commands wait at most 5 seconds for the background service again. Only setup and upgrade allow the 30 seconds a cold Windows start can need, so launching Claude no longer sits silent while the service starts.
* An automatic update whose configuration refresh fails now retries after 5 minutes, then after an hour, then once a day, instead of every 5 minutes. On a Mac with phone-start on, each retry restarted the background service.
* A refresh that cannot succeed until you change something (a legacy phone-start service, phone-start turned on without a usable agent, or a config file Pushary cannot rewrite) is no longer retried automatically. `pushary doctor` shows it, and running `pushary upgrade` after the fix finishes it.
* When a Codex approval hook is turned off in Codex, setup and upgrade tell you to enable it in `/hooks` instead of asking you to trust it. A missing or background (asynchronous) approval hook points you to setup again, as doctor does.
* Upgrades, including automatic ones, regenerate the OpenCode plugin Pushary installed, so the 1.9.4 fix, which makes OpenCode approvals use the directory OpenCode is working in, reaches existing installs without running setup again. A `pushary.js` you wrote yourself is never touched, and no plugin is added where there was none.
## 1.9.6 [#196]
* Read-only shell approval no longer covers commands that make the shell evaluate quoted text again: arithmetic expansion (`$[ ]`), subscripted parameters (`$name[ ]`, including zsh flag prefixes) and `printf -v`. These ran a quoted command substitution in bash or zsh while being approved without asking. They now go to your normal approval.
* An escaped `;`, `&` or `|` no longer splits a command when rating its risk, so destructive words passed as arguments to `sh -c` keep the command destructive and above a standing Bash auto-approve.
## 1.9.5 [#195]
* Setup allows up to 30 seconds each for daemon startup and heartbeat/control proof, allowing cold Windows agent discovery to finish before reporting a failure. Healthy services return immediately.
* Failed phone-start checks show their available local error and report which stage failed, so installation, service startup and server readiness failures can be distinguished.
## 1.9.4 [#194]
* OpenCode approvals use the active directory for project names, policy and relative targets, including nested Git sessions and non-Git workspaces. Run setup again after updating to regenerate an existing plugin.
## 1.9.3 [#193]
* Shell approval checks preserve non-shell Unicode whitespace as part of the executable name, so an unfamiliar command cannot inherit a read-only command’s approval exemption. Shared command analysis keeps risk classification and human approval policy aligned.
## 1.9.2 [#192]
* Setup, doctor and login finish writing their output before exiting, including when output is piped. Windows setup saves the key without printing it when no shell profile is available.
* Editor approvals stop waiting when a question is missing or withdrawn. Pending configuration refreshes retry after five minutes, readiness tolerates client clock skew, and activation checks follow symlinks.
* Codex coverage requires enabled, trusted synchronous permission hooks. Native permission requests show their redacted input without treating it as a shell command or file edit.
## 1.9.1 [#191]
* **Fewer approvals for commands that only read.** A chain of reads joined by `;` or `||`, a `|`, `\` or `{` inside quotes (as in `grep "a\|b" src/` or `grep '^{' file`), `sed -n 50,66p file` and `date '+%H:%M:%S'` no longer wait for you. Before, any of these sent a prompt to your phone or the Mac notch.
* **A read is no longer treated as destructive because of a word it reads.** `tail -5 release.log`, `grep "DELETE FROM" src/db.ts` and `cat deploy.log` were rated destructive because of `release`, `delete` or `deploy`, and a destructive rating overrides your own "auto-approve Bash" rule. They are now rated by what they do. A command that can act is rated as before, including a read piped into a shell or a database client, such as `echo "rm -rf ~" | sh`.
* `git`, `find`, `sed` and `date` now ask when an argument is something the shell expands, such as `$VAR` or a glob at the start of a word. An expansion like that can turn into a flag the check never saw, for example a file named `-delete` matched by `find . -name *`.
* Setup's global install now takes the same lock as `pushary upgrade` and automatic updates, so it never installs over an upgrade that is already running. (#1837)
## 1.9.0 [#190]
* When you scan setup's QR code with the Pushary app, the app now shows which computer is asking to connect: its name, whether it is a Mac, Windows or Linux computer, and the agents setup found on it. Before, it showed only the fingerprint. This needs Pushary app 1.0.31 or later; older apps work as before.
* Denying the request in the Pushary app now stops setup at once with "You denied this computer on your phone". Before, setup kept waiting until the code expired.
* Setup's report to your workspace now says which kind of computer it ran on, so the Pushary app can show it next to the computer.
## 1.8.3 [#183]
* Checking installed agents no longer blocks the daemon while npm or PATH lookups run. Concurrent refreshes share one lookup, and stopping the daemon cancels discovery.
* Automatic and manual upgrades share an operating-system lock. The finishing process keeps it if its parent exits, and a crash releases it without leaving a recovery file that blocks later upgrades. Unavailable locks stop the upgrade.
## 1.8.2 [#182]
* **Starting Codex from your phone no longer picks an old, broken Codex over the one you use.** The daemon looked for agents in npm's global folder before the ones your shell runs, so a broken npm copy of Codex won over a working one elsewhere on your PATH, and the session died at once with `spawn ... ENOENT`. On macOS and Linux the daemon and both session bridges now start the agent your shell finds first. Windows keeps npm's `.cmd` launcher first, because the first match on PATH there can be a script Windows cannot run.
* A Codex install with no native program left in its package, as XProtect left some npm installs, is no longer offered to your phone, and a request for it fails with the path it found and "Reinstall Codex". This is read from the disk; Pushary never runs the agent to check it. Setup, `pushary doctor` and `pushary upgrade` still treat that Codex as installed, as before, so they never stop the background service because of it, and they check the service against the agents it can actually start, so a broken Codex does not make them report that the service failed.
* The daemon checks every minute which agents it can start, so installing or repairing an agent shows on your phone without restarting the daemon. The daemon log says when that list changes.
* The daemon log now records how each session started from your phone ended: finished, stopped from Pushary, interrupted, stopped by a signal, or exited with its code, and after how long. Only an exit with an error code is recorded as the daemon's last error, which `pushary daemon status` and `pushary doctor` show. Stopping a session from your phone, or the kill switch halting it, is not an error.
* The Codex install check that setup also uses no longer judges a standalone Codex by an unrelated npm copy in a folder above it.
## 1.8.1 [#181]
* The CLI is now also published as `pushary`, from the same build as `@pushary/agent-hooks`, and the commands it prints now show `npx pushary@latest`. `npx @pushary/agent-hooks@latest` keeps working.
## 1.8.0 [#180]
* **An approval you give after auto mode blocks a call is no longer lost when the agent rewords the call.** Your approval covers only the exact call that was blocked. Claude Code tells the agent only that it may retry, and an agent that sent a changed command was blocked again, so the approval you gave from your phone or the Mac notch did nothing. Pushary now refuses the first changed call and tells the agent to send the blocked call again unchanged. If the agent ends its turn without sending it, Pushary tells it to send it.
* Claude Code no longer tells your phone and the Mac notch that it is waiting for you while its background tasks are still running. Claude Code reports itself idle after a minute at the prompt even when it will continue by itself once those tasks finish. A question from the agent still reaches you as before.
* A prompt longer than 500 characters, or a shell command longer than about 490, no longer stops your agent at its next phone approval. The hook sent the whole text and the server refused it, so Claude Code halted with "Pushary could not create a verifiable approval" and Codex and OpenCode denied the call. The question, intent, action, blocker and agent name are now cut to what the card can show, and a cut line ends in "…". The server now does the same for every older CLI, so this fix does not wait for you to update. (#1604)
* A line cut to fit, including a long diff, never ends on half an emoji. The server stores these lines as JSON, and Postgres refuses a half character, so the approval failed to save.
* `pushary doctor`, `pushary upgrade`, `pushary clean` and automatic updates recognise a global install under either package name, `@pushary/agent-hooks` or `pushary`. A machine keeps the name it already has, and setup on a new machine installs the name it was run from. Nothing changes for an existing install.
* `pushary doctor` flags a machine with both names installed globally, because uninstalling either one removes every Pushary command.
* `pushary upgrade` no longer installs over an automatic update that is still running. It says another upgrade is running, with its process id, and stops.
## 1.7.3 [#173]
* **Approval text never hides part of a command.** The value after a credential name is now hidden only up to the first space, quote or piece of shell syntax. 1.7.2 could still hide a command: `eval TOKEN="x; rm -rf ~"` read `eval TOKEN="[redacted]"`, `password= reboot` read `password= [redacted]`, and `rm -rf {password=x,/}` hid the `,/` that also deletes `/`. A private key block is hidden only when it holds nothing but the key. Tokens, keys and quoted values without spaces are hidden as before. A quoted value with spaces now shows what follows its first word, as in `--password='[redacted] pass'`. Logs and error reports still hide the whole value.
## 1.7.2 [#172]
* **Approval text shows the whole command again.** A credential name that closes a quoted string, as in `grep -rl "password=" . | xargs rm -f`, made 1.7.1 redact everything after it, so the approval read `grep -rl "password="[redacted]"` and hid the `rm`. The value after a credential name is still redacted, and the rest of the command is shown.
* **Hermes works again after an uninstall.** When setup found Pushary's own Hermes settings already in `~/.hermes/config.yaml`, left by an earlier install, its record of your config included them, and `pushary clean` put them back: approvals stayed pointed at the plugin clean had just uninstalled, and Hermes' `clarify` tool stayed off, so Hermes could not ask you anything in the terminal. Clean now recognises Pushary's own values in that record, including records older versions wrote, and removes `transport: pushary`, the `pushary` plugin entry, and the `clarify` entry that came with Pushary's transport, while restoring everything else you set. Running clean again, or after the Mac app has disconnected Hermes, no longer fails once Hermes' defaults fill the removed settings back in.
* `pushary clean` says what it did to Hermes: "Pushary settings removed" only when it changed `config.yaml`, "no Pushary settings" when there was nothing of ours, and it skips the plugin uninstall when the plugin is not installed. `--dry-run` previews the exact change without saving it.
* `pushary doctor` checks Hermes when Pushary set it up: that the plugin is installed in Hermes' Python and enabled, and whether approvals and questions go through Pushary. A plugin the Mac app left installed but disabled is not reported as a failure.
* Setup on a machine without npm now says so and asks you to install Node.js, instead of printing `/bin/sh: npm: command not found` and suggesting an `npx` retry that needs npm too. Pushary still needs npm; bun, pnpm and yarn installs are not supported yet.
* `pushary clean` tells an npm it cannot find, a package that is not installed and a failed uninstall apart, instead of calling each of them "not installed". A failed uninstall no longer ends in "Clean complete".
* `pushary clean --dry-run` lists only the Mac keychain items and preference domains that exist, and the fx setup record only when there is one. It used to list every candidate it would try. A keychain that cannot be read, for example while locked, still counts as possibly holding Pushary's items, so clean tries them and reports a failure instead of calling the keychain clean.
* `pushary clean` honours `NO_COLOR`.
* Setup's agent picker shows only the names you picked once you confirm, instead of repeating every agent's description, and the transcript notice is indented like the lines around it.
* A message you send from your phone or the Mac notch while Claude Code is running a subagent now waits for Claude Code itself. It used to be handed to the subagent's next tool call, where the main conversation never saw it.
* When your Pushary workspace has no active subscription, Pushary now steps aside and your agent's own approval prompt decides, as it does when Pushary is not installed. Claude Code, Codex, Gemini CLI and OpenCode used to refuse gated tool calls instead, telling you to retry or that the approval was cancelled, and Claude Code questions, plan approvals and MCP forms stopped the same way. A kill switch and your deny rules still apply.
* Your agent's terminal says once per session why Pushary stepped aside, with the link to subscribe.
* A Claude Code or Gemini CLI session started from your phone has no prompt on your machine, so it still refuses the call, now saying the workspace has no active subscription instead of asking you to retry.
* Pairing your phone during setup now says when your workspace has no active subscription, and to subscribe in the Pushary app or at pushary.com before running setup again. It used to say only that pairing failed.
## 1.7.1 [#171]
* **fx asks through Pushary.** Setup now adds `"ask_user_question": "deny"` as the last rule in `~/.fx/settings.json`, which removes fx's own terminal question tool from what its agent can call, so every question goes through `mcp_pushary_ask_user`: to the Mac notch at the Mac, to the phone away from it. fx cannot forward its own prompt, so a question asked there used to wait unseen once you walked away. A rule you already set for `ask_user_question` is kept, and setup names it. The instructions in `~/.fx/AGENTS.md` now tell fx's agent to load `mcp_pushary_ask_user` with `mcp_select_tool` by exact name and to ask there even while you are at the terminal, and to ask in its reply only when Pushary hands the question back or cannot be reached.
* fx's full-access mode (`yolo`, `full-access` or `full access`, from settings, a workspace, or `FX_PERMISSION_MODE`) ignores permission rules, so the terminal question tool stays on there; setup and `pushary doctor` say so instead of claiming questions are routed, and the instructions still steer the agent to Pushary. fx workspaces with their own permission rules replace the global ones, and setup and doctor name how many keep the question tool on. Setup reads the rules the way fx does (globs, last match wins, per-target maps) and leaves a `settings.json` fx would reject, such as one with an unknown action or permission mode, untouched and says fx ignores it.
* `pushary clean` removes the question rule only when setup recorded adding it and it is still `deny`, so a rule of your own survives.
* `pushary clean` now removes Pushary entries from every event in `~/.cursor/hooks.json`, not only the events the permission gate uses. The Mac app registers its bridge on session, compaction and stop events too, and those eight entries were left behind under "Clean complete". Other tools' hooks are untouched.
* `pushary clean` now removes the Pushary block from OpenCode's `~/.config/opencode/AGENTS.md`, as it already did for Codex and Gemini. Setup wrote it and clean left it.
* `pushary clean` removes temp files that an interrupted config write left next to an agent's config (`..pushary-` and `.-