@pushary/agent-hooks 0.79.0 → 0.80.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +52 -0
  3. package/data/SKILL.md +66 -115
  4. package/data/cursor-plugin/skills/pushary/SKILL.md +65 -114
  5. package/data/vscode-plugin/skills/pushary/SKILL.md +65 -114
  6. package/dist/bin/pushary-bell-hook.d.ts +1 -0
  7. package/dist/bin/pushary-bell-hook.js +22 -0
  8. package/dist/bin/pushary-bell.d.ts +1 -0
  9. package/dist/bin/pushary-bell.js +203 -0
  10. package/dist/bin/pushary-claude.js +8 -7
  11. package/dist/bin/pushary-clean.js +14 -14
  12. package/dist/bin/pushary-codex-hook.js +8 -7
  13. package/dist/bin/pushary-codex.js +3 -2
  14. package/dist/bin/pushary-connect.js +15 -14
  15. package/dist/bin/pushary-daemon.js +5 -5
  16. package/dist/bin/pushary-doctor.js +34 -64
  17. package/dist/bin/pushary-gemini-hook.js +8 -7
  18. package/dist/bin/pushary-hook.js +11 -10
  19. package/dist/bin/pushary-login.js +15 -14
  20. package/dist/bin/pushary-logout.js +12 -11
  21. package/dist/bin/pushary-mode.js +7 -7
  22. package/dist/bin/pushary-notification-hook.js +3 -2
  23. package/dist/bin/pushary-permission-denied-hook.js +7 -6
  24. package/dist/bin/pushary-permission-hook.js +7 -6
  25. package/dist/bin/pushary-post-hook.js +3 -2
  26. package/dist/bin/pushary-prompt-hook.js +3 -2
  27. package/dist/bin/pushary-session-end-hook.js +3 -2
  28. package/dist/bin/pushary-session-start-hook.js +3 -2
  29. package/dist/bin/pushary-setup.js +131 -41
  30. package/dist/bin/pushary-stats.js +6 -6
  31. package/dist/bin/pushary-status.js +17 -16
  32. package/dist/bin/pushary-stop-hook.js +3 -2
  33. package/dist/bin/pushary-stopfailure-hook.js +3 -2
  34. package/dist/bin/pushary-suggestions.js +4 -4
  35. package/dist/bin/pushary-upgrade.js +10 -10
  36. package/dist/bin/pushary-wait.js +9 -9
  37. package/dist/bin/pushary.js +6 -5
  38. package/dist/{chunk-PXE6HXRP.js → chunk-7DYFAM32.js} +1 -1
  39. package/dist/{chunk-QVOJF23R.js → chunk-A5DAEWBZ.js} +2 -2
  40. package/dist/{chunk-MGFZXUUR.js → chunk-BOMF4C2Q.js} +114 -14
  41. package/dist/chunk-DNFYUBRP.js +231 -0
  42. package/dist/{chunk-MDPSJR45.js → chunk-DUYM5SHH.js} +10 -1
  43. package/dist/{chunk-NHKWU7MN.js → chunk-E6I3DBJW.js} +8 -1
  44. package/dist/{chunk-NH7RW736.js → chunk-EBP4YG75.js} +1 -1
  45. package/dist/{chunk-NQTRA7H5.js → chunk-GQLB2GBJ.js} +3 -3
  46. package/dist/{chunk-HAP7UTZS.js → chunk-MHA2WB7S.js} +1 -1
  47. package/dist/chunk-PPD3HHNJ.js +44 -0
  48. package/dist/{chunk-TNG3EWFN.js → chunk-Q4QULGAV.js} +1 -1
  49. package/dist/{chunk-FCB6J5YC.js → chunk-R6AYBATA.js} +1 -1
  50. package/dist/{chunk-BBK3TTU2.js → chunk-RRZZ7TFJ.js} +1 -1
  51. package/dist/{chunk-KJFKEKTC.js → chunk-T6BNEQ2A.js} +23 -0
  52. package/dist/{chunk-VDHV6KAS.js → chunk-U2PZKYZM.js} +1 -1
  53. package/dist/{chunk-5Q64QDCR.js → chunk-UBUPETEQ.js} +2 -2
  54. package/dist/{chunk-IXZMT6IS.js → chunk-ULITFWLG.js} +8 -8
  55. package/dist/{chunk-EQJZS2LE.js → chunk-WYVKPLVT.js} +1 -1
  56. package/dist/{chunk-7OYGFJYZ.js → chunk-ZUKH2NPJ.js} +18 -0
  57. package/dist/src/index.js +7 -6
  58. package/package.json +4 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,68 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.80.1
