@pushary/agent-hooks 0.87.2 → 0.87.4

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 (63) hide show
  1. package/data/cursor-plugin/hooks/hooks.json +7 -0
  2. package/data/cursor-plugin/scripts/pushary-gate.mjs +94 -28
  3. package/data/cursor-plugin/scripts/pushary-gate.test.mjs +81 -1
  4. package/dist/bin/pushary-bell-hook.js +4 -4
  5. package/dist/bin/pushary-bell.js +7 -6
  6. package/dist/bin/pushary-claude.js +14 -11
  7. package/dist/bin/pushary-clean.js +36 -20
  8. package/dist/bin/pushary-codex-bridge.js +6 -5
  9. package/dist/bin/pushary-codex-hook.js +129 -52
  10. package/dist/bin/pushary-codex.js +4 -3
  11. package/dist/bin/pushary-connect.js +6 -6
  12. package/dist/bin/pushary-daemon.js +5 -4
  13. package/dist/bin/pushary-disconnect.js +65 -8
  14. package/dist/bin/pushary-doctor.js +79 -18
  15. package/dist/bin/pushary-elicitation-hook.js +1 -1
  16. package/dist/bin/pushary-gemini-bridge.js +6 -5
  17. package/dist/bin/pushary-gemini-hook.js +12 -10
  18. package/dist/bin/pushary-hook.js +6 -5
  19. package/dist/bin/pushary-login.js +4 -4
  20. package/dist/bin/pushary-logout.js +5 -4
  21. package/dist/bin/pushary-mode.js +3 -3
  22. package/dist/bin/pushary-notification-hook.js +4 -3
  23. package/dist/bin/pushary-opencode-hook.d.ts +1 -0
  24. package/dist/bin/pushary-opencode-hook.js +329 -0
  25. package/dist/bin/pushary-permission-denied-hook.js +6 -5
  26. package/dist/bin/pushary-permission-hook.js +6 -5
  27. package/dist/bin/pushary-post-hook.js +4 -3
  28. package/dist/bin/pushary-prompt-hook.js +4 -3
  29. package/dist/bin/pushary-session-end-hook.js +4 -3
  30. package/dist/bin/pushary-session-start-hook.js +4 -3
  31. package/dist/bin/pushary-setup.js +238 -84
  32. package/dist/bin/pushary-stats.js +2 -2
  33. package/dist/bin/pushary-status.js +5 -5
  34. package/dist/bin/pushary-stop-hook.js +4 -3
  35. package/dist/bin/pushary-stopfailure-hook.js +4 -3
  36. package/dist/bin/pushary-upgrade.js +7 -6
  37. package/dist/bin/pushary-wait.js +3 -3
  38. package/dist/{chunk-5646VKR4.js → chunk-2JYQAW4B.js} +5 -53
  39. package/dist/chunk-7J67NYK4.js +287 -0
  40. package/dist/{chunk-M7RYIZX6.js → chunk-B7ZBHWOW.js} +10 -1
  41. package/dist/{chunk-QBWYNVKY.js → chunk-BFBAHOGR.js} +1 -1
  42. package/dist/{chunk-7YJS2VTG.js → chunk-DMW54VWK.js} +12 -1
  43. package/dist/{chunk-3LKYBFWA.js → chunk-EJL3PJU7.js} +1 -1
  44. package/dist/chunk-GIRT7677.js +11 -0
  45. package/dist/{chunk-SJVLFIYP.js → chunk-H5PIPIKQ.js} +1 -1
  46. package/dist/{chunk-HAUTLCMA.js → chunk-ICERLAE2.js} +1 -1
  47. package/dist/{chunk-ZZE6FN7T.js → chunk-K42RXJPG.js} +52 -1
  48. package/dist/{chunk-Y3UTTFTZ.js → chunk-KBS53WOE.js} +1 -1
  49. package/dist/{chunk-A4I5JGCW.js → chunk-KFAH6KRE.js} +1 -1
  50. package/dist/{chunk-LT2VLPUK.js → chunk-KULUNLRO.js} +18 -2
  51. package/dist/{chunk-B3V2H6SA.js → chunk-NAPRGHZ3.js} +54 -13
  52. package/dist/{chunk-HJ44HIUW.js → chunk-OMA2OZRE.js} +11 -9
  53. package/dist/{chunk-ECEUMWBH.js → chunk-QLMX5LLV.js} +1 -1
  54. package/dist/{chunk-XW5CHAQX.js → chunk-QXZ2CKRK.js} +2 -2
  55. package/dist/{chunk-JAICP7BA.js → chunk-TD3M232T.js} +1 -1
  56. package/dist/{chunk-WLLBFDVY.js → chunk-TV3UFGT7.js} +1 -1
  57. package/dist/{chunk-7MBDFNUC.js → chunk-VIHRP25S.js} +1 -1
  58. package/dist/{chunk-V2XPF77V.js → chunk-VOE4SYPR.js} +42 -279
  59. package/dist/{chunk-OT4YD54Q.js → chunk-YX6ZKCC5.js} +1 -1
  60. package/dist/{chunk-2T3GX7WG.js → chunk-YYTLTEGU.js} +1 -1
  61. package/dist/chunk-ZGC3W23N.js +371 -0
  62. package/dist/src/index.js +6 -5
  63. package/package.json +2 -1
