@pushary/agent-hooks 0.87.2 → 0.87.3

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 +85 -28
  3. package/data/cursor-plugin/scripts/pushary-gate.test.mjs +42 -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-7YJS2VTG.js → chunk-DMW54VWK.js} +12 -1
  42. package/dist/{chunk-LT2VLPUK.js → chunk-DPUOW6NS.js} +18 -2
  43. package/dist/{chunk-3LKYBFWA.js → chunk-EJL3PJU7.js} +1 -1
  44. package/dist/chunk-GIRT7677.js +11 -0
  45. package/dist/{chunk-HAUTLCMA.js → chunk-ICERLAE2.js} +1 -1
  46. package/dist/{chunk-ZZE6FN7T.js → chunk-K42RXJPG.js} +52 -1
  47. package/dist/{chunk-Y3UTTFTZ.js → chunk-KBS53WOE.js} +1 -1
  48. package/dist/{chunk-SJVLFIYP.js → chunk-KCAFCGGY.js} +1 -1
  49. package/dist/{chunk-A4I5JGCW.js → chunk-KFAH6KRE.js} +1 -1
  50. package/dist/{chunk-QBWYNVKY.js → chunk-LGVHYFIB.js} +1 -1
  51. package/dist/{chunk-HJ44HIUW.js → chunk-OMA2OZRE.js} +11 -9
  52. package/dist/{chunk-ECEUMWBH.js → chunk-QLMX5LLV.js} +1 -1
  53. package/dist/{chunk-XW5CHAQX.js → chunk-TAYHEJFF.js} +2 -2
  54. package/dist/{chunk-JAICP7BA.js → chunk-TD3M232T.js} +1 -1
  55. package/dist/{chunk-WLLBFDVY.js → chunk-TV3UFGT7.js} +1 -1
  56. package/dist/{chunk-7MBDFNUC.js → chunk-VIHRP25S.js} +1 -1
  57. package/dist/{chunk-B3V2H6SA.js → chunk-VL5ZNK6V.js} +43 -12
  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,68 @@ 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
+ const params = typeof input.tool_input === 'string'
407
+ ? input.tool_input
408
+ : input.tool_input === undefined ? '' : JSON.stringify(input.tool_input)
409
+ return {
410
+ toolName,
411
+ toolInputs: [{ tool: toolName, params }],
412
+ display: params ? `${toolName}\n${params}` : toolName,
413
+ toolTarget: toolName.slice(0, 120),
414
+ prompt: 'Allow this tool call?',
415
+ verb: 'call this tool',
416
+ denied: denialFor('tool call'),
417
+ }
418
+ }
419
+
420
+ // Shell is also the fallback for a payload with no event name: a `command` is
421
+ // what `beforeShellExecution` has always been recognised by, and an older
422
+ // Cursor that omits `hook_event_name` must keep working exactly as before.
423
+ const command = typeof input.command === 'string' ? input.command.trim() : ''
424
+ if (!command || (event && event !== 'beforeShellExecution')) return null
425
+ return {
426
+ toolName: 'Bash',
427
+ toolInputs: [{ command }],
428
+ display: command,
429
+ toolTarget: commandHead(command),
430
+ prompt: 'Allow this command?',
431
+ verb: 'run this',
432
+ denied: denialFor('command'),
433
+ }
434
+ }
378
435
 
379
436
  // push_only: wait up to the policy timeout, then apply the timeout action.