4
+
5
+ ### The bell's heartbeat directory is private to you now
6
+
7
+ `pushary bell` counts how many agents are live by keeping one empty file
8
+ per session in the system temp directory. On macOS that is already a
9
+ per-user directory, so this was fine. On Linux it is the shared `/tmp`,
10
+ where it was not: the first user to create `pushary-bell` owned it, and
11
+ every other user on the box got a permission error and silently counted
12
+ zero agents. Anyone who could write to it could also inflate the count
13
+ and trigger the upgrade line.
14
+
15
+ The directory is now per uid and `0700`, the heartbeats inside it `0600`,
16
+ and the sweep judges an entry with `lstat` so a planted symlink is dropped
17
+ rather than followed. Nothing about the bell itself changes, and it still
18
+ makes no network calls.
19
+
20
+ ## 0.80.0
21
+
22
+ ### A bell, free, for when you only need to know it finished
23
+
24
+ `npx @pushary/agent-hooks@latest bell` makes a noise in your terminal and raises
25
+ a desktop notification when Claude Code says an agent finished or is waiting on
26
+ you. No account, no API key, no network call. Nothing leaves your machine, and
27
+ `pushary bell --off` removes it without touching anything else.
28
+
29
+ If you run one agent, that is genuinely all you need and we would rather you did
30
+ not pay for more. Above three agents at once the bell says so, once a day, and
31
+ then stops talking, because a bell cannot tell you which of them is asking and
32
+ cannot reach you once you have walked away.
33
+
34
+ If you got here from `claude config set --global preferrednotifchannel
35
+ terminal_bell` and the `unknown option '--global'` error, the README now answers
36
+ that directly.
37
+
38
+ ### Setup waits for you to tap, instead of trusting that a push went out
39
+
40
+ Setup used to finish on a delivered notification. A notification that arrives
41
+ and cannot be answered looks exactly the same at that point, and that is the
42
+ most common way this quietly breaks: approvals keep arriving and keep going
43
+ unanswered.
44
+
45
+ So setup now sends a real question and waits for you to answer it, and only says
46
+ `Setup complete, and proven.` once you have. If nothing comes back it tells you
47
+ rather than congratulating you. It only does this when someone is actually
48
+ there, which includes a pairing you just scanned, and never in CI. Pass
49
+ `--verify push` for the old behaviour, or `--verify none` to skip the check.
50
+
51
+ `pushary doctor --roundtrip` runs the same check and now says the same things
52
+ about it.
53
+
54
+ ### Your agent works out when to reach you, without being told
55
+
56
+ The bundled skill used to describe itself only in terms of what you might say to
57
+ it: ping me on my phone, run this overnight. That only fires when you are there
58
+ to say something. It now also describes the moments that are true of the work
59
+ itself: about to do something irreversible, about to spend money or deploy,
60
+ blocked on a decision that is not the agent's to make, or a long task finishing
61
+ with nobody watching.
62
+
63
+ Everything it matched before, it still matches. Re-run
64
+ `npx @pushary/agent-hooks setup` to update the copy your agents already have.
65
+
3
66
  ## 0.79.0
4
67
 
5
68
  ### Your agent now says what kind of update it is sending, so you can route it
package/README.md CHANGED
@@ -24,6 +24,57 @@ There are three things.
24
24
 
25
25
  **Gate.** Risky shell commands (`rm -rf`, force pushes, history rewrites, database drops, deploys) are checked before they run. What happens is set by your dashboard policy: auto approve trusted read-only commands, push to your phone for approval, or hold for the phone and never auto-deny. If the check cannot run, the decision falls back to the agent's own permission prompt in the terminal; nothing is silently allowed. Without an API key configured, the hooks stay out of the way entirely and only count what they would have gated.
26
26
 
27
+ ## Just want a bell? That part is free.
28
+
29
+ If your agent runs in front of you and you only need to know when it finishes,
30
+ you do not need any of the above and you should not pay for it:
31
+
32
+ ```bash
33
+ npx @pushary/agent-hooks@latest bell
34
+ ```
35
+
36
+ That is the whole thing. It makes a noise in your terminal and raises a desktop
37
+ notification when Claude Code says an agent finished or is waiting on you.
38
+ It rides Claude Code's own notification events, so it fires when you have
39
+ actually stepped away rather than after every reply. No account,
40
+ no API key, no network call, nothing leaves your machine. Turn it off with
41
+ `pushary bell --off`; it touches nothing else in your settings.
42
+
43
+ ### If you landed here from `error: unknown option '--global'`
44
+
45
+ You probably ran this, from a blog post or an answer somewhere:
46
+
47
+ ```
48
+ claude config set --global preferrednotifchannel terminal_bell
49
+ error: unknown option '--global'
50
+ ```
51
+
52
+ `claude config set` no longer takes `--global`. The setting moved into
53
+ `~/.claude/settings.json`, so the direct equivalent is to put it there yourself:
54
+
55
+ ```json
56
+ { "preferredNotifChannel": "terminal_bell" }
57
+ ```
58
+
59
+ That rings on Claude Code's own notification events. `pushary bell` does the same
60
+ job through a hook instead, which means it also fires on Stop (the agent
61
+ finished, which is the case most people actually want) and can raise a real
62
+ desktop notification rather than only a beep. Either is fine. Neither costs
63
+ anything.
64
+
65
+ ### Where the free part stops
66
+
67
+ A bell has a ceiling, and the ceiling is how many agents you run:
68
+
69
+ - **One agent.** A bell is perfect. Pushary is overkill and we would rather you
70
+ did not buy it.
71
+ - **Two or three.** The bell rings and you cannot tell which agent. You alt-tab
72
+ looking for it.
73
+ - **Four or more.** The bell is noise, and you have left the room anyway.
74
+
75
+ Above three concurrent agents the bell says so, once a day, and then stops
76
+ talking. Set `PUSHARY_BELL_QUIET=1` if you would rather it never mentioned it.
77
+
27
78
  ## Setup
