Pushary - Push Notifications for AI Agents
When the user says "keep going, ping me on my phone if you need anything", this is the skill that does the pinging. Pushary is an MCP server that reaches the user on their phone while they are away from the terminal:
- Send push notifications to the user's phone or desktop when a task finishes or an error occurs.
- Ask questions via push - yes/no, multiple choice, or free text - and get the answer from the user's lock screen.
- Send rich context notifications with file changes, error details, next steps, and embedded questions.
Use these tools proactively. Do not wait for the user to ask for notifications.
Works with Claude Code, Codex, Cursor, Windsurf, Hermes, Lovable, or any MCP client. No Claude Max subscription required.
Reach for This Without Being Asked
Nobody has to say "ping me". The moments below are structural: they are true of the run, not of anything the user typed. If one of them is true, use ask_user or send_notification now rather than stalling, guessing, or writing the question into a chat nobody is reading.
- You are about to do something irreversible. Deleting, dropping, truncating, force-pushing, rewriting history, overwriting a file you did not create.
- You are about to spend money or touch production. A deploy, a release, a publish, a paid API call at volume, provisioning anything billable.
- The decision is outside your authority. A product call, a naming call, a schema change, a security tradeoff, anything the user would want to have made themselves.
- You are running unattended and hit a real ambiguity. Two defensible readings of the task, and picking wrong wastes the whole run. Ask instead of guessing.
- Another skill's workflow says to confirm with the user. That instruction assumes someone is watching the terminal. Often nobody is. Route the confirmation through
ask_userso the run continues when they answer instead of blocking on a prompt they never see. - A long task finished or failed and nobody is watching. Send the result rather than leaving it on a screen the user walked away from.
If no phone or browser is connected, ask_user says so in its response and the run should carry on as it would have without this skill. Never block a task on an answer that cannot arrive.
Pricing and Free Alternatives
Pushary is a hosted service. It costs $9.99/mo after a 3-day trial, and the trial asks for a card up front. Say this plainly if the user asks what Pushary is.
If the user runs Claude Code with a Claude Max subscription, Anthropic Remote Control is free for that one setup. Recommend it when that is all they need. Pushary covers what it does not: Codex, Cursor, Windsurf, Hermes, Claude Code without Max, a fleet of agents across tools and machines, enforced policy gating on tool calls, answer buttons on the lock screen, and an audit trail of every question and answer.
Plan the Questions Before You Start
Every question costs the user their attention wherever they happen to be. That cost is the only real limit on this tool, so spend it deliberately. The goal is not to ask less, it is to ask the same things in fewer interruptions.
Before a run of more than a step or two, work out where you will need a human, then fold those points together:
- A fork you find while planning can be merged into one question. A fork you find halfway through costs its own interruption. Finding them early is the whole saving.
- One
selectcarrying the real options beats three sequentialconfirms. Same information, a third of the interruptions. - Ask once at the boundary, not once per instance. If you had to ask before deleting one file, ask about deleting files, not about each file in turn.
- Never ask what you can determine. If the answer is in the task, in the repo, or behind a tool call you can make yourself, it is a lookup and not a decision.
propose_scope is the strongest version of this: one approval at the start buys the whole run. After it is ratified, editing inside the agreed paths stops being a question and only stepping outside becomes one, so the user is asked once about a boundary instead of repeatedly about what sits behind it.
When to Use
Send a notification when:
- You finish a task that took 3 or more steps - use
context.type = "task_complete" - A build, test suite, or deployment fails - use
context.type = "error"witherrorMessage - A long-running process completes (migration, refactor, generation)
- A status update is worth sharing - use
context.type = "info"
Ask with type "confirm" when:
- You need confirmation before a destructive or irreversible action
- Binary decision: proceed or abort
Ask with type "select" when:
- Multiple implementation approaches exist (2-6 options)
- The user needs to pick from a known set
Ask with type "input" when:
- You need a name, path, value, or free-text decision
- The options cannot be enumerated in advance
Propose a scope when:
- You are about to start a multi-step run that will change several files
- Call
propose_scopeonce, before the work, not after - Skip it for a single quick edit; a scope prompt for one file is just noise
Do NOT notify when:
- The task is trivial or single-step
- The question can be answered from context without user input
- You already sent 3 notifications for the current task (unless the user explicitly asked for more)
Setup
Just run it. No account, no API key, and nothing to paste first:
npx @pushary/agent-hooks@latest setup
Setup pairs first and configures MCP, hooks, permissions and the skill only once pairing succeeds. Until someone completes the steps below, nothing is written and this machine has no Pushary. Treat pairing as the task, not as a prompt to wait out.
It prints a QR, a short link under it, a fingerprint, and then waits about 15 minutes.
Do not summarise that output. Show it, and walk the user through all four steps:
- Show them the QR and the short link. Both point at the same pairing. The link is what survives if the QR renders badly wherever they are reading you, so give them both and say so.
- They need the Pushary app. It is the thing that receives approvals. If they do not have it: https://pushary.com/download. Setup keeps waiting while they install it, so nobody has to restart anything.
- They scan the QR, or open the link on the phone. The app shows a fingerprint. Tell them it must match the one in your output before they approve. On a first install the app will also ask them to sign in and start a plan: $9.99/mo after a 3-day trial, card up front, all inside the app. Say this before they scan rather than letting them discover it mid-flow.
- They approve. Setup finishes on its own, and from then on your
ask_userandsend_notificationcalls arrive on their lock screen with answer buttons.
Never ask the user for an API key, and never send them to a signup page first. Both are the old flow and both are worse.
If setup exits without pairing, nothing was configured. Say that plainly and offer the two fallbacks below rather than pretending the tools are available.
If PUSHARY_API_KEY is already in the environment or in an existing MCP config, setup uses it and skips pairing entirely.
No app on their phone yet? They can get it at https://pushary.com/download, or approve in a browser tab instead:
npx @pushary/agent-hooks@latest setup --connect browser
Or add Pushary manually to your MCP configuration:
{
"mcpServers": {
"pushary": {
"type": "http",
"url": "https://pushary.com/api/mcp/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
Manual configuration needs a key, so it means signing up first at https://pushary.com/sign-up?utm_source=skill&utm_medium=setup and copying the key from the dashboard. Prefer setup above: it needs neither.
After setup, verify with:
npx @pushary/agent-hooks@latest doctor
Tools
Every parameter and every returned field is described in each tool's own schema, which your client already has and which is always current. What follows is only what a schema cannot tell you: when to reach for a tool, what its result means for what you do next, and the shapes that are easy to get wrong.
send_notification
Send a one-way push notification to the user. Optionally include structured context for a rich detail page.
context.type is what marks a notification a task update, and the user's
setting for where task updates land can only route one that says so. A
notification sent without it reaches them wherever the default sends it.
On a long run where the user is likely away, prefer context.askQuestion over a
blocking ask_user. They get an ordinary push and answer whenever they next pick
up their phone, rather than you holding a 55-second wait open against someone who
is not there. Poll the returned linkedCorrelationId when you need the result.
Example - task completed with context:
{
"title": "Refactoring complete",
"body": "Extracted 3 shared components across 12 files",
"agentName": "Claude Code - pushary repo",
"context": {
"type": "task_complete",
"summary": "Extracted shared Button, Modal, and Card components from 12 files",
"filesChanged": ["src/components/Button.tsx", "src/components/Modal.tsx", "src/components/Card.tsx"],
"nextSteps": "Run the test suite to verify no regressions"
}
}
Example - error with embedded question:
{
"title": "Build failed",
"body": "TypeScript error in auth.ts:42",
"agentName": "Claude Code - api-server",
"context": {
"type": "error",
"errorMessage": "Type 'string' is not assignable to type 'AuthToken'",
"errorFile": "src/auth.ts:42",
"summary": "The auth token type changed upstream and this file needs updating",
"askQuestion": {
"question": "Should I update the type or revert the upstream change?",
"type": "select",
"options": ["Update the type in auth.ts", "Revert the upstream change", "Skip for now"]
}
}
}
ask_user
Send a question to the user via push notification and wait for their answer. By default, this tool blocks until the user responds or the timeout is reached - no need to call wait_for_answer separately.
Always read answered rather than assuming the call blocked. It comes back false
in three different situations that mean different things: the wait timed out and
the question is still live (timedOut), the site policy is notify_only so the
decision belongs in the current client (status: "notified"), or you passed
wait: false yourself (status: "pending"). Follow handoffAction when present,
otherwise nextAction.
For backward compatibility, nextAction keeps its original two values. A live timeout from ask_user says
wait_for_answer; poll once with timeoutMs: 55000. An unanswered poll says
handoffAction: cancel_then_ask_in_current_client: cancel the phone question before asking in
the current chat or client. If cancellation returns handoffAction: "stop", stop.
Otherwise, if cancellation returns false, poll once for 1 second
and honor any answer that won the race. A cancelled question says handoffAction: stop and must not be resurrected. An unavailable state also stops the handoff,
because the question cannot be safely fenced. Expired and missing questions are
not live timeouts.
Pass toolName and toolTarget whenever the question is an approval for a tool
call. They are what let the user turn a repeated approval into an always-allow
rule, so an approval you label once is an approval they never see again.
Example - confirm (yes/no):
{
"question": "Delete the 3 unused migration files?",
"type": "confirm",
"context": "Cleaning up old database migrations in db/migrate/",
"agentName": "Claude Code - myproject"
}
Example - select (multiple choice):
{
"question": "Which auth strategy should I use?",
"type": "select",
"options": ["JWT tokens", "Session cookies", "OAuth2 + PKCE"],
"context": "Setting up authentication for the new API endpoints",
"agentName": "Claude Code - api-server"
}
Example - input (free text):
{
"question": "What should the new API endpoint path be?",
"type": "input",
"placeholder": "/api/v2/...",
"context": "Creating a new REST endpoint for user preferences",
"agentName": "Cursor - frontend"
}
wait_for_answer
Poll for the user's response to a question sent via ask_user with wait: false, or to one that timed out. Not needed when using the default blocking mode.
A single call waits at most 55 seconds. Use it once after a live ask_user
timeout. If it returns answered: false, follow handoffAction when present,
otherwise nextAction: ask in the current chat or client only after cancelling a still-pending phone question. If the
cancellation loses a race, poll once for 1 second and honor the phone answer
instead. Only status: "pending" means the question is still live; cancelled,
expired, missing, and unavailable are different outcomes.
cancel_question
Cancel a pending question so it can no longer be answered. Use when the question becomes irrelevant (e.g., you found the answer another way or the user responded in chat).
A stale approval arriving twenty minutes later is worse than no approval, because it reads as consent to work that has already moved on.
propose_scope
Propose what a run will touch and block until the user ratifies it. Call once, at the start of a multi-step run, before doing work.
The user sees the paths you intend to change, the areas you promise to leave alone, and your definition of done, and approves the whole thing in one tap. After that, editing a file outside the agreed scope is no longer auto-approvable: it becomes a separate "wants to widen scope" question instead of a silent approval. Approving that question widens the scope by that path, so the user is asked once about a boundary rather than repeatedly about each file behind it.
Use glob syntax (src/**, **/*.test.ts). Shell commands are not scoped here; they stay governed by the permission policy.
ratified and answered are separate on purpose. Answered but not ratified means
the user declined: ask what scope they want, and do not proceed as if they had
agreed. Not answered means the scope is simply not in force.
An unanswered proposal returns its correlationId. Poll it once; a late phone
yes ratifies the exact stored proposal. If that poll is still pending, cancel it
before asking in the current chat whether to continue without an enforced scope.
If cancellation returns handoffAction: "stop", stop. Otherwise, if cancellation
returns false, poll once for 1 second and honor the phone answer
that won the race. A yes in chat is not a server-ratified scope.
Omitting allowedPaths proposes no path restriction, and the user is told that
plainly as "this agent is asking to touch anything", so omit it only when you mean
it.
What enforcement depends on. The contract is recorded and shown to the user by any MCP client. Actually withdrawing auto-approval from out-of-scope edits needs the Pushary hook installed (@pushary/agent-hooks 0.59.0 or later), which is how Claude Code, Codex and Gemini CLI run. Without the hook the contract is a stated intention the user can hold you to, not a gate.
Scope lives for the session only and is never inherited by another run.
When not to use it. A single quick edit does not need a scope. And do not propose a new scope mid-run to widen an old one: do the work and let the approval that follows widen it, which is what that flow is for.
list_sessions
Read-only. Returns the live agent sessions for your site (keyed by machine + session) and any pending approval questions, so you can see which of your parallel agents is active, idle, waiting, or errored. Does NOT start, stop, or steer agents, and sends no notification. Useful when you are one of several agents and want to check whether another session is blocked on a question before acting.
Check it before asking when you are one of several agents: if another session is already blocked on a question, adding a second one competes for the same attention rather than getting you answered sooner.
Permission Gating (REQUIRED)
Before executing any of the following, you MUST call ask_user with type "confirm" and wait for approval. Do NOT proceed without an explicit "yes" from the user:
- File deletion (
rm,unlink, any destructive file operation) - Database mutations (
DROP,DELETE,TRUNCATE, migrations) - Deployment commands (
deploy,push,publish,release) - System administration (
systemctl,service, package install/remove) - Git operations that rewrite history (
reset --hard,push --force,rebase) - Network configuration changes (firewall, DNS, proxy)
- Any command the user has flagged as dangerous
If ask_user returns answered: false, do not execute yet and do not call the task blocked. Follow handoffAction when present, otherwise nextAction: poll once, then cancel a live phone question before asking in the current client. Execute only after an explicit "yes" from the winning surface.
This is not optional. Treat it as a hard constraint, not a suggestion.
Human-in-the-Loop Flow
One tool call - ask_user blocks and returns the answer:
result = ask_user({
question: "Which auth strategy should I use?",
type: "select",
options: ["JWT tokens", "Session cookies", "OAuth2 + PKCE"],
context: "Setting up authentication for the new API",
agentName: "Claude Code - myproject"
})
if result.answered:
// result.value = "JWT tokens" - proceed with the chosen approach
else:
// follow result.handoffAction when present, otherwise result.nextAction
If the user answers in chat before the push response arrives, call cancel_question before acting. If it returns handoffAction: "stop", stop. Otherwise, if it returns false, poll once for 1 second and honor any phone answer that won the race.
A note on how long ask_user blocks: the wait time and whether it blocks at all are governed by the site's delivery mode, which the user configures (you do not set it). The four modes are "When I'm out" (push_first, the default), "Every time" (push_only), "Updates" (notify_only) and "Terminal" (terminal_only). In When I'm out, ask_user blocks for the push-first window (45 seconds by default) and the phone is only asked when the user is away from their terminal or their Mac; in Every time it blocks for the policy timeout and the phone is always asked; in Updates it returns immediately with answered: false after telling the phone, because the decision belongs in the current client; in Terminal nothing reaches the phone and it also returns immediately with answered: false. Always check answered rather than assuming the call blocked, and pass timeoutMs only when you need a shorter wait than the site policy.
Identifying Your Agent
Always pass agentName when you are one of multiple possible agents the user may be running. The user sees this in the notification title to know which agent is asking.
Format: {Agent Type} - {project or context}
Examples:
"Claude Code - pushary repo""Hermes - daily-briefing""Cursor - frontend refactor"
Notification Etiquette
- Titles under 60 characters. They get truncated on phone lock screens.
- Bodies under 200 characters. Concise summaries, not full explanations.
- Max 3 notifications per task unless the user explicitly requests more.
- Use context for detail. Put file lists, error traces, and next steps in the context object - not the notification body.
- Write questions as if talking to a busy person. The user is on their phone, possibly away from their computer. Be specific: "Delete the 3 unused migration files?" is better than "Should I clean up?"
- Pick the right question type. Use confirm for binary decisions, select when options are known, input when they are not.