@@ -8,6 +8,13 @@
8
8
  "timeout": 60,
9
9
  "failClosed": true
10
10
  }
11
+ ],
12
+ "beforeMCPExecution": [
13
+ {
14
+ "command": "node ./scripts/pushary-gate.mjs",
15
+ "timeout": 60,
16
+ "failClosed": true
17
+ }
11
18
  ]
12
19
  }
13
20
  }
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // Pushary gate — Cursor `beforeShellExecution` hook.
2
+ // Pushary gate — Cursor `beforeShellExecution` and `beforeMCPExecution` hooks.
3
3
  //
4
4
  // Routes risky shell commands through your Pushary permission policy before they
5
5
  // run. Which commands reach this gate is the `matcher` in ../hooks/hooks.json; what
@@ -16,7 +16,9 @@
16
16
  // Self-contained: no dependencies, uses the global fetch (Node 18+).
17
17
  //
18
18
  // Contract (https://cursor.com/docs/hooks):
19
- // stdin : { "command": string, "cwd": string, "conversation_id": string, ... }
19
+ // stdin : { "hook_event_name": string, "cwd": string, "conversation_id": string, ... }
20
+ // beforeShellExecution adds { "command": string }
21
+ // beforeMCPExecution adds { "tool_name", "tool_input", "mcp_server_name" }
20
22
  // stdout : { "permission": "allow" | "deny" | "ask", "user_message"?, "agent_message"? }
21
23
  //
22
24
  // Failure model: every handled path writes a decision and exits 0. Network/parse