28
79
 
29
80
  You need an API key. Sign up at [pushary.com](https://pushary.com/sign-up?from=npm), then run:
@@ -70,6 +121,7 @@ status One screen: which key is in force, whether the server accepts it, w
70
121
  login Sign in from this terminal, no key to copy (--with-token to pipe one in)
71
122
  logout Remove the key this machine stores, and say what it does not remove
72
123
  connect Connect a phone without re-running setup (--app for the Pushary app)
124
+ bell Free local bell when an agent finishes or needs you. No account, no key, no network
73
125
  doctor Verify your Pushary installation is working
74
126
  clean Remove all Pushary configuration (--yes for non-interactive)
75
127
  mode Switch approval mode (push_only, push_first, terminal_only)
package/data/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: pushary
3
- version: 0.7.1
4
- description: Push notifications and human-in-the-loop for AI agents. Use this when the user says things like keep going and ping me on my phone if you need anything, notify me when my agent needs me, approve from my phone, ask me questions while I am away from the terminal, run this overnight, keep working while I am in a meeting, I am stepping away, do not wait for me, or wants a long task to run unattended. Send alerts when tasks finish or fail, ask questions (yes/no, multiple choice, or free text) via push, and get answers from the user's lock screen. Use these tools proactively - do not wait for the user to ask for notifications. Works with Claude Code, Codex, Cursor, VS Code, Windsurf, Hermes, Lovable, or any MCP client; no Claude Max subscription required. Pushary is a hosted service, $9.99/mo after a 3-day card-first trial.
3
+ version: 0.9.0
4
+ description: Push notifications and human-in-the-loop for AI agents. Use this whenever a running agent needs a human and nobody is at the terminal: before an irreversible or destructive action, before spending money, deploying, force-pushing or deleting, when blocked on a decision outside your authority, when running unattended and you hit a genuine ambiguity, when another skill's workflow says to confirm with the user, and when a long task finishes or fails with nobody watching. Also use it when the user says things like keep going and ping me on my phone if you need anything, notify me when my agent needs me, approve from my phone, ask me questions while I am away from the terminal, run this overnight, keep working while I am in a meeting, I am stepping away, do not wait for me, or wants a long task to run unattended. Send alerts when tasks finish or fail, ask questions (yes/no, multiple choice, or free text) via push, and get answers from the user's lock screen. Use these tools proactively - do not wait for the user to ask for notifications. Every question and answer is recorded, so an unattended run stays reviewable afterwards. Works with Claude Code, Codex, Cursor, VS Code, Windsurf, Hermes, Lovable, or any MCP client; no Claude Max subscription required. Pushary is a hosted service, $9.99/mo after a 3-day card-first trial.
5
5
  metadata:
6
6
  hermes:
7
7
  tags: [notifications, push, human-in-the-loop, alerts, permissions, mcp]
@@ -22,12 +22,38 @@ Use these tools proactively. Do not wait for the user to ask for notifications.
22
22
 
23
23
  Works with Claude Code, Codex, Cursor, Windsurf, Hermes, Lovable, or any MCP client. No Claude Max subscription required.
24
24
 
25
+ ## Reach for This Without Being Asked
26
+
27
+ 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.
28
+
29
+ - **You are about to do something irreversible.** Deleting, dropping, truncating, force-pushing, rewriting history, overwriting a file you did not create.
30
+ - **You are about to spend money or touch production.** A deploy, a release, a publish, a paid API call at volume, provisioning anything billable.
31
+ - **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.
32
+ - **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.
33
+ - **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_user` so the run continues when they answer instead of blocking on a prompt they never see.
34
+ - **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.
35
+
36
+ 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.
37
+
25
38
  ## Pricing and Free Alternatives
26
39
 
27
40
  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.
28
41
 
29
42
  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.
30
43
 
44
+ ## Plan the Questions Before You Start
45
+
46
+ 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.
47
+
48
+ Before a run of more than a step or two, work out where you will need a human, then fold those points together:
49
+
50
+ - **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.
51
+ - **One `select` carrying the real options beats three sequential `confirm`s.** Same information, a third of the interruptions.
52
+ - **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.
53
+ - **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.
54
+
55
+ `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.
56
+
31
57
  ## When to Use
32
58
 
33
59
  **Send a notification when:**
@@ -106,54 +132,23 @@ npx @pushary/agent-hooks@latest doctor
106
132
 
107
133
  ## Tools
108
134
 
135
+ Every parameter and every returned field is described in each tool's own schema,
136
+ which your client already has and which is always current. What follows is only
137
+ what a schema cannot tell you: when to reach for a tool, what its result means for
138
+ what you do next, and the shapes that are easy to get wrong.
139
+
109
140
  ### send_notification
110
141
 
111
142
  Send a one-way push notification to the user. Optionally include structured context for a rich detail page.
112
143
 
113
- **Parameters:**
114
-
115
- | Name | Type | Required | Description |
116
- |------|------|----------|-------------|
117
- | title | string | Yes | Notification title (max 100 chars, aim for under 60) |
118
- | body | string | Yes | Notification body (max 500 chars, aim for under 200) |
119
- | url | string | No | URL opened when tapped. Ignored if context is provided. |
120
- | agentName | string | No | Identifies which agent sent this (e.g., "Claude Code - myproject") |
121
- | iconUrl | string | No | Custom notification icon URL |
122
- | imageUrl | string | No | Large image shown in the notification |
123
- | sessionId | string | No | Opaque per-session id of the sending agent, so parallel sessions are attributed separately (max 128 chars) |
124
- | machineId | string | No | Stable machine id of the sending agent, so two machines never collapse into one session (max 128 chars) |
125
- | subscriberIds | string[] | No | Target specific subscriber IDs |
126
- | externalIds | string[] | No | Target by external IDs |
127
- | tags | string[] | No | Target by subscriber tags |
128
- | context | object | No | Structured context for a rich detail page (see below) |
129
-
130
- **Context object:**
131
-
132
- | Name | Type | Description |
133
- |------|------|-------------|
134
- | type | "task_complete" / "error" / "info" | The kind of notification |
135
- | summary | string | Short summary of what happened |
136
- | details | string[] | Bullet-point details |
137
- | filesChanged | string[] | List of files that were changed |
138
- | errorMessage | string | Error message (for error type) |
139
- | errorFile | string | File path where the error occurred |
140
- | nextSteps | string | Suggested next steps for the user |
141
- | askQuestion | object | Embed a decision prompt in the notification (see below) |
142
-
143
- **Embedded askQuestion:**
144
-
145
- | Name | Type | Description |
146
- |------|------|-------------|
147
- | question | string | A follow-up question shown below the context |
148
- | type | "confirm" / "select" / "input" | Question type (default: confirm) |
149
- | options | string[] | Options for select type (2-6 items) |
150
-
151
- When `askQuestion` is provided, the response includes a `linkedCorrelationId` you pass to `wait_for_answer`.
152
-
153
- **Returns:**
154
- - `delivery` - per-channel result: `{ "web": { "recipients": <n> }, "mobile": { "recipients": <n> } }` (each channel may also include a `status` like `no_recipients` or `not_configured`)
155
- - `sent` - total devices reached across all channels
156
- - `warning` - present only when the notification reached 0 devices because no phone or browser is connected; the user must connect one in the dashboard under Settings then Connections
144
+ `context.type` is what marks a notification a **task update**, and the user's
145
+ setting for where task updates land can only route one that says so. A
146
+ notification sent without it reaches them wherever the default sends it.
147
+
148
+ On a long run where the user is likely away, prefer `context.askQuestion` over a
149
+ blocking `ask_user`. They get an ordinary push and answer whenever they next pick
150
+ up their phone, rather than you holding a 55-second wait open against someone who
151
+ is not there. Poll the returned `linkedCorrelationId` when you need the result.
157
152
 
158
153
  **Example - task completed with context:**
159
154
 
@@ -196,36 +191,15 @@ When `askQuestion` is provided, the response includes a `linkedCorrelationId` yo
196
191
 
197
192
  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.
198
193
 
199
- **Parameters:**
200
-
201
- | Name | Type | Required | Description |
202
- |------|------|----------|-------------|
203
- | question | string | Yes | The question to ask (max 500 chars) |
204
- | type | "confirm" / "select" / "input" | No | Question type (default: confirm) |
205
- | options | string[] | No | Choices for select type (2-6 options). Required when type is select. |
206
- | placeholder | string | No | Placeholder text for input type (max 200 chars) |
207
- | context | string | No | What the agent is working on, shown above the question (max 500 chars) |
208
- | wait | boolean | No | Wait for the answer before returning (default: true). Set false for manual polling. |
209
- | timeoutMs | integer | No | Max wait time in ms (max 55000). Uses site policy if omitted. |
210
- | agentName | string | No | Identifies which agent is asking. Format: "{Agent} - {project}" (e.g., "Claude Code - myproject") |
211
- | sessionId | string | No | Opaque per-session id of the asking agent, so parallel sessions are attributed separately (max 128 chars) |
212
- | machineId | string | No | Stable machine id of the asking agent, so two machines never collapse into one session (max 128 chars) |
213
- | toolName | string | No | The tool this approval is for (e.g. "Bash"), so the user can choose to always-allow it (max 100 chars) |
214
- | toolTarget | string | No | Compact target of the tool call (e.g. command head "git push" for Bash, or a file extension like ".ts" for Edit/Write). Used to mine always-allow policy suggestions (max 80 chars) |
215
- | callbackUrl | string | No | Webhook URL to POST the answer to when the user responds |
216
- | subscriberIds | string[] | No | Target specific subscriber IDs |
217
- | externalIds | string[] | No | Target by external IDs |
218
- | tags | string[] | No | Target by subscriber tags |
219
-
220
- **Returns (when wait=true, default):**
221
- - `{ "answered": true, "value": "yes", "correlationId": "uuid" }` - user responded
222
- - `{ "answered": false, "timedOut": true, "correlationId": "uuid" }` - timeout reached
223
-
224
- **Returns (when wait=false):**
225
- - `{ "correlationId": "uuid", "status": "pending", "expiresInSeconds": 600 }` - use `wait_for_answer` to poll
226
-
227
- **Returns (when the site policy is notify_only):**
228
- - `{ "correlationId": "uuid", "status": "notified", "answered": false, "mode": "notify_only" }` - the question was pushed but no answer was awaited (the user gets a heads-up, not a blocking prompt). Call `wait_for_answer` if you want to poll for a response anyway.
194
+ Always read `answered` rather than assuming the call blocked. It comes back false
195
+ in three different situations that mean different things: the wait timed out and
196
+ the question is still live (`timedOut`), the site policy is notify_only so nothing
197
+ was awaited (`status: "notified"`), or you passed `wait: false` yourself
198
+ (`status: "pending"`). All three leave a `correlationId` you can poll.
199
+
200
+ Pass `toolName` and `toolTarget` whenever the question is an approval for a tool
201
+ call. They are what let the user turn a repeated approval into an always-allow
202
+ rule, so an approval you label once is an approval they never see again.
229
203
 
230
204
  **Example - confirm (yes/no):**
231
205
 
@@ -264,28 +238,19 @@ Send a question to the user via push notification and wait for their answer. By
264
238
 
265
239
  ### wait_for_answer
266
240
 
267
- Poll for the user's response to a question sent via `ask_user` with `wait: false`. Not needed when using the default blocking mode.
268
-
269
- **Parameters:**
270
-
271
- | Name | Type | Required | Description |
272
- |------|------|----------|-------------|
273
- | correlationId | string (uuid) | Yes | The correlationId from ask_user |
274
- | timeoutMs | integer | No | How long to wait (default 30000, max 55000) |
241
+ 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.
275
242
 
276
- **Returns:**
277
- - `{ "answered": true, "value": "yes" }` - user responded
278
- - `{ "answered": false }` - timeout reached, no answer yet
243
+ A single call waits at most 55 seconds but the question stays answerable for 10
244
+ minutes, so one empty return is not a refusal. Retry with the same
245
+ `correlationId` up to three times at `timeoutMs: 55000` before treating it as
246
+ unanswered.
279
247
 
280
248
  ### cancel_question
281
249
 
282
250
  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).