380
- const handlePushOnly = async (apiKey, command, project, ident, timeoutSeconds, timeoutAction) => {
437
+ const handlePushOnly = async (apiKey, request, project, ident, timeoutSeconds, timeoutAction) => {
381
438
  let asked
382
439
  try {
383
- asked = await withRetry(() => callTool(apiKey, 'ask_user', askArgs(command, project, ident)), 3)
440
+ asked = await withRetry(() => callTool(apiKey, 'ask_user', askArgs(request, project, ident)), 3)
384
441
  } catch {
385
442
  return fromTimeoutAction(timeoutAction, 'Push notification failed; denied per your Pushary policy.')
386
443
  }
@@ -389,7 +446,7 @@ const handlePushOnly = async (apiKey, command, project, ident, timeoutSeconds, t
389
446
  const realMs = Math.max(timeoutSeconds, 1) * 1000
390
447
  const cap = Math.min(realMs, MAX_BLOCK_MS)
391
448
  const answer = await pollForAnswer(apiKey, asked.correlationId, Date.now() + cap)
392
- if (answer.answered) return answer.value === 'yes' ? ALLOW : deny(DENIED)
449
+ if (answer.answered) return answer.value === 'yes' ? ALLOW : deny(request.denied)
393
450
 
394
451
  // If Cursor's hook limit cut us off before the configured timeout, hand off to
395
452
  // Cursor's own prompt rather than misapplying the policy's timeout action.
@@ -398,10 +455,10 @@ const handlePushOnly = async (apiKey, command, project, ident, timeoutSeconds, t
398
455
  }
399
456
 
400
457
  // 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) => {
458
+ const handlePushFirst = async (apiKey, request, project, ident, pushFirstSeconds) => {
402
459
  let asked
403
460
  try {
404
- asked = await withRetry(() => callTool(apiKey, 'ask_user', askArgs(command, project, ident)), 3)
461
+ asked = await withRetry(() => callTool(apiKey, 'ask_user', askArgs(request, project, ident)), 3)
405
462
  } catch {
406
463
  return ask()
407
464
  }
@@ -409,16 +466,16 @@ const handlePushFirst = async (apiKey, command, project, ident, pushFirstSeconds
409
466
 
410
467
  const cap = Math.min(Math.max(pushFirstSeconds, 1) * 1000, MAX_BLOCK_MS)
411
468
  const answer = await pollForAnswer(apiKey, asked.correlationId, Date.now() + cap)
412
- if (answer.answered) return answer.value === 'yes' ? ALLOW : deny(DENIED)
469
+ if (answer.answered) return answer.value === 'yes' ? ALLOW : deny(request.denied)
413
470
  return ask('Sent to your phone via Pushary — you can also approve here.')
414
471
  }
415
472
 
416
473
  // notify_only: fire an awareness notification, let Cursor's prompt decide.
417
- const handleNotifyOnly = async (apiKey, command, project, ident) => {
474
+ const handleNotifyOnly = async (apiKey, request, project, ident) => {
418
475
  try {
419
476
  await callTool(apiKey, 'send_notification', {
420
477
  title: 'Agent needs approval',
421
- body: redactSecrets(command).slice(0, 180),
478
+ body: redactSecrets(request.display).slice(0, 180),
422
479
  agentName: ident.agentName,
423
480
  sessionId: ident.sessionId,
424
481
  machineId: ident.machineId,
@@ -436,8 +493,8 @@ const main = async () => {
436
493
  return respond(ask())
437
494
  }
438
495
 
439
- const command = typeof input.command === 'string' ? input.command.trim() : ''
440
- if (!command) return respond(ask())
496
+ const request = describeRequest(input)
497
+ if (!request) return respond(ask())
441
498
 
442
499
  const apiKey = resolveApiKey()
443
500
  if (!apiKey) {
@@ -451,7 +508,7 @@ const main = async () => {
451
508
  const ident = { agentName: `Cursor - ${project}`, sessionId, machineId: getMachineId() }
452
509
 
453
510
  try {
454
- const verdict = await decide(apiKey, command, input.cwd, sessionId)
511
+ const verdict = await decide(apiKey, request, input.cwd, sessionId)
455
512
 
456
513
  // No verdict, or one that says nothing: Cursor's own prompt decides, exactly
457
514
  // as if this gate were not installed. Never a forced denial on an outage.
@@ -467,12 +524,12 @@ const main = async () => {
467
524
  case 'terminal_only':
468
525
  return respond(ask())
469
526
  case 'notify_only':
470
- return respond(await handleNotifyOnly(apiKey, command, project, ident))
527
+ return respond(await handleNotifyOnly(apiKey, request, project, ident))
471
528
  case 'push_only':
472
- return respond(await handlePushOnly(apiKey, command, project, ident, tool.timeoutSeconds, tool.timeoutAction))
529
+ return respond(await handlePushOnly(apiKey, request, project, ident, tool.timeoutSeconds, tool.timeoutAction))
473
530
  case 'push_first':
474
531
  default:
475
- return respond(await handlePushFirst(apiKey, command, project, ident, tool.pushFirstSeconds))
532
+ return respond(await handlePushFirst(apiKey, request, project, ident, tool.pushFirstSeconds))
476
533
  }
477
534
  } catch (error) {
478
535
  process.stderr.write(`[pushary-gate] ${error?.message ?? error}\n`)
@@ -482,7 +539,7 @@ const main = async () => {
482
539
 
483
540
  // Exported for scripts/pushary-gate.test.mjs. main() only runs when the gate is
484
541
  // executed directly, so importing this file for a test does not read stdin.
485
- export { normalizeRepoRemote, deriveRepoKey }
542
+ export { normalizeRepoRemote, deriveRepoKey, describeRequest }
486
543
 
487
544
  const isDirectRun = process.argv[1] && import.meta.url === `file://${process.argv[1]}`
488
545
  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,44 @@ 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
+ it('declines an event it does not gate rather than calling it Bash', () => {
68
+ // beforeReadFile is registered by nobody here on purpose: its response
69
+ // schema has no `ask`, so the no-opinion path this gate takes on every error
70
+ // has nothing to return. If one ever arrives, it must not be mistaken for a
71
+ // shell command carrying no command.
72
+ assert.equal(describeRequest({ hook_event_name: 'beforeReadFile', file_path: '/etc/passwd' }), null)
73
+ assert.equal(describeRequest({ hook_event_name: 'beforeMCPExecution' }), null)
74
+ assert.equal(describeRequest({}), null)
75
+ })
76
+ })
@@ -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-VL5ZNK6V.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-KCAFCGGY.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-LGVHYFIB.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-VL5ZNK6V.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-DPUOW6NS.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-VL5ZNK6V.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-LGVHYFIB.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-VL5ZNK6V.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";