Reach a human from a running agent. Approvals on the lock screen, plus a cross-agent audit trail.
io.github.Pushary/pushary — MCP Server
The io.github.Pushary/pushary server (Pushary Agent Skill) provides push notifications and human-in-the-loop behavior for AI agents, including approvals from a lock screen and an audit trail across agents. It is packaged as an MCP server skill with 5 tools.
🛠️ Key Features
Push notifications for AI agents
Human-in-the-loop approvals on the lock screen
Cross-agent audit trail
Total tool set: 5
🚀 Use Cases
Reaching a human from a running agent
Gating agent actions with user approvals
Tracing activity across multiple agents
⚡ Developer Benefits
Supports human approval workflows for agent-driven tasks
Provides an audit trail for cross-agent oversight
⚠️ Limitations
Only described capabilities: approvals, lock-screen interaction, push notifications, and cross-agent audit trail (no other tool behavior specified)
Send a one-way push notification to the user's phone and browser. Nothing is awaited; use ask_user instead when you need an answer back. Reach for this when a long-running task finishes and the user asked to be told, when the agent hits an error it cannot resolve on its own, or for any "notify me when my agent needs me" moment while the user is away from the terminal. By default the notification reaches every device connected to the site; narrow delivery with subscriberIds, externalIds, or tags. The optional context object turns the tap-through into a rich detail page (summary, bullet details, changed files, error info, next steps), and context.askQuestion embeds a decision prompt on that page, returning a linkedCorrelationId you can poll with wait_for_answer. Returns per-channel delivery counts for web and mobile, plus a warning when zero devices are connected. Works from Claude Code, Codex, Cursor, Hermes, or any MCP client; no Claude subscription is required. SIDE EFFECT: delivers real notifications to real devices immediately.
Parameters12
title
string
required
Notification title shown on the lock screen (max 100 chars). Lead with the outcome, e.g. "Build finished" or "Migration failed".
body
string
required
Notification body text (max 500 chars). One or two sentences the user can act on without opening anything.
url
string
optional
URL opened when the user taps the notification. Ignored if context is provided, because a context detail page URL is generated automatically.
iconUrl
string
optional
URL of the notification icon image
imageUrl
string
optional
URL of a large image shown in the notification
agentName
string
optional
Name of the agent sending this notification, format "{Agent} - {project}" (e.g. "Claude Code - myproject"). Shown in the notification so the user knows which session is talking. Falls back to the MCP client name if omitted.
sessionId
string
optional
Opaque per-session id of the sending agent, so parallel sessions are attributed separately in the activity feed.
machineId
string
optional
Stable machine id of the sending agent, so two machines never collapse into one session.
subscriberIds
array
optional
Deliver only to these subscriber IDs. Omit all targeting fields to reach every connected device.
externalIds
array
optional
Deliver only to subscribers matching these external IDs.
tags
array
optional
Deliver only to subscribers that have any of these tags.
context
object
optional
Structured context rendered as a rich detail page when the user taps the notification. Strongly recommended for task_complete and error notifications so the user can act from their phone.
Raw schema
{
"type": "object",
"properties": {
"title": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Notification title shown on the lock screen (max 100 chars). Lead with the outcome, e.g. \"Build finished\" or \"Migration failed\"."
},
"body": {
"type": "string",
"minLength": 1,
"maxLength": 500,
"description": "Notification body text (max 500 chars). One or two sentences the user can act on without opening anything."
},
"url": {
"description": "URL opened when the user taps the notification. Ignored if context is provided, because a context detail page URL is generated automatically.",
"type": "string",
"format": "uri"
},
"iconUrl": {
"description": "URL of the notification icon image",
"type": "string",
"format": "uri"
},
"imageUrl": {
"description": "URL of a large image shown in the notification",
"type": "string",
"format": "uri"
},
"agentName": {
"description": "Name of the agent sending this notification, format \"{Agent} - {project}\" (e.g. \"Claude Code - myproject\"). Shown in the notification so the user knows which session is talking. Falls back to the MCP client name if omitted.",
"type": "string",
"maxLength": 100
},
"sessionId": {
"description": "Opaque per-session id of the sending agent, so parallel sessions are attributed separately in the activity feed.",
"type": "string",
"maxLength": 128
},
"machineId": {
"description": "Stable machine id of the sending agent, so two machines never collapse into one session.",
"type": "string",
"maxLength": 128
},
"subscriberIds": {
"description": "Deliver only to these subscriber IDs. Omit all targeting fields to reach every connected device.",
"type": "array",
"items": {
"type": "string"
}
},
"externalIds": {
"description": "Deliver only to subscribers matching these external IDs.",
"type": "array",
"items": {
"type": "string"
}
},
"tags": {
"description": "Deliver only to subscribers that have any of these tags.",
"type": "array",
"items": {
"type": "string"
}
},
"context": {
"description": "Structured context rendered as a rich detail page when the user taps the notification. Strongly recommended for task_complete and error notifications so the user can act from their phone.",
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"task_complete",
"error",
"info"
],
"description": "What kind of update this is. Use \"task_complete\" when work finished, \"error\" when something failed (delivered with high urgency), \"info\" for everything else."
},
"summary": {
"description": "Short summary of what happened, shown at the top of the detail page",
"type": "string"
},
"details": {
"description": "Bullet-point details rendered as a list",
"type": "array",
"items": {
"type": "string"
}
},
"filesChanged": {
"description": "Paths of files that were created or modified",
"type": "array",
"items": {
"type": "string"
}
},
"errorMessage": {
"description": "The error message, when type is \"error\"",
"type": "string"
},
"errorFile": {
"description": "File path where the error occurred",
"type": "string"
},
"nextSteps": {
"description": "What the user should do next, e.g. \"Review the PR\" or \"Re-run with --force\"",
"type": "string"
},
"askQuestion": {
"description": "Embed a decision prompt on the detail page. The response includes a linkedCorrelationId; pass it to wait_for_answer to collect the answer. The embedded question expires 10 minutes after it is created.",
"type": "object",
"properties": {
"question": {
"type": "string",
"minLength": 1,
"maxLength": 500,
"description": "A follow-up question shown below the context (e.g. \"Retry with a different approach?\")"
},
"type": {
"default": "confirm",
"description": "Question type: confirm (yes/no), select (pick from options), or input (free text)",
"type": "string",
"enum": [
"confirm",
"select",
"input"
]
},
"options": {
"description": "The 2 to 6 choices for a select question",
"minItems": 2,
"maxItems": 6,
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"question"
]
}
},
"required": [
"type"
]
}
},
"required": [
"title",
"body"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}
ask_user
Ask the user a question as a push notification on their phone and block until they answer. Reach for this whenever you need the user's decision and they may be away from the terminal: approving a risky or irreversible step (deleting files, force pushing, spending money, sending external messages), picking between implementation options, or supplying missing input. The user answers from the lock screen or a decision page; you do not need a separate wait_for_answer call because this tool waits by default. Three question types: "confirm" (yes/no), "select" (2 to 6 fixed choices), "input" (free text). Timing: a single call blocks for at most 55 seconds, but the question itself stays answerable for 10 minutes. On { answered: true } the response carries value with the user's choice or text. On { answered: false, timedOut: true } keep the returned correlationId and call wait_for_answer with it, retrying up to 3 times with timeoutMs 55000, before falling back to asking in the terminal. Every response carries answerUrl, the signed-in dashboard page where this question is waiting. When you report that you are waiting, print that URL to the user so they can answer from a browser instead of hunting for it. Works from Claude Code, Codex, Cursor, Hermes, or any MCP client; no Claude subscription is required. SIDE EFFECT: sends a real push notification.
Parameters24
question
string
required
The question shown on the user's lock screen (max 500 chars). Phrase it so it is answerable at a glance; put background in context instead.
type
string
optional
Question type: confirm renders yes/no buttons, select renders the options list, input renders a free-text field.
options
array
optional
The 2 to 6 choices for a select question. Required when type is "select", ignored otherwise. The answered value is the chosen option string.
questions
array
optional
A bounded Claude-compatible question set rendered and submitted as one phone task. Runtime-populated; ordinary single-question callers should omit it.
placeholder
string
optional
Hint text shown inside the free-text field for input questions
context
string
optional
One or two sentences about what the agent is working on, shown above the question so the user can decide without opening the terminal.
wait
boolean
optional
true (default) blocks until the user answers or the timeout fires. Set false to return immediately with a pending correlationId and poll it yourself via wait_for_answer.
timeoutMs
integer
optional
How long this call blocks, in milliseconds (max 55000). Defaults to the site policy timeout. The question stays open for 10 minutes regardless, so a timeout here is not a refusal; follow up with wait_for_answer.
callbackUrl
string
optional
Webhook URL that receives a POST with the answer when the user responds, signed with the X-Pushary-Signature header. Useful when the agent process may exit before the answer arrives.
agentName
string
optional
Name of the agent asking, format "{Agent} - {project}" (e.g. "Claude Code - myproject"). Shown in the notification title so the user knows which session needs them. Falls back to the MCP client name if omitted.
sessionId
string
optional
Opaque per-session id of the asking agent, so parallel sessions are attributed separately.
machineId
string
optional
Stable machine id of the asking agent, so two machines never collapse into one session.
toolName
string
optional
The tool this approval is for (e.g. "Bash"), so the user can choose to always-allow it.
toolTarget
string
optional
Compact target of the tool call (e.g. the command head "git push" for Bash, or a file extension like ".ts" for Edit/Write). Used to mine policy suggestions.
repoKey
string
optional
Stable repository identity for the working directory, e.g. "github.com/acme/api". Lets an approval routing rule scoped to one repository avoid governing another. Optional; omit it and only workspace-wide routing rules apply.
toolUseId
string
optional
MACHINE-POPULATED. The agent RUNTIME's own identifier for the tool call this approval gates, forwarded verbatim by a hook that received it. Do NOT invent, guess, derive, or reuse a value: two different questions sent under the same id collapse into one, and the second one never reaches a human. If you are a model deciding to call this tool, omit this field.
intent
string
optional
The user's stated task (from their last prompt), one line. Shown as the Intent line so the user can see why the agent stopped.
action
string
optional
The concrete operation about to happen, one line. Shown as the Action line.
blocker
string
optional
The single gating reason the agent stopped, one line. Shown as the Blocker line.
actionBody
string
optional
The diff (Edit/Write) or full command (Bash/apply_patch), secret-redacted and size-capped. Rendered as a collapsible detail block; never used as the push body.
scopePath
string
optional
Set ONLY when this approval exists because the path falls outside the scope the user ratified via propose_scope. Approving then widens the run scope to include this exact path, so the user is not asked again for the same area.
subscriberIds
array
optional
Deliver only to these subscriber IDs. Omit all targeting fields to reach every connected device.
externalIds
array
optional
Deliver only to subscribers matching these external IDs.
tags
array
optional
Deliver only to subscribers that have any of these tags.
Raw schema
{
"type": "object",
"properties": {
"question": {
"type": "string",
"minLength": 1,
"maxLength": 500,
"description": "The question shown on the user's lock screen (max 500 chars). Phrase it so it is answerable at a glance; put background in context instead."
},
"type": {
"default": "confirm",
"description": "Question type: confirm renders yes/no buttons, select renders the options list, input renders a free-text field.",
"type": "string",
"enum": [
"confirm",
"select",
"input"
]
},
"options": {
"description": "The 2 to 6 choices for a select question. Required when type is \"select\", ignored otherwise. The answered value is the chosen option string.",
"minItems": 2,
"maxItems": 6,
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
},
"questions": {
"description": "A bounded Claude-compatible question set rendered and submitted as one phone task. Runtime-populated; ordinary single-question callers should omit it.",
"minItems": 1,
"maxItems": 4,
"type": "array",
"items": {
"type": "object",
"properties": {
"question": {
"type": "string",
"minLength": 1,
"maxLength": 500
},
"header": {
"type": "string",
"maxLength": 40
},
"multiSelect": {
"type": "boolean"
},
"options": {
"minItems": 2,
"maxItems": 4,
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"description": {
"type": "string",
"maxLength": 500
}
},
"required": [
"label"
]
}
}
},
"required": [
"question",
"multiSelect",
"options"
]
}
},
"placeholder": {
"description": "Hint text shown inside the free-text field for input questions",
"type": "string",
"maxLength": 200
},
"context": {
"description": "One or two sentences about what the agent is working on, shown above the question so the user can decide without opening the terminal.",
"type": "string",
"maxLength": 500
},
"wait": {
"default": true,
"description": "true (default) blocks until the user answers or the timeout fires. Set false to return immediately with a pending correlationId and poll it yourself via wait_for_answer.",
"type": "boolean"
},
"timeoutMs": {
"description": "How long this call blocks, in milliseconds (max 55000). Defaults to the site policy timeout. The question stays open for 10 minutes regardless, so a timeout here is not a refusal; follow up with wait_for_answer.",
"type": "integer",
"minimum": 1000,
"maximum": 55000
},
"callbackUrl": {
"description": "Webhook URL that receives a POST with the answer when the user responds, signed with the X-Pushary-Signature header. Useful when the agent process may exit before the answer arrives.",
"type": "string",
"format": "uri"
},
"agentName": {
"description": "Name of the agent asking, format \"{Agent} - {project}\" (e.g. \"Claude Code - myproject\"). Shown in the notification title so the user knows which session needs them. Falls back to the MCP client name if omitted.",
"type": "string",
"maxLength": 100
},
"sessionId": {
"description": "Opaque per-session id of the asking agent, so parallel sessions are attributed separately.",
"type": "string",
"maxLength": 128
},
"machineId": {
"description": "Stable machine id of the asking agent, so two machines never collapse into one session.",
"type": "string",
"maxLength": 128
},
"toolName": {
"description": "The tool this approval is for (e.g. \"Bash\"), so the user can choose to always-allow it.",
"type": "string",
"maxLength": 100
},
"toolTarget": {
"description": "Compact target of the tool call (e.g. the command head \"git push\" for Bash, or a file extension like \".ts\" for Edit/Write). Used to mine policy suggestions.",
"type": "string",
"maxLength": 80
},
"repoKey": {
"description": "Stable repository identity for the working directory, e.g. \"github.com/acme/api\". Lets an approval routing rule scoped to one repository avoid governing another. Optional; omit it and only workspace-wide routing rules apply.",
"type": "string",
"maxLength": 200
},
"toolUseId": {
"description": "MACHINE-POPULATED. The agent RUNTIME's own identifier for the tool call this approval gates, forwarded verbatim by a hook that received it. Do NOT invent, guess, derive, or reuse a value: two different questions sent under the same id collapse into one, and the second one never reaches a human. If you are a model deciding to call this tool, omit this field.",
"type": "string",
"maxLength": 200
},
"intent": {
"description": "The user's stated task (from their last prompt), one line. Shown as the Intent line so the user can see why the agent stopped.",
"type": "string",
"maxLength": 500
},
"action": {
"description": "The concrete operation about to happen, one line. Shown as the Action line.",
"type": "string",
"maxLength": 500
},
"blocker": {
"description": "The single gating reason the agent stopped, one line. Shown as the Blocker line.",
"type": "string",
"maxLength": 500
},
"actionBody": {
"description": "The diff (Edit/Write) or full command (Bash/apply_patch), secret-redacted and size-capped. Rendered as a collapsible detail block; never used as the push body.",
"type": "string",
"maxLength": 4000
},
"scopePath": {
"description": "Set ONLY when this approval exists because the path falls outside the scope the user ratified via propose_scope. Approving then widens the run scope to include this exact path, so the user is not asked again for the same area.",
"type": "string",
"maxLength": 200
},
"subscriberIds": {
"description": "Deliver only to these subscriber IDs. Omit all targeting fields to reach every connected device.",
"type": "array",
"items": {
"type": "string"
}
},
"externalIds": {
"description": "Deliver only to subscribers matching these external IDs.",
"type": "array",
"items": {
"type": "string"
}
},
"tags": {
"description": "Deliver only to subscribers that have any of these tags.",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"question"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}
propose_scope
Propose what this 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 stops being auto-approvable: it becomes a separate "wants to widen scope" question instead of a silent approval, so you are asked once about the boundary rather than repeatedly about each file. Use glob syntax ("src/**", "**/*.test.ts"). Shell commands are NOT scoped here, they stay governed by the permission policy. Scope lives for this session only and is never inherited by another run. Returns { ratified, answered, value }; ratified:true means the contract is live. If the user declines, do not proceed as if they had agreed, ask them what scope they want. SIDE EFFECT: sends a real push notification.
Parameters7
doneWhen
string
required
What "finished" means for this run, one or two lines. Carried for the human to judge against; never enforced automatically.
sessionId
string
required
Your per-session id. Required: a scope with no session cannot be enforced, and must never leak into another run.
allowedPaths
array
optional
Globs you intend to change, e.g. ["src/**", "docs/*.md"]. Omit or leave empty to propose no path restriction, which the user is told plainly.
offLimitsPaths
array
optional
Globs you promise not to touch, e.g. ["**/.env*", "infra/**"]. These win wherever they overlap allowedPaths.
agentName
string
optional
Name of the agent asking, format "{Agent} - {project}".
machineId
string
optional
Stable machine id, so two machines never collapse into one session.
timeoutMs
integer
optional
How long this call blocks, in milliseconds (max 55000).
Raw schema
{
"type": "object",
"properties": {
"doneWhen": {
"type": "string",
"minLength": 1,
"maxLength": 300,
"description": "What \"finished\" means for this run, one or two lines. Carried for the human to judge against; never enforced automatically."
},
"sessionId": {
"type": "string",
"minLength": 1,
"maxLength": 128,
"description": "Your per-session id. Required: a scope with no session cannot be enforced, and must never leak into another run."
},
"allowedPaths": {
"description": "Globs you intend to change, e.g. [\"src/**\", \"docs/*.md\"]. Omit or leave empty to propose no path restriction, which the user is told plainly.",
"maxItems": 40,
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 200
}
},
"offLimitsPaths": {
"description": "Globs you promise not to touch, e.g. [\"**/.env*\", \"infra/**\"]. These win wherever they overlap allowedPaths.",
"maxItems": 40,
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 200
}
},
"agentName": {
"description": "Name of the agent asking, format \"{Agent} - {project}\".",
"type": "string",
"maxLength": 100
},
"machineId": {
"description": "Stable machine id, so two machines never collapse into one session.",
"type": "string",
"maxLength": 128
},
"timeoutMs": {
"description": "How long this call blocks, in milliseconds (max 55000).",
"type": "integer",
"minimum": 1000,
"maximum": 55000
}
},
"required": [
"doneWhen",
"sessionId"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}
wait_for_answer
Poll for the user's answer to a previously created question. Use it in three cases: after ask_user with wait set to false, after an ask_user call that returned timedOut, or with the linkedCorrelationId from a send_notification that embedded an askQuestion. Each call blocks until the answer arrives or timeoutMs expires (default 30 seconds, max 55). Questions live for 10 minutes in Redis, so when a call comes back { answered: false }, retry with the same correlationId up to 3 times with timeoutMs 55000 to give the user time to reach their phone; only then treat the question as unanswered and fall back to asking in the terminal. Returns { answered: true, value } once the user responds, where value is "yes"/"no" for confirm, the chosen option for select, or the typed text for input.
Parameters2
correlationId
string
required
The correlationId from an earlier ask_user response, or the linkedCorrelationId from a send_notification with an embedded askQuestion
timeoutMs
integer
optional
How long this call blocks, in milliseconds (default 30000, max 55000). Retry with the same correlationId to keep waiting; the question expires 10 minutes after it was asked.
Raw schema
{
"type": "object",
"properties": {
"correlationId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The correlationId from an earlier ask_user response, or the linkedCorrelationId from a send_notification with an embedded askQuestion"
},
"timeoutMs": {
"description": "How long this call blocks, in milliseconds (default 30000, max 55000). Retry with the same correlationId to keep waiting; the question expires 10 minutes after it was asked.",
"type": "integer",
"minimum": 1000,
"maximum": 55000
}
},
"required": [
"correlationId"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}
cancel_question
Retract a pending question so it can no longer be answered. Use this when a question became irrelevant before the user replied: the agent found the answer itself, the task was aborted, or a newer question supersedes it. Cancelling prevents a stale approval from arriving later and acting on work that has moved on. Only affects questions that are still pending; questions expire on their own 10 minutes after creation. Returns { cancelled: true } when a pending question was removed, { cancelled: false } when it was already answered, expired, or unknown.
Parameters1
correlationId
string
required
The correlationId of the pending question to cancel, as returned by ask_user or send_notification
Raw schema
{
"type": "object",
"properties": {
"correlationId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The correlationId of the pending question to cancel, as returned by ask_user or send_notification"
}
},
"required": [
"correlationId"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}
Try an approval without signing up
→ connect an open phone page → approve or decline → see the agent continue or stop.
This browser demo uses polling and temporary state, not real push delivery or the production ledger.
The skill and adapters are MIT-licensed. Hosted phone delivery requires a Pushary
account; embedding approvals for your own users requires Partner access.
The two framework examples also run locally with simulated answers and no credentials.
Your AI agent finishes a 20-minute refactor while you're making coffee. Without Pushary, you'd never know until you checked back. With Pushary, you get a push notification on your phone the moment it's done - or a question on your lock screen when the agent needs a decision. It's the tool behind "keep going, ping me on my phone if you need anything".
Pushary is a hosted service: $9.99/mo after a 3-day card-first trial. It works with Claude Code, Claude Cowork, Codex, Cursor, Windsurf, Hermes, Lovable, or any MCP client, and it does not need a Claude Max subscription. If you run Claude Code with Claude Max, Anthropic Remote Control covers that one setup for free. Pushary covers what it does not: Codex, Cursor, Claude Code without Max, a cross-agent fleet, enforced policy gating, lock screen answer buttons, and an audit trail of every question and answer.
How It Works
code
AI Agent -> MCP Protocol -> Pushary API -> Push Notification -> Your Phone
Pushary is an MCP server that connects your AI coding agent to push notifications. The agent calls Pushary's tools over the Model Context Protocol, and you receive notifications instantly on any device.
Three question types - yes/no confirmations, multiple choice, and free text input. Your agent picks the right one for the situation.
Rich context notifications - agents can include file changes, error details, and suggested next steps in a detail page you see when tapping the notification.
Agent identification - when you run multiple agents, each notification shows which agent is asking so you always know what you're responding to.
Permission hooks - route Claude Code's tool approval prompts through push notifications so you can approve or deny from your phone.
Personal setup: phone first
Run the pairing setup in your agent's terminal:
bash
npx @pushary/agent-hooks@latest setup
Install Pushary on your phone, scan the QR or open the printed link, compare the fingerprints, and approve. Setup then configures the selected agent's tools, supported hooks and skill. You do not need to copy an API key first. Personal accounts require a plan; the current offer is $9.99/mo after a 3-day card-first trial. Existing credentials are reused.
Verify setup:
bash
npx @pushary/agent-hooks@latest doctor
Ask your agent for one harmless test question. Confirm the intended device receives it and the answer returns to the same task.
This installs the native plugin in Hermes' interpreter and configures it. The plugin provides notification, question, wait, cancel and scope tools, plus Partner enrollment and customer-question tools. Its parameters use snake_case; use the Hermes skill, not MCP parameter names. The native approval transport preserves Hermes' session and standing approvals.
Mac notch and other answer surfaces
Install Pushary for Mac, sign in with the same personal account and connect your agents from the app. The notch provides confirm, choice, text and question-set controls with keyboard support. Keep the app running for presence and delivery; verify one harmless question there, then phone fallback while away from the Mac.
Surface
Answers
Mobile
Confirm, choice and text in the app. Supported confirm notifications expose lock-screen approve/deny; arbitrary choices and text open the app.
Mac notch
Personal account questions, including keyboard answering. No Partner customer inbox is currently provided.
Slack
Buttons, choice menus and text modals when the integration and recipient are configured.
Browser
Decision-page fallback and browser notifications when permission is enabled.
Delivery follows your settings and presence. push_first uses presence, push_only requests push every time, notify_only keeps decisions in the current client, and terminal_only avoids push. A successful API call alone does not prove a device displayed the request.
Partner setup: your own customers
Personal pairing connects you, the operator. Partner integrations enroll each of your application's customers through a scoped connection link and address decisions using your stable customer ID. Customers use the mobile enrollment flow; do not route their approvals to your personal Mac inbox or share an operator API key.
Keep the key private. Install the skill with npx skills add Pushary/pushary-skill if the client supports skills.sh. Manual MCP provides cooperative questions and notifications; enforced approvals require the supported host hooks/runtime integration.
Claude Code users can alternatively install this repository as a plugin:
That path reads PUSHARY_API_KEY from the environment. Pick one installation path: installing the plugin on top of CLI-installed Claude hooks can run hooks twice. Use npx @pushary/agent-hooks@latest clean before switching paths.
Setup: Lovable
Lovable is a hosted app builder, so there is no local install step (npx skills add does not apply). You connect the MCP server in Lovable's UI, then paste the skill into Lovable's Knowledge so the agent uses it on its own.
1. Connect the MCP server. In Lovable, go to Settings -> Connectors -> Personal connectors (paid Lovable plans), click New MCP server, set the URL to https://pushary.com/api/mcp/mcp, choose Bearer token, and paste your API key (pk_xxx.sk_xxx).
2. Add the skill to Knowledge. Lovable cannot install skills via npx, so paste the skill guidance into Settings -> Knowledge. Use the condensed SKILL-LITE.md as the source. This makes the Lovable agent notify you when a build finishes and ask before risky changes, without you prompting each time.
Lovable gets notifications and questions only (no enforced gate, since it has no permission hook). Full walkthrough: Lovable guide.
Setup: Claude Cowork
Claude Cowork is Anthropic's agentic workspace inside the Claude apps. It is a hosted surface (npx skills add does not apply), so you connect Pushary as a custom connector and add the skill through Cowork's own skill upload.
1. Connect the MCP server. Get your connector link from the Pushary dashboard (Settings -> Connections, Claude section). In Claude, open Settings -> Connectors -> Add custom connector, leave the OAuth fields empty, and paste the link. Connectors are account level, so the same connector is available inside Cowork; enable it in a session under Customize -> Connectors.
2. Add the skill. Zip the skills/pushary-cowork folder and upload it in Cowork under Customize -> Skills (skills need code execution enabled). Alternatively, paste the standing instructions block from your Pushary dashboard into Claude Settings -> Cowork, so sessions ask for unresolved decisions and report meaningful unattended results while respecting authorization already given.
Cowork gets notifications and questions only (no enforced gate; this connector does not install native permission hooks). Full walkthrough: Claude Cowork guide. Dedicated plugin repo: Pushary/cowork-plugin.
Tools
The skill guides these MCP tools:
Tool
Description
send_notification
Send a push notification with optional rich context (file changes, errors, next steps)
ask_user
Ask the user a question via push - yes/no, multiple choice, or free text
wait_for_answer
Long-poll for the user's response to a question
cancel_question
Cancel a pending question that's no longer relevant
list_sessions
Read-only view of your live agent sessions and pending questions
propose_scope
Ratify the boundary of a run: file globs, enforced on supported hosts, or a recorded promise when the run changes no files
Full tool documentation with parameters, examples, and usage guidelines is in skills/pushary/SKILL.md.
Human-in-the-Loop
The agent can ask you decisions via push and wait for your answer:
code
Agent: "Which auth strategy should I use?"
Options: JWT tokens / Session cookies / OAuth2 + PKCE
-> push to your phone
You: tap "JWT tokens"
Agent: proceeds with JWT implementation
Supports three question types:
Confirm - yes/no binary decisions
Select - pick from 2-6 options
Input - free text response
Read answered, status and handoffAction (or nextAction) from the tool result. Poll only as directed; cancel a live question before handing it to another surface and honor any winning answer. Expiry and waits follow server policy. Silence is not approval, and a late reply cannot restart an ended agent turn.
server.json is not published by merging it. The MCP registry serves one record per version, so an edit here reaches nobody until the new version is pushed to the registry, and the old text keeps being what every agent reads.
Everyday path: bump version in server.json, merge to main, then sync the public skill mirror. Its release workflow publishes the new registry entry.
The release workflow publishes from the public Pushary repository so GitHub OIDC can prove ownership of the io.github.Pushary/pushary namespace. Verify the workflow and registry result after release.
That endpoint returns every version, oldest first. Read the entry whose _meta."io.modelcontextprotocol.registry/official".isLatest is true; the first row in the list is the oldest record, not the live one.
Contributing
Contributions are welcome! Please read the contributing guide before submitting a pull request.
Security
If you discover a security vulnerability, please report it responsibly. See SECURITY.md for details.
Funding
This project is fully funded by RalphNex OU, an Estonian software development agency.