283
251
 
284
- **Parameters:**
285
-
286
- | Name | Type | Required | Description |
287
- |------|------|----------|-------------|
288
- | correlationId | string (uuid) | Yes | The correlationId of the question to cancel |
252
+ A stale approval arriving twenty minutes later is worse than no approval, because
253
+ it reads as consent to work that has already moved on.
289
254
 
290
255
  ### propose_scope
291
256
 
@@ -295,21 +260,13 @@ The user sees the paths you intend to change, the areas you promise to leave alo
295
260
 
296
261
  Use glob syntax (`src/**`, `**/*.test.ts`). Shell commands are **not** scoped here; they stay governed by the permission policy.
297
262
 
298
- **Parameters:**
299
-
300
- | Name | Type | Required | Description |
301
- |------|------|----------|-------------|
302
- | doneWhen | string | Yes | What "finished" means for this run. Carried for the human to judge against, never enforced automatically |
303
- | sessionId | string | Yes | Your per-session id. A scope with no session cannot be enforced and must never leak into another run |
304
- | allowedPaths | string[] | No | Globs you intend to change. Omit to propose no path restriction, which the user is told plainly |
305
- | offLimitsPaths | string[] | No | Globs you promise not to touch. These win wherever they overlap `allowedPaths` |
306
- | agentName | string | No | Name of the agent asking, format `"{Agent} - {project}"` |
307
- | timeoutMs | integer | No | How long this call blocks, max 55000 |
263
+ `ratified` and `answered` are separate on purpose. Answered but not ratified means
264
+ the user declined: ask what scope they want, and do **not** proceed as if they had
265
+ agreed. Not answered means the scope is simply not in force.
308
266
 