@@ -245,12 +247,12 @@ const callTool = async (apiKey, name, args) => {
245
247
  const GATE_TIMEOUT_MS = 5000
246
248
 
247
249
  /**
248
- * The verdict for one command, or null if we could not get one.
250
+ * The verdict for one request, or null if we could not get one.
249
251
  *
250
252
  * Null is not a denial and not an approval: every failure here hands the call
251
253
  * back to Cursor's own prompt, exactly as if this gate were not installed.
252
254
  */
253
- const decide = async (apiKey, command, cwd, sessionId) => {
255
+ const decide = async (apiKey, request, cwd, sessionId) => {
254
256
  try {
255
257
  const response = await fetch(`${BASE_URL}/api/agent/gate`, {
256
258
  method: 'POST',
@@ -258,8 +260,8 @@ const decide = async (apiKey, command, cwd, sessionId) => {
258
260
  body: JSON.stringify({
259
261
  v: 1,
260
262
  source: 'cursor',
261
- toolName: 'Bash',
262
- toolInputs: [{ command }],
263
+ toolName: request.toolName,
264
+ toolInputs: request.toolInputs,
263
265
  cwd,
264
266
  repoKey: deriveRepoKey(cwd),
265
267
  sessionId,
@@ -341,16 +343,16 @@ const deriveActionBody = (command) => capActionBody(redactSecretsDeep(command))
341
343
  // lock screen ungated.
342
344
  const commandHead = (command) => command.trim().split(/\s+/).slice(0, 2).join(' ').slice(0, 120)
343
345
 
344
- const askArgs = (command, project, ident) => ({
345
- question: `Allow this command?\n\n${redactSecrets(command)}`,
346
+ const askArgs = (request, project, ident) => ({
347
+ question: `${request.prompt}\n\n${redactSecrets(request.display)}`,
346
348
  type: 'confirm',
347
- context: `Cursor agent wants to run this in ${project}`,
349
+ context: `Cursor agent wants to ${request.verb} in ${project}`,
348
350
  agentName: ident.agentName,
349
351
  sessionId: ident.sessionId,
350
352
  machineId: ident.machineId,
351
- toolName: 'Bash',
352
- toolTarget: commandHead(command),
353
- actionBody: deriveActionBody(command),
353
+ toolName: request.toolName,
354
+ toolTarget: request.toolTarget,
355
+ actionBody: deriveActionBody(request.display),
354
356
  wait: false,
355
357
  })
356
358
 
@@ -374,13 +376,77 @@ const pollForAnswer = async (apiKey, correlationId, deadlineMs) => {
374
376
  const fromTimeoutAction = (action, deniedReason) =>
375
377
  action === 'approve' ? ALLOW : action === 'deny' ? deny(deniedReason) : ask()
376
378
 
377
- const DENIED = 'The user denied this command via a Pushary push approval. Do not run it — propose an alternative or ask how to proceed.'
379
+ const denialFor = (noun) =>
380
+ `The user denied this ${noun} via a Pushary push approval. Do not run it — propose an alternative or ask how to proceed.`
381
+
382
+ /**
383
+ * What this hook is being asked about, in the one shape the rest of the gate
384
+ * reads.
385
+ *
386
+ * Cursor sends a different payload per event and the gate used to know only one
387
+ * of them: it read `input.command`, called everything "Bash", and any event that
388
+ * was not a shell execution fell out at the empty-command guard as `ask`. That
389
+ * was correct while `beforeShellExecution` was the only thing registered.
390
+ *
391
+ * Returns null when there is nothing to gate, which the caller answers with
392
+ * Cursor's own prompt.
393
+ */
394
+ const describeRequest = (input) => {
395
+ const event = typeof input.hook_event_name === 'string' ? input.hook_event_name : ''
396
+
397
+ if (event === 'beforeMCPExecution') {
398
+ const tool = typeof input.tool_name === 'string' ? input.tool_name.trim() : ''
399
+ if (!tool) return null
400
+ const server = typeof input.mcp_server_name === 'string' ? input.mcp_server_name.trim() : ''
401
+ // `mcp__<server>__<tool>`, the same spelling Claude Code uses and the one
402
+ // `validateToolPattern` accepts, so a policy someone wrote once matches the
403
+ // same tool whichever agent called it. That portability is the only reason
404
+ // the policy engine is server-side.
405
+ const toolName = server ? `mcp__${server}__${tool}` : `mcp__${tool}`
406
+
407
+ // Never gate Pushary's own MCP tools. Asking Pushary to approve Pushary's
408
+ // ask_user call deadlocks: the approval cannot be delivered until the call
409
+ // it is gating goes through. The Claude hook carries the same guard, where
410
+ // it is a precaution because the matcher does not route these there. Here it
411
+ // is load-bearing: `beforeMCPExecution` is registered with NO matcher, so
412
+ // every MCP call reaches this gate, ours included.
413
+ if (toolName.startsWith('mcp__pushary__')) return null
414
+
415
+ const params = typeof input.tool_input === 'string'
416
+ ? input.tool_input
417
+ : input.tool_input === undefined ? '' : JSON.stringify(input.tool_input)
418
+ return {
419
+ toolName,
420
+ toolInputs: [{ tool: toolName, params }],
421
+ display: params ? `${toolName}\n${params}` : toolName,
422
+ toolTarget: toolName.slice(0, 120),
423
+ prompt: 'Allow this tool call?',
424
+ verb: 'call this tool',
425
+ denied: denialFor('tool call'),
426
+ }
427
+ }
428
+
429
+ // Shell is also the fallback for a payload with no event name: a `command` is
430
+ // what `beforeShellExecution` has always been recognised by, and an older
431
+ // Cursor that omits `hook_event_name` must keep working exactly as before.
432
+ const command = typeof input.command === 'string' ? input.command.trim() : ''
433
+ if (!command || (event && event !== 'beforeShellExecution')) return null
434
+ return {
435
+ toolName: 'Bash',
436
+ toolInputs: [{ command }],
437
+ display: command,
438
+ toolTarget: commandHead(command),
439
+ prompt: 'Allow this command?',
440
+ verb: 'run this',
441
+ denied: denialFor('command'),
442
+ }
443
+ }
378
444
 
379
445
  // push_only: wait up to the policy timeout, then apply the timeout action.
380
- const handlePushOnly = async (apiKey, command, project, ident, timeoutSeconds, timeoutAction) => {
446
+ const handlePushOnly = async (apiKey, request, project, ident, timeoutSeconds, timeoutAction) => {
381
447
  let asked
382
448
  try {
383
- asked = await withRetry(() => callTool(apiKey, 'ask_user', askArgs(command, project, ident)), 3)
449
+ asked = await withRetry(() => callTool(apiKey, 'ask_user', askArgs(request, project, ident)), 3)
384
450
  } catch {
385
451
  return fromTimeoutAction(timeoutAction, 'Push notification failed; denied per your Pushary policy.')
386
452
  }
@@ -389,7 +455,7 @@ const handlePushOnly = async (apiKey, command, project, ident, timeoutSeconds, t
389
455
  const realMs = Math.max(timeoutSeconds, 1) * 1000
390
456
  const cap = Math.min(realMs, MAX_BLOCK_MS)
391
457
  const answer = await pollForAnswer(apiKey, asked.correlationId, Date.now() + cap)
392
- if (answer.answered) return answer.value === 'yes' ? ALLOW : deny(DENIED)
458
+ if (answer.answered) return answer.value === 'yes' ? ALLOW : deny(request.denied)
393
459
 
394
460
  // If Cursor's hook limit cut us off before the configured timeout, hand off to
395
461
  // Cursor's own prompt rather than misapplying the policy's timeout action.
@@ -398,10 +464,10 @@ const handlePushOnly = async (apiKey, command, project, ident, timeoutSeconds, t
398
464
  }
399
465
 
400
466
  // push_first: race the push for a short window, then fall back to Cursor's prompt.
401
- const handlePushFirst = async (apiKey, command, project, ident, pushFirstSeconds) => {
467
+ const handlePushFirst = async (apiKey, request, project, ident, pushFirstSeconds) => {
402
468
  let asked
403
469
  try {
404
- asked = await withRetry(() => callTool(apiKey, 'ask_user', askArgs(command, project, ident)), 3)
470
+ asked = await withRetry(() => callTool(apiKey, 'ask_user', askArgs(request, project, ident)), 3)
405
471
  } catch {
406
472
  return ask()
407
473
  }
@@ -409,16 +475,16 @@ const handlePushFirst = async (apiKey, command, project, ident, pushFirstSeconds
409
475
 
410
476
  const cap = Math.min(Math.max(pushFirstSeconds, 1) * 1000, MAX_BLOCK_MS)
411
477
  const answer = await pollForAnswer(apiKey, asked.correlationId, Date.now() + cap)
412
- if (answer.answered) return answer.value === 'yes' ? ALLOW : deny(DENIED)
478
+ if (answer.answered) return answer.value === 'yes' ? ALLOW : deny(request.denied)
413
479
  return ask('Sent to your phone via Pushary — you can also approve here.')
414
480
  }
415
481
 
416
482
  // notify_only: fire an awareness notification, let Cursor's prompt decide.
417
- const handleNotifyOnly = async (apiKey, command, project, ident) => {
483
+ const handleNotifyOnly = async (apiKey, request, project, ident) => {
418
484
  try {
419
485
  await callTool(apiKey, 'send_notification', {
420
486
  title: 'Agent needs approval',
421
- body: redactSecrets(command).slice(0, 180),
487
+ body: redactSecrets(request.display).slice(0, 180),
422
488
  agentName: ident.agentName,
423
489
  sessionId: ident.sessionId,
424
490
  machineId: ident.machineId,
@@ -436,8 +502,8 @@ const main = async () => {
436
502
  return respond(ask())
437
503
  }
438
504
 
439
- const command = typeof input.command === 'string' ? input.command.trim() : ''
440
- if (!command) return respond(ask())
505
+ const request = describeRequest(input)
506
+ if (!request) return respond(ask())
441
507
 
442
508
  const apiKey = resolveApiKey()
443
509
  if (!apiKey) {
@@ -451,7 +517,7 @@ const main = async () => {
451
517
  const ident = { agentName: `Cursor - ${project}`, sessionId, machineId: getMachineId() }
452
518
 
453
519
  try {
454
- const verdict = await decide(apiKey, command, input.cwd, sessionId)
520
+ const verdict = await decide(apiKey, request, input.cwd, sessionId)
455
521
 
456
522
  // No verdict, or one that says nothing: Cursor's own prompt decides, exactly
457
523
  // as if this gate were not installed. Never a forced denial on an outage.
@@ -467,12 +533,12 @@ const main = async () => {
467
533
  case 'terminal_only':
468
534
  return respond(ask())
469
535
  case 'notify_only':
470
- return respond(await handleNotifyOnly(apiKey, command, project, ident))
536
+ return respond(await handleNotifyOnly(apiKey, request, project, ident))
471
537
  case 'push_only':
472
- return respond(await handlePushOnly(apiKey, command, project, ident, tool.timeoutSeconds, tool.timeoutAction))
538
+ return respond(await handlePushOnly(apiKey, request, project, ident, tool.timeoutSeconds, tool.timeoutAction))
473
539
  case 'push_first':
474
540
  default:
475
- return respond(await handlePushFirst(apiKey, command, project, ident, tool.pushFirstSeconds))
541
+ return respond(await handlePushFirst(apiKey, request, project, ident, tool.pushFirstSeconds))
476
542
  }
477
543
  } catch (error) {
478
544
  process.stderr.write(`[pushary-gate] ${error?.message ?? error}\n`)
@@ -482,7 +548,7 @@ const main = async () => {
482
548
 
483
549
  // Exported for scripts/pushary-gate.test.mjs. main() only runs when the gate is
484
550
  // executed directly, so importing this file for a test does not read stdin.
485
- export { normalizeRepoRemote, deriveRepoKey }
551
+ export { normalizeRepoRemote, deriveRepoKey, describeRequest }
486
552
 
487
553
  const isDirectRun = process.argv[1] && import.meta.url === `file://${process.argv[1]}`
488
554
  if (!isDirectRun) {
@@ -14,7 +14,7 @@
14
14
 
15
15
  import assert from 'node:assert/strict'
16
16
  import { describe, it } from 'node:test'
17
- import { normalizeRepoRemote } from './pushary-gate.mjs'
17
+ import { describeRequest, normalizeRepoRemote } from './pushary-gate.mjs'
18
18
 
19
19
  describe('repo identity', () => {
20
20
  it('collapses the forms of one remote', () => {
@@ -33,3 +33,83 @@ describe('repo identity', () => {
33
33
  for (const r of ['', '../rel', 'file:///Users/dev/repo']) assert.equal(normalizeRepoRemote(r), undefined, r)
34
34
  })
35
35
  })
36
+
37
+ // The gate answers two Cursor events now, and they arrive on the same stdin with
38
+ // different fields. What each one turns into is the only thing standing between
39
+ // an MCP call and being gated as if it were a shell command.
40
+ describe('what the gate was asked about', () => {
41
+ it('names an MCP call the way every other agent names one', () => {
42
+ const request = describeRequest({
43
+ hook_event_name: 'beforeMCPExecution',
44
+ tool_name: 'create_issue',
45
+ mcp_server_name: 'github',
46
+ tool_input: '{"title":"x"}',
47
+ })
48
+
49
+ // `mcp__<server>__<tool>` is what `validateToolPattern` accepts and what
50
+ // Claude Code sends, so one policy rule covers the tool on both agents. A
51
+ // Cursor-only spelling would silently miss every rule a user already wrote.
52
+ assert.equal(request.toolName, 'mcp__github__create_issue')
53
+ assert.deepEqual(request.toolInputs, [{ tool: 'mcp__github__create_issue', params: '{"title":"x"}' }])
54
+ assert.match(request.display, /create_issue/)
55
+ })
56
+
57
+ it('still reads a shell command, including from a payload with no event name', () => {
58
+ const named = describeRequest({ hook_event_name: 'beforeShellExecution', command: ' rm -rf /tmp/x ' })
59
+ assert.equal(named.toolName, 'Bash')
60
+ assert.equal(named.toolTarget, 'rm -rf')
61
+
62
+ // An older Cursor that omits `hook_event_name` must keep working exactly as
63
+ // it did when shell was the only registered event.
64
+ assert.equal(describeRequest({ command: 'git push' }).toolName, 'Bash')
65
+ })
66
+
67
+ /**
68
+ * `beforeMCPExecution` is registered with NO matcher, so every MCP call in the
69
+ * editor reaches this gate, ours included. Gating our own tools deadlocks: the
70
+ * approval for `ask_user` cannot reach the phone until the `ask_user` call it
71
+ * is gating goes through. With `failClosed: true` on that entry it is worse
72
+ * than a hang, because a timeout turns into a denial of the user's own
73
+ * question.
74
+ *
75
+ * The Claude hook carries this guard three times over and calls it a
76
+ * precaution, because its matcher does not route these there. Here it is the
77
+ * only thing standing between the two.
78
+ */
79
+ it('never gates Pushary\'s own MCP tools', () => {
80
+ for (const tool of ['ask_user', 'wait_for_answer', 'send_notification', 'propose_scope']) {
81
+ assert.equal(
82
+ describeRequest({
83
+ hook_event_name: 'beforeMCPExecution',
84
+ tool_name: tool,
85
+ mcp_server_name: 'pushary',
86
+ tool_input: '{}',
87
+ }),
88
+ null,
89
+ `mcp__pushary__${tool} must reach Cursor's own prompt, not ours`,
90
+ )
91
+ }
92
+
93
+ // A neighbour whose name merely starts the same way is still gated. The
94
+ // guard is on the `mcp__pushary__` prefix, not on "contains pushary".
95
+ assert.equal(
96
+ describeRequest({
97
+ hook_event_name: 'beforeMCPExecution',
98
+ tool_name: 'run',
99
+ mcp_server_name: 'pushary-clone',
100
+ tool_input: '{}',
101
+ }).toolName,
102
+ 'mcp__pushary-clone__run',
103
+ )
104
+ })
105
+
106
+ it('declines an event it does not gate rather than calling it Bash', () => {
107
+ // beforeReadFile is registered by nobody here on purpose: its response
108
+ // schema has no `ask`, so the no-opinion path this gate takes on every error
109
+ // has nothing to return. If one ever arrives, it must not be mistaken for a
110
+ // shell command carrying no command.
111
+ assert.equal(describeRequest({ hook_event_name: 'beforeReadFile', file_path: '/etc/passwd' }), null)
112
+ assert.equal(describeRequest({ hook_event_name: 'beforeMCPExecution' }), null)
113
+ assert.equal(describeRequest({}), null)
114
+ })
115
+ })
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleBell
4
- } from "../chunk-2T3GX7WG.js";
4
+ } from "../chunk-YYTLTEGU.js";
5
5
  import {
6
6
  readHookInput
7
7
  } from "../chunk-6CUUA7HO.js";
8
- import "../chunk-OT4YD54Q.js";
8
+ import "../chunk-YX6ZKCC5.js";
9
9
  import "../chunk-BC3VCZ3E.js";
10
10
  import "../chunk-6MTNS63X.js";
11
- import "../chunk-Y3UTTFTZ.js";
11
+ import "../chunk-KBS53WOE.js";
12
12
  import "../chunk-2UMNXADU.js";
13
- import "../chunk-7YJS2VTG.js";
13
+ import "../chunk-DMW54VWK.js";
14
14
 
15
15
  // bin/pushary-bell-hook.ts
16
16
  var main = async () => {
@@ -5,18 +5,19 @@ import {
5
5
  liveAgentCount,
6
6
  ring,
7
7
  upgradeThreshold
8
- } from "../chunk-2T3GX7WG.js";
8
+ } from "../chunk-YYTLTEGU.js";
9
9
  import {
10
10
  guardBinary
11
- } from "../chunk-M7RYIZX6.js";
11
+ } from "../chunk-B7ZBHWOW.js";
12
12
  import "../chunk-VT4IVERX.js";
13
13
  import {
14
14
  claudeSettings
15
- } from "../chunk-B3V2H6SA.js";
15
+ } from "../chunk-NAPRGHZ3.js";
16
+ import "../chunk-GIRT7677.js";
16
17
  import {
17
18
  createIo
18
19
  } from "../chunk-5RUIFDTP.js";
19
- import "../chunk-OT4YD54Q.js";
20
+ import "../chunk-YX6ZKCC5.js";
20
21
  import {
21
22
  parseFlags
22
23
  } from "../chunk-GMXKITVA.js";
@@ -30,13 +31,13 @@ import {
30
31
  readJsonSafe,
31
32
  writeJsonAtomic
32
33
  } from "../chunk-6MTNS63X.js";
33
- import "../chunk-Y3UTTFTZ.js";
34
+ import "../chunk-KBS53WOE.js";
34
35
  import "../chunk-7QLSKOSU.js";
35
36
  import {
36
37
  EXIT
37
38
  } from "../chunk-KZERVKTD.js";
38
39
  import "../chunk-2UMNXADU.js";
39
- import "../chunk-7YJS2VTG.js";
40
+ import "../chunk-DMW54VWK.js";
40
41
 
41
42
  // src/bell/install.ts
42
43
  import { join } from "path";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ensureDaemonRunning
4
- } from "../chunk-SJVLFIYP.js";
4
+ } from "../chunk-H5PIPIKQ.js";
5
5
  import {
6
6
  buildTranscriptRecords,
7
7
  decodePublicKey,
@@ -9,34 +9,37 @@ import {
9
9
  generateSessionKey,
10
10
  uploadTranscriptRecords,
11
11
  wrapSessionKey
12
- } from "../chunk-ECEUMWBH.js";
12
+ } from "../chunk-QLMX5LLV.js";
13
13
  import {
14
14
  startProcessOwnership,
15
15
  withSpawnProcessOwnership
16
- } from "../chunk-QBWYNVKY.js";
16
+ } from "../chunk-BFBAHOGR.js";
17
17
  import {
18
18
  needsShell,
19
19
  spawnClaude
20
20
  } from "../chunk-XHKBHWLX.js";
21
21
  import "../chunk-VT4IVERX.js";
22
- import "../chunk-B3V2H6SA.js";
22
+ import "../chunk-NAPRGHZ3.js";
23
23
  import {
24
24
  isDeferAnswer
25
25
  } from "../chunk-YHG74UFF.js";
26
+ import "../chunk-GIRT7677.js";
26
27
  import {
27
- deriveToolTarget,
28
- describeToolCall,
29
28
  fetchModeState,
30
29
  getPolicy,
31
30
  repoKeyFor,
32
- reportEvent,
33
- scopePathFor
34
- } from "../chunk-V2XPF77V.js";
31
+ reportEvent
32
+ } from "../chunk-VOE4SYPR.js";
35
33
  import {
36
34
  askUser,
37
35
  cancelQuestion,
38
36
  waitForAnswer
39
37
  } from "../chunk-J3YDT4HM.js";
38
+ import {
39
+ deriveToolTarget,
40
+ describeToolCall,
41
+ scopePathFor
42
+ } from "../chunk-7J67NYK4.js";
40
43
  import {
41
44
  getMachineId
42
45
  } from "../chunk-RN3NOEJF.js";
@@ -44,7 +47,7 @@ import "../chunk-DINDL2CF.js";
44
47
  import "../chunk-6MTNS63X.js";
45
48
  import "../chunk-BSZYIAZL.js";
46
49
  import "../chunk-SAF6HGAA.js";
47
- import "../chunk-Y3UTTFTZ.js";
50
+ import "../chunk-KBS53WOE.js";
48
51
  import "../chunk-7QLSKOSU.js";
49
52
  import "../chunk-KZERVKTD.js";
50
53
  import {
@@ -54,7 +57,7 @@ import {
54
57
  import {
55
58
  KILL_REASON,
56
59
  resolveGate
57
- } from "../chunk-7YJS2VTG.js";
60
+ } from "../chunk-DMW54VWK.js";
58
61
 
59
62
  // src/wrapper/claudeBinary.ts
60
63
  import { statSync } from "fs";
@@ -2,18 +2,20 @@
2
2
  import {
3
3
  removeClaudeMcpServers,
4
4
  removePusharySettings
5
- } from "../chunk-5646VKR4.js";
5
+ } from "../chunk-2JYQAW4B.js";
6
6
  import {
7
+ CURSOR_GATE_EVENTS,
7
8
  removeInstructionBlock,
8
9
  shortenHome,
9
10
  unregisterPluginLocation,
10
11
  vscodeSettingsTargets
11
- } from "../chunk-LT2VLPUK.js";
12
+ } from "../chunk-KULUNLRO.js";
13
+ import "../chunk-ZGC3W23N.js";
12
14
  import {
13
15
  removeGeminiSettings
14
- } from "../chunk-ZZE6FN7T.js";
16
+ } from "../chunk-K42RXJPG.js";
15
17
  import "../chunk-ATBKJNWS.js";
16
- import "../chunk-M7RYIZX6.js";
18
+ import "../chunk-B7ZBHWOW.js";
17
19
  import {
18
20
  execNpm
19
21
  } from "../chunk-VT4IVERX.js";
@@ -24,6 +26,7 @@ import {
24
26
  claudeSkillDir,
25
27
  codexAgentsMd,
26
28
  codexConfigToml,
29
+ codexHookPositions,
27
30
  codexHooksJson,
28
31
  codexSkillDir,
29
32
  cursorPluginDir,
@@ -32,9 +35,12 @@ import {
32
35
  geminiMd,
33
36
  geminiSettings,
34
37
  pusharyDir,
38
+ removeCodexHookTrust,
35
39
  removeCodexHooks,
36
40
  vscodePluginDir
37
- } from "../chunk-B3V2H6SA.js";
41
+ } from "../chunk-NAPRGHZ3.js";
42
+ import "../chunk-GIRT7677.js";
43
+ import "../chunk-7J67NYK4.js";
38
44
  import {
39
45
  rejectUnknownFlags
40
46
  } from "../chunk-GMXKITVA.js";
@@ -50,7 +56,7 @@ import "../chunk-7QLSKOSU.js";
50
56
  import {
51
57
  EXIT
52
58
  } from "../chunk-KZERVKTD.js";
53
- import "../chunk-7YJS2VTG.js";
59
+ import "../chunk-DMW54VWK.js";
54
60
 
55
61
  // bin/pushary-clean.ts
56
62
  import { existsSync, readFileSync, copyFileSync, readdirSync } from "fs";
@@ -157,11 +163,16 @@ var removeCursorUserHook = () => {
157
163
  }
158
164
  if (!parsed || typeof parsed !== "object") return "failed";
159
165
  const hooks = parsed.hooks ?? {};
160
- const existing = Array.isArray(hooks.beforeShellExecution) ? hooks.beforeShellExecution : [];
161
- const others = existing.filter((h) => !String(h.command ?? "").includes("pushary-gate"));
162
- if (others.length === existing.length) return "absent";
163
- if (others.length === 0) delete hooks.beforeShellExecution;
164
- else hooks.beforeShellExecution = others;
166
+ let removedAny = false;
167
+ for (const event of CURSOR_GATE_EVENTS) {
168
+ const existing = Array.isArray(hooks[event]) ? hooks[event] : [];
169
+ const others = existing.filter((h) => !String(h.command ?? "").includes("pushary-gate"));
170
+ if (others.length === existing.length) continue;
171
+ removedAny = true;
172
+ if (others.length === 0) delete hooks[event];
173
+ else hooks[event] = others;
174
+ }
175
+ if (!removedAny) return "absent";
165
176
  parsed.hooks = hooks;
166
177
  try {
167
178
  writeJson(CURSOR_USER_HOOKS, parsed);
@@ -171,10 +182,12 @@ var removeCursorUserHook = () => {
171
182
  if (dryRun) return "removed";
172
183
  try {
173
184
  const after = JSON.parse(readFileSync(CURSOR_USER_HOOKS, "utf-8"));
174
- const stillThere = after?.hooks?.beforeShellExecution ?? [];
175
- if (Array.isArray(stillThere) && stillThere.some((h) => String(h.command ?? "").includes("pushary-gate"))) {
176
- return "failed";
177
- }
185
+ const afterHooks = after?.hooks ?? {};
186
+ const stillThere = CURSOR_GATE_EVENTS.some((event) => {
187
+ const entries = afterHooks[event];
188
+ return Array.isArray(entries) && entries.some((h) => String(h?.command ?? "").includes("pushary-gate"));
189
+ });
190
+ if (stillThere) return "failed";
178
191
  } catch {
179
192
  return "failed";
180
193
  }
@@ -297,11 +310,15 @@ var main = async () => {
297
310
  console.log(` ${skip} Skill directory ${dim("(not found)")}`);
298
311
  }
299
312
  const codexConfig = codexConfigToml();
313
+ const codexHooksJson2 = codexHooksJson();
314
+ const codexHooksData = readJson(codexHooksJson2);
315
+ const codexPositions = codexHookPositions(codexHooksData ?? void 0);
300
316
  try {
301
317
  const config = readFileSync(codexConfig, "utf-8");
318
+ const parsed = parseTOML(config);
319
+ const trustRemoved = removeCodexHookTrust(parsed, codexHooksJson2, codexPositions);
302
320
  const hadPushary = config.includes("pushary");
303
- if (hadPushary) {
304
- const parsed = parseTOML(config);
321
+ if (hadPushary || trustRemoved) {
305
322
  const mcpServers = parsed.mcp_servers ?? {};
306
323
  delete mcpServers.pushary;
307
324
  if (Object.keys(mcpServers).length === 0) delete parsed.mcp_servers;
@@ -311,15 +328,14 @@ var main = async () => {
311
328
  if (parsed.notify.length === 0) delete parsed.notify;
312
329
  }
313
330
  guardedWrite(codexConfig, stringifyTOML(parsed));
314
- console.log(` ${check} Codex config ${dim(did("cleaned"))}`);
331
+ const what = trustRemoved ? "cleaned, hook trust removed" : "cleaned";
332
+ console.log(` ${check} Codex config ${dim(did(what))}`);
315
333
  } else {
316
334
  console.log(` ${skip} Codex config ${dim("(no pushary entries)")}`);
317
335
  }
318
336
  } catch {
319
337
  console.log(` ${skip} Codex config ${dim("(not found)")}`);
320
338
  }
321
- const codexHooksJson2 = codexHooksJson();
322
- const codexHooksData = readJson(codexHooksJson2);
323
339
  if (codexHooksData) {
324
340
  if (removeCodexHooks(codexHooksData)) {
325
341
  writeJson(codexHooksJson2, codexHooksData);
@@ -6,19 +6,20 @@ import {
6
6
  postLiveSession,
7
7
  postLiveSessionUntilAccepted,
8
8
  startLiveSessionHeartbeat
9
- } from "../chunk-WLLBFDVY.js";
10
- import "../chunk-ECEUMWBH.js";
9
+ } from "../chunk-TV3UFGT7.js";
10
+ import "../chunk-QLMX5LLV.js";
11
11
  import {
12
12
  startProcessOwnership,
13
13
  withSpawnProcessOwnership
14
- } from "../chunk-QBWYNVKY.js";
14
+ } from "../chunk-BFBAHOGR.js";
15
15
  import {
16
16
  spawnClaude
17
17
  } from "../chunk-XHKBHWLX.js";
18
18
  import {
19
19
  resolveBinary
20
20
  } from "../chunk-VT4IVERX.js";
21
- import "../chunk-B3V2H6SA.js";
21
+ import "../chunk-NAPRGHZ3.js";
22
+ import "../chunk-GIRT7677.js";
22
23
  import {
23
24
  getMachineId
24
25
  } from "../chunk-RN3NOEJF.js";
@@ -27,7 +28,7 @@ import {
27
28
  getApiKey,
28
29
  getBaseUrl
29
30
  } from "../chunk-2UMNXADU.js";
30
- import "../chunk-7YJS2VTG.js";
31
+ import "../chunk-DMW54VWK.js";
31
32
 
32
33
  // bin/pushary-codex-bridge.ts
33
34
  import { existsSync } from "fs";