309
- **Returns:**
310
- - `{ "ratified": true, "answered": true, "value": "yes", "contract": {...} }` - the contract is live
311
- - `{ "ratified": false, "answered": true, "value": "no" }` - the user declined. Ask what scope they want; do **not** proceed as if they agreed
312
- - `{ "ratified": false, "answered": false }` - no answer yet. The scope is **not** in force
267
+ Omitting `allowedPaths` proposes no path restriction, and the user is told that
268
+ plainly as "this agent is asking to touch anything", so omit it only when you mean
269
+ it.
313
270
 
314
271
  **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.
315
272
 
@@ -321,15 +278,9 @@ Scope lives for the session only and is never inherited by another run.
321
278
 
322
279
  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.
323
280
 
324
- **Parameters:**
325
-
326
- | Name | Type | Required | Description |
327
- |------|------|----------|-------------|
328
- | status | "active" / "idle" / "waiting" / "errored" | No | Filter to sessions with this computed status |
329
- | limit | integer | No | Max number of sessions to return, most recently seen first (1-100) |
330
-
331
- **Returns:**
332
- - `{ "sessions": [...], "pendingQuestions": [...] }` - current sessions and open questions for the site
281
+ Check it before asking when you are one of several agents: if another session is
282
+ already blocked on a question, adding a second one competes for the same
283
+ attention rather than getting you answered sooner.
333
284
 
334
285
  ## Permission Gating (REQUIRED)
335
286
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pushary
3
- description: Push notifications and human-in-the-loop for AI agents. Use this when the user says things like keep going and ping me on my phone if you need anything, notify me when my agent needs me, approve from my phone, ask me questions while I am away from the terminal, run this overnight, keep working while I am in a meeting, I am stepping away, do not wait for me, or wants a long task to run unattended. Send alerts when tasks finish or fail, ask questions (yes/no, multiple choice, or free text) via push, and get answers from the user's lock screen. Use these tools proactively - do not wait for the user to ask for notifications. Works with Claude Code, Codex, Cursor, VS Code, Windsurf, Hermes, Lovable, or any MCP client; no Claude Max subscription required. Pushary is a hosted service, $9.99/mo after a 3-day card-first trial.
3
+ description: Push notifications and human-in-the-loop for AI agents. Use this whenever a running agent needs a human and nobody is at the terminal: before an irreversible or destructive action, before spending money, deploying, force-pushing or deleting, when blocked on a decision outside your authority, when running unattended and you hit a genuine ambiguity, when another skill's workflow says to confirm with the user, and when a long task finishes or fails with nobody watching. Also use it when the user says things like keep going and ping me on my phone if you need anything, notify me when my agent needs me, approve from my phone, ask me questions while I am away from the terminal, run this overnight, keep working while I am in a meeting, I am stepping away, do not wait for me, or wants a long task to run unattended. Send alerts when tasks finish or fail, ask questions (yes/no, multiple choice, or free text) via push, and get answers from the user's lock screen. Use these tools proactively - do not wait for the user to ask for notifications. Every question and answer is recorded, so an unattended run stays reviewable afterwards. Works with Claude Code, Codex, Cursor, VS Code, Windsurf, Hermes, Lovable, or any MCP client; no Claude Max subscription required. Pushary is a hosted service, $9.99/mo after a 3-day card-first trial.
4
4
  ---
5
5
 
6
6
  # Pushary - Push Notifications for AI Agents
@@ -15,12 +15,38 @@ Use these tools proactively. Do not wait for the user to ask for notifications.
15
15
 
16
16
  Works with Claude Code, Codex, Cursor, Windsurf, Hermes, Lovable, or any MCP client. No Claude Max subscription required.
17
17
 
18
+ ## Reach for This Without Being Asked
19
+
20
+ 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.
21
+
22
+ - **You are about to do something irreversible.** Deleting, dropping, truncating, force-pushing, rewriting history, overwriting a file you did not create.
23
+ - **You are about to spend money or touch production.** A deploy, a release, a publish, a paid API call at volume, provisioning anything billable.
24
+ - **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.
25
+ - **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.
26
+ - **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_user` so the run continues when they answer instead of blocking on a prompt they never see.
27
+ - **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.
28
+
29
+ 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.
30
+
18
31
  ## Pricing and Free Alternatives
19
32
 
20
33
  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.
21
34
 
22
35
  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.
23
36
 
37
+ ## Plan the Questions Before You Start
38
+
39
+ 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.
40
+
41
+ Before a run of more than a step or two, work out where you will need a human, then fold those points together:
42
+
43
+ - **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.
44
+ - **One `select` carrying the real options beats three sequential `confirm`s.** Same information, a third of the interruptions.
45
+ - **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.
46
+ - **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.
47
+
48
+ `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.
49
+
24
50
  ## When to Use
25
51
 
26
52
  **Send a notification when:**
@@ -99,54 +125,23 @@ npx @pushary/agent-hooks@latest doctor
99
125
 
100
126
  ## Tools
101
127
 
128
+ Every parameter and every returned field is described in each tool's own schema,
129
+ which your client already has and which is always current. What follows is only
130
+ what a schema cannot tell you: when to reach for a tool, what its result means for
131
+ what you do next, and the shapes that are easy to get wrong.
132
+
102
133
  ### send_notification
103
134
 
104
135
  Send a one-way push notification to the user. Optionally include structured context for a rich detail page.
105
136
 
106
- **Parameters:**
107
-
108
- | Name | Type | Required | Description |
109
- |------|------|----------|-------------|
110
- | title | string | Yes | Notification title (max 100 chars, aim for under 60) |
111
- | body | string | Yes | Notification body (max 500 chars, aim for under 200) |
112
- | url | string | No | URL opened when tapped. Ignored if context is provided. |
113
- | agentName | string | No | Identifies which agent sent this (e.g., "Claude Code - myproject") |
114
- | iconUrl | string | No | Custom notification icon URL |
115
- | imageUrl | string | No | Large image shown in the notification |
116
- | sessionId | string | No | Opaque per-session id of the sending agent, so parallel sessions are attributed separately (max 128 chars) |
117
- | machineId | string | No | Stable machine id of the sending agent, so two machines never collapse into one session (max 128 chars) |
118
- | subscriberIds | string[] | No | Target specific subscriber IDs |
119
- | externalIds | string[] | No | Target by external IDs |
120
- | tags | string[] | No | Target by subscriber tags |
121
- | context | object | No | Structured context for a rich detail page (see below) |
122
-
123
- **Context object:**
124
-
125
- | Name | Type | Description |
126
- |------|------|-------------|
127
- | type | "task_complete" / "error" / "info" | The kind of notification |
128
- | summary | string | Short summary of what happened |
129
- | details | string[] | Bullet-point details |
130
- | filesChanged | string[] | List of files that were changed |
131
- | errorMessage | string | Error message (for error type) |
132
- | errorFile | string | File path where the error occurred |
133
- | nextSteps | string | Suggested next steps for the user |
134
- | askQuestion | object | Embed a decision prompt in the notification (see below) |
135
-
136
- **Embedded askQuestion:**
137
-
138
- | Name | Type | Description |
139
- |------|------|-------------|
140
- | question | string | A follow-up question shown below the context |
141
- | type | "confirm" / "select" / "input" | Question type (default: confirm) |
142
- | options | string[] | Options for select type (2-6 items) |
143
-
144
- When `askQuestion` is provided, the response includes a `linkedCorrelationId` you pass to `wait_for_answer`.
145
-
146
- **Returns:**
147
- - `delivery` - per-channel result: `{ "web": { "recipients": <n> }, "mobile": { "recipients": <n> } }` (each channel may also include a `status` like `no_recipients` or `not_configured`)
148
- - `sent` - total devices reached across all channels
149
- - `warning` - present only when the notification reached 0 devices because no phone or browser is connected; the user must connect one in the dashboard under Settings then Connections
137
+ `context.type` is what marks a notification a **task update**, and the user's
138
+ setting for where task updates land can only route one that says so. A
139
+ notification sent without it reaches them wherever the default sends it.
140
+
141
+ On a long run where the user is likely away, prefer `context.askQuestion` over a
142
+ blocking `ask_user`. They get an ordinary push and answer whenever they next pick
143
+ up their phone, rather than you holding a 55-second wait open against someone who
144
+ is not there. Poll the returned `linkedCorrelationId` when you need the result.
150
145
 
151
146
  **Example - task completed with context:**
152
147
 
@@ -189,36 +184,15 @@ When `askQuestion` is provided, the response includes a `linkedCorrelationId` yo
189
184
 
190
185
  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.
191
186
 
192
- **Parameters:**
193
-
194
- | Name | Type | Required | Description |
195
- |------|------|----------|-------------|
196
- | question | string | Yes | The question to ask (max 500 chars) |
197
- | type | "confirm" / "select" / "input" | No | Question type (default: confirm) |
198
- | options | string[] | No | Choices for select type (2-6 options). Required when type is select. |
199
- | placeholder | string | No | Placeholder text for input type (max 200 chars) |
200
- | context | string | No | What the agent is working on, shown above the question (max 500 chars) |
201
- | wait | boolean | No | Wait for the answer before returning (default: true). Set false for manual polling. |
202
- | timeoutMs | integer | No | Max wait time in ms (max 55000). Uses site policy if omitted. |
203
- | agentName | string | No | Identifies which agent is asking. Format: "{Agent} - {project}" (e.g., "Claude Code - myproject") |
204
- | sessionId | string | No | Opaque per-session id of the asking agent, so parallel sessions are attributed separately (max 128 chars) |
205
- | machineId | string | No | Stable machine id of the asking agent, so two machines never collapse into one session (max 128 chars) |
206
- | toolName | string | No | The tool this approval is for (e.g. "Bash"), so the user can choose to always-allow it (max 100 chars) |
207
- | toolTarget | string | No | Compact target of the tool call (e.g. command head "git push" for Bash, or a file extension like ".ts" for Edit/Write). Used to mine always-allow policy suggestions (max 80 chars) |
208
- | callbackUrl | string | No | Webhook URL to POST the answer to when the user responds |
209
- | subscriberIds | string[] | No | Target specific subscriber IDs |
210
- | externalIds | string[] | No | Target by external IDs |
211
- | tags | string[] | No | Target by subscriber tags |
212
-
213
- **Returns (when wait=true, default):**
214
- - `{ "answered": true, "value": "yes", "correlationId": "uuid" }` - user responded
215
- - `{ "answered": false, "timedOut": true, "correlationId": "uuid" }` - timeout reached
216
-
217
- **Returns (when wait=false):**
218
- - `{ "correlationId": "uuid", "status": "pending", "expiresInSeconds": 600 }` - use `wait_for_answer` to poll
219
-
220
- **Returns (when the site policy is notify_only):**
221
- - `{ "correlationId": "uuid", "status": "notified", "answered": false, "mode": "notify_only" }` - the question was pushed but no answer was awaited (the user gets a heads-up, not a blocking prompt). Call `wait_for_answer` if you want to poll for a response anyway.
187
+ Always read `answered` rather than assuming the call blocked. It comes back false
188
+ in three different situations that mean different things: the wait timed out and
189
+ the question is still live (`timedOut`), the site policy is notify_only so nothing
190
+ was awaited (`status: "notified"`), or you passed `wait: false` yourself
191
+ (`status: "pending"`). All three leave a `correlationId` you can poll.
192
+
193
+ Pass `toolName` and `toolTarget` whenever the question is an approval for a tool
194
+ call. They are what let the user turn a repeated approval into an always-allow
195
+ rule, so an approval you label once is an approval they never see again.
222
196
 
223
197
  **Example - confirm (yes/no):**
224
198
 
@@ -257,28 +231,19 @@ Send a question to the user via push notification and wait for their answer. By
257
231
 
258
232
  ### wait_for_answer
259
233
 
260
- Poll for the user's response to a question sent via `ask_user` with `wait: false`. Not needed when using the default blocking mode.
261
-
262
- **Parameters:**
263
-
264
- | Name | Type | Required | Description |
265
- |------|------|----------|-------------|
266
- | correlationId | string (uuid) | Yes | The correlationId from ask_user |
267
- | timeoutMs | integer | No | How long to wait (default 30000, max 55000) |
234
+ 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.
268
235
 
269
- **Returns:**
270
- - `{ "answered": true, "value": "yes" }` - user responded
271
- - `{ "answered": false }` - timeout reached, no answer yet
236
+ A single call waits at most 55 seconds but the question stays answerable for 10
237
+ minutes, so one empty return is not a refusal. Retry with the same
238
+ `correlationId` up to three times at `timeoutMs: 55000` before treating it as
239
+ unanswered.
272
240
 
273
241
  ### cancel_question
274
242
 
275
243
  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).
276
244
 
277
- **Parameters:**
278
-
279
- | Name | Type | Required | Description |
280
- |------|------|----------|-------------|
281
- | correlationId | string (uuid) | Yes | The correlationId of the question to cancel |
245
+ A stale approval arriving twenty minutes later is worse than no approval, because
246
+ it reads as consent to work that has already moved on.
282
247
 
283
248
  ### propose_scope
284
249
 
@@ -288,21 +253,13 @@ The user sees the paths you intend to change, the areas you promise to leave alo
288
253
 
289
254
  Use glob syntax (`src/**`, `**/*.test.ts`). Shell commands are **not** scoped here; they stay governed by the permission policy.
290
255
 
291
- **Parameters:**
292
-
293
- | Name | Type | Required | Description |
294
- |------|------|----------|-------------|
295
- | doneWhen | string | Yes | What "finished" means for this run. Carried for the human to judge against, never enforced automatically |
296
- | sessionId | string | Yes | Your per-session id. A scope with no session cannot be enforced and must never leak into another run |
297
- | allowedPaths | string[] | No | Globs you intend to change. Omit to propose no path restriction, which the user is told plainly |
298
- | offLimitsPaths | string[] | No | Globs you promise not to touch. These win wherever they overlap `allowedPaths` |
299
- | agentName | string | No | Name of the agent asking, format `"{Agent} - {project}"` |
300
- | timeoutMs | integer | No | How long this call blocks, max 55000 |
256
+ `ratified` and `answered` are separate on purpose. Answered but not ratified means
257
+ the user declined: ask what scope they want, and do **not** proceed as if they had
258
+ agreed. Not answered means the scope is simply not in force.
301
259
 
302
- **Returns:**
303
- - `{ "ratified": true, "answered": true, "value": "yes", "contract": {...} }` - the contract is live
304
- - `{ "ratified": false, "answered": true, "value": "no" }` - the user declined. Ask what scope they want; do **not** proceed as if they agreed
305
- - `{ "ratified": false, "answered": false }` - no answer yet. The scope is **not** in force
260
+ Omitting `allowedPaths` proposes no path restriction, and the user is told that
261
+ plainly as "this agent is asking to touch anything", so omit it only when you mean
262
+ it.
306
263
 
307
264
  **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.
308
265
 
@@ -314,15 +271,9 @@ Scope lives for the session only and is never inherited by another run.
314
271
 
315
272
  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.
316
273
 
317
- **Parameters:**
318
-
319
- | Name | Type | Required | Description |
320
- |------|------|----------|-------------|
321
- | status | "active" / "idle" / "waiting" / "errored" | No | Filter to sessions with this computed status |
322
- | limit | integer | No | Max number of sessions to return, most recently seen first (1-100) |
323
-
324
- **Returns:**
325
- - `{ "sessions": [...], "pendingQuestions": [...] }` - current sessions and open questions for the site
274
+ Check it before asking when you are one of several agents: if another session is
275
+ already blocked on a question, adding a second one competes for the same
276
+ attention rather than getting you answered sooner.
326
277
 
327
278
  ## Permission Gating (REQUIRED)
328
279