@pushary/agent-hooks 0.12.0 → 0.14.0

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.
@@ -2,10 +2,11 @@
2
2
  import {
3
3
  removeClaudeMcpServers,
4
4
  removePusharySettings
5
- } from "../chunk-5GFUI5N6.js";
5
+ } from "../chunk-5MA3CPZB.js";
6
6
  import {
7
- execNpm
8
- } from "../chunk-RSHN2AQ7.js";
7
+ execNpm,
8
+ removeCodexHooks
9
+ } from "../chunk-2HMNOZPY.js";
9
10
 
10
11
  // bin/pushary-clean.ts
11
12
  import { existsSync, readFileSync, writeFileSync, rmSync } from "fs";
@@ -121,6 +122,18 @@ var main = async () => {
121
122
  } catch {
122
123
  console.log(` ${skip} Codex config ${dim("(not found)")}`);
123
124
  }
125
+ const codexHooksJson = join(homedir(), ".codex", "hooks.json");
126
+ const codexHooksData = readJson(codexHooksJson);
127
+ if (codexHooksData) {
128
+ if (removeCodexHooks(codexHooksData)) {
129
+ writeJson(codexHooksJson, codexHooksData);
130
+ console.log(` ${check} Codex hooks ${dim("(removed from ~/.codex/hooks.json)")}`);
131
+ } else {
132
+ console.log(` ${skip} Codex hooks ${dim("(no pushary entries)")}`);
133
+ }
134
+ } else {
135
+ console.log(` ${skip} Codex hooks ${dim("(not found)")}`);
136
+ }
124
137
  for (const shellFile of SHELL_FILES) {
125
138
  try {
126
139
  const content = readFileSync(shellFile, "utf-8");
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ handlePostToolUse,
4
+ handleStop,
5
+ handleUserPrompt,
6
+ reportEvent
7
+ } from "../chunk-SH26ZOHU.js";
8
+ import {
9
+ DEFAULT_SESSION,
10
+ askUser,
11
+ deriveToolTarget,
12
+ describeToolCall,
13
+ fetchModeState,
14
+ getMachineId,
15
+ getPolicy,
16
+ resolvePolicy,
17
+ savePendingQuestion,
18
+ sendNotification,
19
+ waitForAnswer
20
+ } from "../chunk-QRXWPZKN.js";
21
+ import "../chunk-22CV7V7A.js";
22
+ import "../chunk-3MIR7ODJ.js";
23
+ import {
24
+ getApiKey
25
+ } from "../chunk-VUNL35KE.js";
26
+
27
+ // bin/pushary-codex-hook.ts
28
+ import { basename, join } from "path";
29
+ import { tmpdir } from "os";
30
+ import { existsSync, mkdirSync, statSync, unlinkSync, writeFileSync } from "fs";
31
+
32
+ // src/codex-adapter.ts
33
+ var CODEX_AGENT = { type: "codex", label: "Codex" };
34
+ var codexAllow = () => ({ kind: "allow" });
35
+ var codexDeny = (reason) => ({ kind: "deny", reason });
36
+ var codexPass = () => ({ kind: "pass" });
37
+ var toCodexWire = (event, decision) => {
38
+ if (event === "PermissionRequest") {
39
+ if (decision.kind === "allow") {
40
+ return {
41
+ hookSpecificOutput: {
42
+ hookEventName: "PermissionRequest",
43
+ decision: { behavior: "allow" }
44
+ }
45
+ };
46
+ }
47
+ if (decision.kind === "deny") {
48
+ return {
49
+ hookSpecificOutput: {
50
+ hookEventName: "PermissionRequest",
51
+ decision: { behavior: "deny", message: decision.reason }
52
+ }
53
+ };
54
+ }
55
+ return null;
56
+ }
57
+ if (event === "PreToolUse" && decision.kind === "deny") {
58
+ return {
59
+ hookSpecificOutput: {
60
+ hookEventName: "PreToolUse",
61
+ permissionDecision: "deny",
62
+ permissionDecisionReason: decision.reason
63
+ }
64
+ };
65
+ }
66
+ return null;
67
+ };
68
+ var toPolicyLookup = (toolName, toolInput) => {
69
+ if (toolName !== "apply_patch") return { tool: toolName, input: toolInput };
70
+ const command = toolInput.command;
71
+ return {
72
+ tool: "Edit",
73
+ input: typeof command === "string" ? { file_path: command } : {}
74
+ };
75
+ };
76
+ var permissionTimeoutDecision = (timeoutAction) => {
77
+ if (timeoutAction === "approve") return codexAllow();
78
+ if (timeoutAction === "deny") return codexDeny("No response within timeout");
79
+ return codexPass();
80
+ };
81
+ var preToolUseTimeoutDecision = (timeoutAction, denyReason = "No response within timeout") => timeoutAction === "deny" ? codexDeny(denyReason) : codexPass();
82
+
83
+ // bin/pushary-codex-hook.ts
84
+ var KILL_REASON = "Stopped by user: this agent was halted from Pushary";
85
+ var MAX_WAIT_SECONDS = 170;
86
+ var APPROVAL_DIR = join(tmpdir(), "pushary-codex-approvals");
87
+ var APPROVAL_TTL_MS = 10 * 60 * 1e3;
88
+ var sanitizeId = (value) => value.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 128);
89
+ var markApproved = (toolUseId) => {
90
+ if (!toolUseId) return;
91
+ try {
92
+ if (!existsSync(APPROVAL_DIR)) mkdirSync(APPROVAL_DIR, { recursive: true });
93
+ writeFileSync(join(APPROVAL_DIR, sanitizeId(toolUseId)), "", "utf-8");
94
+ } catch {
95
+ }
96
+ };
97
+ var consumeApproval = (toolUseId) => {
98
+ if (!toolUseId) return false;
99
+ const path = join(APPROVAL_DIR, sanitizeId(toolUseId));
100
+ try {
101
+ const fresh = Date.now() - statSync(path).mtimeMs < APPROVAL_TTL_MS;
102
+ unlinkSync(path);
103
+ return fresh;
104
+ } catch {
105
+ return false;
106
+ }
107
+ };
108
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
109
+ var pollForAnswer = async (apiKey, correlationId, deadlineMs, pollInterval = 2e3) => {
110
+ while (Date.now() < deadlineMs) {
111
+ const remaining = Math.min(Math.max(deadlineMs - Date.now(), 1e3), 3e4);
112
+ let answer;
113
+ try {
114
+ answer = await waitForAnswer(apiKey, correlationId, remaining);
115
+ } catch {
116
+ if (Date.now() + pollInterval >= deadlineMs) break;
117
+ await sleep(pollInterval);
118
+ continue;
119
+ }
120
+ if (answer.answered) return answer;
121
+ if (Date.now() + pollInterval >= deadlineMs) break;
122
+ await sleep(pollInterval);
123
+ }
124
+ return { answered: false };
125
+ };
126
+ var agentNameFor = (input) => `Codex - ${basename(input.cwd ?? process.cwd())}`;
127
+ var pushQuestion = async (apiKey, input, waitSeconds) => {
128
+ const toolName = input.tool_name ?? "";
129
+ const toolInput = input.tool_input ?? {};
130
+ const projectName = basename(input.cwd ?? process.cwd());
131
+ const description = describeToolCall(toolName, toolInput, "hook");
132
+ const result = await askUser(apiKey, {
133
+ question: `Allow ${description}?`,
134
+ type: "confirm",
135
+ context: `Agent wants to run this in ${projectName}`,
136
+ agentName: agentNameFor(input),
137
+ sessionId: input.session_id,
138
+ machineId: getMachineId(),
139
+ toolName,
140
+ toolTarget: deriveToolTarget(toolName, toolInput)
141
+ });
142
+ const deadline = Date.now() + Math.min(waitSeconds, MAX_WAIT_SECONDS) * 1e3;
143
+ const answer = await pollForAnswer(apiKey, result.correlationId, deadline);
144
+ return { answer, correlationId: result.correlationId };
145
+ };
146
+ var notifyApprovalNeeded = async (apiKey, input) => {
147
+ try {
148
+ await sendNotification(apiKey, {
149
+ title: "Agent needs approval",
150
+ body: describeToolCall(input.tool_name ?? "", input.tool_input ?? {}, "hook"),
151
+ agentName: agentNameFor(input),
152
+ sessionId: input.session_id,
153
+ machineId: getMachineId()
154
+ });
155
+ } catch {
156
+ }
157
+ };
158
+ var decidePermissionRequest = async (input) => {
159
+ try {
160
+ const apiKey = getApiKey();
161
+ const modeState = await fetchModeState(apiKey, input.session_id);
162
+ if (modeState.kill) return codexDeny(KILL_REASON);
163
+ const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {});
164
+ const policy = await getPolicy(apiKey);
165
+ const toolPolicy = resolvePolicy(policy, lookup.tool, modeState.mode, lookup.input);
166
+ if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") return codexAllow();
167
+ if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
168
+ return codexDeny(`Denied by policy for ${toolPolicy.tool}`);
169
+ }
170
+ if (toolPolicy.mode === "terminal_only") return codexPass();
171
+ if (toolPolicy.mode === "notify_only") {
172
+ await notifyApprovalNeeded(apiKey, input);
173
+ return codexPass();
174
+ }
175
+ const waitSeconds = toolPolicy.mode === "push_first" ? toolPolicy.pushFirstSeconds : toolPolicy.timeoutSeconds;
176
+ const { answer, correlationId } = await pushQuestion(apiKey, input, waitSeconds);
177
+ if (answer.answered) {
178
+ if (answer.value === "yes") {
179
+ if (toolPolicy.mode === "push_only") markApproved(input.tool_use_id);
180
+ return codexAllow();
181
+ }
182
+ return codexDeny("Denied via push notification");
183
+ }
184
+ savePendingQuestion(input.session_id || DEFAULT_SESSION, correlationId);
185
+ if (toolPolicy.mode === "push_first") return codexPass();
186
+ const decision = permissionTimeoutDecision(toolPolicy.timeoutAction);
187
+ if (decision.kind === "allow") markApproved(input.tool_use_id);
188
+ return decision;
189
+ } catch {
190
+ return codexPass();
191
+ }
192
+ };
193
+ var decidePreToolUse = async (input) => {
194
+ try {
195
+ const apiKey = getApiKey();
196
+ const modeState = await fetchModeState(apiKey, input.session_id);
197
+ if (modeState.kill) return codexDeny(KILL_REASON);
198
+ const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {});
199
+ const policy = await getPolicy(apiKey);
200
+ const toolPolicy = resolvePolicy(policy, lookup.tool, modeState.mode, lookup.input);
201
+ if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") return codexPass();
202
+ if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
203
+ return codexDeny(`Denied by policy for ${toolPolicy.tool}`);
204
+ }
205
+ if (toolPolicy.mode === "push_only") {
206
+ if (consumeApproval(input.tool_use_id)) return codexPass();
207
+ let pushed;
208
+ try {
209
+ pushed = await pushQuestion(apiKey, input, toolPolicy.timeoutSeconds);
210
+ } catch {
211
+ return preToolUseTimeoutDecision(toolPolicy.timeoutAction, "Push notification failed, denying per policy");
212
+ }
213
+ const { answer, correlationId } = pushed;
214
+ if (answer.answered) {
215
+ return answer.value === "yes" ? codexPass() : codexDeny("Denied via push notification");
216
+ }
217
+ savePendingQuestion(input.session_id || DEFAULT_SESSION, correlationId);
218
+ return preToolUseTimeoutDecision(toolPolicy.timeoutAction);
219
+ }
220
+ if (toolPolicy.mode === "notify_only") {
221
+ await notifyApprovalNeeded(apiKey, input);
222
+ return codexPass();
223
+ }
224
+ return codexPass();
225
+ } catch {
226
+ return codexPass();
227
+ }
228
+ };
229
+ var reportSessionStart = async (input) => {
230
+ try {
231
+ await reportEvent({
232
+ event: "session_begin",
233
+ agentType: CODEX_AGENT.type,
234
+ agentName: agentNameFor(input),
235
+ action: "Session started",
236
+ sessionId: input.session_id
237
+ }, { maxAttempts: 1, timeoutMs: 5e3 });
238
+ } catch {
239
+ }
240
+ };
241
+ var asToolResult = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
242
+ var emit = (wire) => {
243
+ if (wire) process.stdout.write(JSON.stringify(wire));
244
+ };
245
+ var main = async () => {
246
+ let rawInput = "";
247
+ for await (const chunk of process.stdin) {
248
+ rawInput += chunk;
249
+ }
250
+ if (!rawInput.trim()) {
251
+ process.exit(0);
252
+ }
253
+ let input;
254
+ try {
255
+ input = JSON.parse(rawInput);
256
+ } catch {
257
+ process.exit(0);
258
+ }
259
+ try {
260
+ switch (input.hook_event_name) {
261
+ case "PermissionRequest":
262
+ emit(toCodexWire("PermissionRequest", await decidePermissionRequest(input)));
263
+ break;
264
+ case "PreToolUse":
265
+ emit(toCodexWire("PreToolUse", await decidePreToolUse(input)));
266
+ break;
267
+ case "PostToolUse":
268
+ await handlePostToolUse({
269
+ tool_name: input.tool_name ?? "",
270
+ tool_input: input.tool_input ?? {},
271
+ tool_result: asToolResult(input.tool_response),
272
+ cwd: input.cwd,
273
+ session_id: input.session_id
274
+ }, CODEX_AGENT);
275
+ break;
276
+ case "UserPromptSubmit":
277
+ await handleUserPrompt({
278
+ prompt: input.prompt,
279
+ cwd: input.cwd,
280
+ session_id: input.session_id
281
+ }, CODEX_AGENT);
282
+ break;
283
+ case "Stop":
284
+ await handleStop({
285
+ cwd: input.cwd,
286
+ session_id: input.session_id
287
+ }, CODEX_AGENT);
288
+ break;
289
+ case "SessionStart":
290
+ await reportSessionStart(input);
291
+ break;
292
+ default:
293
+ break;
294
+ }
295
+ } catch {
296
+ }
297
+ };
298
+ main();
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  reportEvent
4
- } from "../chunk-AB4KX4XT.js";
4
+ } from "../chunk-SH26ZOHU.js";
5
5
  import {
6
6
  askUser,
7
7
  getMachineId,
8
8
  waitForAnswer
9
- } from "../chunk-OF5WIOYS.js";
9
+ } from "../chunk-QRXWPZKN.js";
10
+ import "../chunk-22CV7V7A.js";
10
11
  import "../chunk-3MIR7ODJ.js";
11
12
  import {
12
13
  getApiKey
@@ -14,18 +15,24 @@ import {
14
15
 
15
16
  // bin/pushary-codex.ts
16
17
  import { basename } from "path";
18
+ var DEPRECATION_NOTICE = "[pushary-codex] Deprecated: this is the legacy Codex notify handler. Native Codex hooks via pushary-codex-hook replace it. Run npx @pushary/agent-hooks setup to migrate.\n";
17
19
  var readStdin = async () => {
18
20
  let raw = "";
19
21
  for await (const chunk of process.stdin) raw += chunk;
20
22
  return raw;
21
23
  };
22
24
  var main = async () => {
25
+ if (process.argv.includes("--help") || process.argv.includes("-h")) {
26
+ process.stderr.write(DEPRECATION_NOTICE);
27
+ process.exit(0);
28
+ }
23
29
  const argvPayload = process.argv.slice(2).find((a) => a.trim().startsWith("{"));
24
30
  let rawInput = argvPayload ?? "";
25
- if (!rawInput.trim()) {
31
+ if (!rawInput.trim() && !process.stdin.isTTY) {
26
32
  rawInput = await readStdin();
27
33
  }
28
34
  if (!rawInput.trim()) {
35
+ if (process.stdin.isTTY) process.stderr.write(DEPRECATION_NOTICE);
29
36
  process.exit(0);
30
37
  }
31
38
  let event;
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- execNpm
4
- } from "../chunk-RSHN2AQ7.js";
3
+ execNpm,
4
+ hasCodexHooks,
5
+ missingCodexHookEvents
6
+ } from "../chunk-2HMNOZPY.js";
5
7
  import {
6
8
  callMcpTool,
7
9
  sendMcpRequest
@@ -14,6 +16,7 @@ import { join } from "path";
14
16
  import { homedir } from "os";
15
17
  import { execSync } from "child_process";
16
18
  import { confirm } from "@inquirer/prompts";
19
+ import { parse as parseTOML } from "smol-toml";
17
20
  var dim = (s) => `\x1B[2m${s}\x1B[0m`;
18
21
  var bold = (s) => `\x1B[1m${s}\x1B[0m`;
19
22
  var green = (s) => `\x1B[32m${s}\x1B[0m`;
@@ -103,9 +106,11 @@ var main = async () => {
103
106
  const hasPreHook = JSON.stringify(hooks?.PreToolUse ?? []).includes("pushary-hook");
104
107
  const hasPostHook = JSON.stringify(hooks?.PostToolUse ?? []).includes("pushary-post-hook");
105
108
  const hasStopHook = JSON.stringify(hooks?.Stop ?? []).includes("pushary-stop-hook");
109
+ const hasPromptHook = JSON.stringify(hooks?.UserPromptSubmit ?? []).includes("pushary-prompt-hook");
106
110
  check(hasPreHook, "Claude Code: PreToolUse hook");
107
111
  check(hasPostHook, "Claude Code: PostToolUse hook");
108
112
  check(hasStopHook, "Claude Code: Stop hook");
113
+ check(hasPromptHook, "Claude Code: UserPromptSubmit hook", hasPromptHook ? void 0 : "missing, re-run setup to register it");
109
114
  const preHookCommand = extractHookCommand(hooks?.PreToolUse, "pushary-hook");
110
115
  if (preHookCommand) {
111
116
  const resolves = commandResolves(preHookCommand);
@@ -122,8 +127,10 @@ var main = async () => {
122
127
  check(false, "Claude Code: settings.json", "not found");
123
128
  }
124
129
  const codexConfigPath = join(homedir(), ".codex", "config.toml");
125
- if (existsSync(codexConfigPath)) {
126
- const codexConfig = readFileSync(codexConfigPath, "utf-8");
130
+ const codexHooksPath = join(homedir(), ".codex", "hooks.json");
131
+ const codexHooksJson = readJson(codexHooksPath);
132
+ if (existsSync(codexConfigPath) || codexHooksJson) {
133
+ const codexConfig = existsSync(codexConfigPath) ? readFileSync(codexConfigPath, "utf-8") : "";
127
134
  const hasPusharyMcp = codexConfig.includes("[mcp_servers.pushary]");
128
135
  check(hasPusharyMcp, "Codex: MCP server configured");
129
136
  if (hasPusharyMcp) {
@@ -135,10 +142,33 @@ var main = async () => {
135
142
  }
136
143
  }
137
144
  const codexNotifyPath = codexConfig.match(/["']([^"']*pushary-codex[^"']*)["']/)?.[1] ?? null;
138
- check(!!codexNotifyPath, "Codex: notify handler configured");
139
- if (codexNotifyPath) {
140
- const resolves = commandResolves(codexNotifyPath);
141
- check(resolves, "Codex: notify handler resolves", resolves ? codexNotifyPath : `not found \u2014 ${codexNotifyPath}`);
145
+ const hooksInstalled = !!codexHooksJson && hasCodexHooks(codexHooksJson);
146
+ if (hooksInstalled) {
147
+ const missingEvents = missingCodexHookEvents(codexHooksJson);
148
+ check(missingEvents.length === 0, "Codex: native hooks installed", missingEvents.length === 0 ? "all 6 events" : `missing ${missingEvents.join(", ")}, re-run setup`);
149
+ const hookCommand = extractHookCommand(codexHooksJson.hooks?.PreToolUse, "pushary-codex-hook") ?? extractHookCommand(codexHooksJson.hooks?.PermissionRequest, "pushary-codex-hook");
150
+ if (hookCommand) {
151
+ const resolves = commandResolves(hookCommand);
152
+ check(resolves, "Codex: hook command resolves", resolves ? hookCommand : `not on PATH: ${hookCommand}`);
153
+ }
154
+ let hooksFeatureDisabled = false;
155
+ try {
156
+ const parsed = parseTOML(codexConfig);
157
+ const features = parsed.features;
158
+ hooksFeatureDisabled = !!features && typeof features === "object" && features.hooks === false;
159
+ } catch {
160
+ }
161
+ check(!hooksFeatureDisabled, "Codex: hooks feature enabled", hooksFeatureDisabled ? "[features].hooks = false in config.toml" : void 0);
162
+ if (codexNotifyPath) {
163
+ console.log(` ${warn} Codex: stale legacy notify entry ${dim("(double-push risk, re-run setup to remove it)")}`);
164
+ }
165
+ console.log(` ${warn} Codex: hooks must be trusted inside Codex ${dim("(run /hooks in Codex, cannot be verified from here)")}`);
166
+ } else {
167
+ check(!!codexNotifyPath, "Codex: notify handler configured (deprecated)", codexNotifyPath ? "upgrade Codex and re-run setup for native hooks" : "missing, re-run setup");
168
+ if (codexNotifyPath) {
169
+ const resolves = commandResolves(codexNotifyPath);
170
+ check(resolves, "Codex: notify handler resolves", resolves ? codexNotifyPath : `not found: ${codexNotifyPath}`);
171
+ }
142
172
  }
143
173
  const codexSkillPath = join(homedir(), ".codex", "skills", "pushary", "SKILL.md");
144
174
  check(existsSync(codexSkillPath), "Codex: skill installed");
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePreToolUse
4
- } from "../chunk-W5KRWUNE.js";
5
- import "../chunk-IBWCHA5M.js";
6
- import "../chunk-OF5WIOYS.js";
4
+ } from "../chunk-TRLBBLSS.js";
5
+ import "../chunk-QRXWPZKN.js";
6
+ import "../chunk-22CV7V7A.js";
7
7
  import "../chunk-3MIR7ODJ.js";
8
8
  import "../chunk-VUNL35KE.js";
9
9
 
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePostToolUse
4
- } from "../chunk-AB4KX4XT.js";
5
- import "../chunk-OF5WIOYS.js";
4
+ } from "../chunk-SH26ZOHU.js";
5
+ import "../chunk-QRXWPZKN.js";
6
+ import "../chunk-22CV7V7A.js";
6
7
  import "../chunk-3MIR7ODJ.js";
7
8
  import "../chunk-VUNL35KE.js";
8
9
 
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ handleUserPrompt
4
+ } from "../chunk-SH26ZOHU.js";
5
+ import "../chunk-QRXWPZKN.js";
6
+ import "../chunk-22CV7V7A.js";
7
+ import "../chunk-3MIR7ODJ.js";
8
+ import "../chunk-VUNL35KE.js";
9
+
10
+ // bin/pushary-prompt-hook.ts
11
+ var main = async () => {
12
+ let rawInput = "";
13
+ for await (const chunk of process.stdin) {
14
+ rawInput += chunk;
15
+ }
16
+ if (!rawInput.trim()) {
17
+ process.exit(0);
18
+ }
19
+ try {
20
+ const input = JSON.parse(rawInput);
21
+ await handleUserPrompt(input);
22
+ } catch {
23
+ }
24
+ };
25
+ main();
@@ -3,14 +3,15 @@ import {
3
3
  addClaudeMcpServer,
4
4
  addPusharyHooks,
5
5
  addPusharyToolPermissions
6
- } from "../chunk-5GFUI5N6.js";
6
+ } from "../chunk-5MA3CPZB.js";
7
7
  import {
8
+ addCodexHooks,
8
9
  execNpm,
9
10
  npmErrorMessage
10
- } from "../chunk-RSHN2AQ7.js";
11
+ } from "../chunk-2HMNOZPY.js";
11
12
  import {
12
13
  isValidApiKey
13
- } from "../chunk-IBWCHA5M.js";
14
+ } from "../chunk-22CV7V7A.js";
14
15
 
15
16
  // bin/pushary-setup.ts
16
17
  import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, cpSync, rmSync } from "fs";
@@ -310,7 +311,7 @@ var setupClaudeCode = async (apiKey) => {
310
311
  addPusharyToolPermissions(settings);
311
312
  });
312
313
  await installGlobally();
313
- await spinner("Adding hooks (PreToolUse, PostToolUse, Stop)", async () => {
314
+ await spinner("Adding hooks (PreToolUse, PostToolUse, UserPromptSubmit, Stop)", async () => {
314
315
  let binDir;
315
316
  try {
316
317
  binDir = join(execNpm("prefix -g --no-workspaces", { timeout: 5e3 }).toString().trim(), "bin");
@@ -396,6 +397,63 @@ var setupHermes = async (_apiKey) => {
396
397
  console.log(` ${dim2("\u2022")} Permission gating: set ${bold2("PUSHARY_GATE_TOOLS")} to require lock-screen approval for risky tools`);
397
398
  console.log(` ${dim2("To re-enable terminal prompts:")} remove ${bold2("clarify")} from ${dim2("agent.disabled_toolsets")} in ~/.hermes/config.yaml`);
398
399
  };
400
+ var CODEX_HOOKS_JSON = join(homedir(), ".codex", "hooks.json");
401
+ var CODEX_HOOKS_MIN_VERSION = [0, 122, 0];
402
+ var parseCodexVersion = (raw) => {
403
+ const match = raw.match(/(\d+)\.(\d+)\.(\d+)/);
404
+ if (!match) return null;
405
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
406
+ };
407
+ var codexSupportsHooks = () => {
408
+ try {
409
+ const raw = execSync("codex --version", { encoding: "utf-8", stdio: "pipe", timeout: 1e4 });
410
+ const version = parseCodexVersion(raw);
411
+ if (!version) return false;
412
+ for (let i = 0; i < 3; i++) {
413
+ if (version[i] > CODEX_HOOKS_MIN_VERSION[i]) return true;
414
+ if (version[i] < CODEX_HOOKS_MIN_VERSION[i]) return false;
415
+ }
416
+ return true;
417
+ } catch {
418
+ return false;
419
+ }
420
+ };
421
+ var removeCodexNotifyEntry = (codexConfig) => {
422
+ let raw = "";
423
+ try {
424
+ raw = readFileSync(codexConfig, "utf-8");
425
+ } catch {
426
+ return;
427
+ }
428
+ if (!raw.includes("pushary-codex")) return;
429
+ const config = parseTOML(raw);
430
+ if (!Array.isArray(config.notify)) return;
431
+ const filtered = config.notify.filter((entry) => typeof entry !== "string" || !entry.includes("pushary-codex"));
432
+ if (filtered.length === config.notify.length) return;
433
+ if (filtered.length === 0) {
434
+ delete config.notify;
435
+ } else {
436
+ config.notify = filtered;
437
+ }
438
+ writeFileSync(codexConfig, stringifyTOML(config), "utf-8");
439
+ };
440
+ var addCodexNotifyEntry = (codexConfig) => {
441
+ const globalPrefix = execNpm("prefix -g --no-workspaces", { timeout: 5e3 }).toString().trim();
442
+ const pusharyCodexPath = join(globalPrefix, "bin", "pushary-codex");
443
+ if (!existsSync(pusharyCodexPath)) throw new Error("pushary-codex not found at " + pusharyCodexPath);
444
+ let raw = "";
445
+ try {
446
+ raw = readFileSync(codexConfig, "utf-8");
447
+ } catch {
448
+ }
449
+ const config = raw ? parseTOML(raw) : {};
450
+ const notify = Array.isArray(config.notify) ? config.notify : [];
451
+ if (!notify.some((n) => typeof n === "string" && n.includes("pushary-codex"))) {
452
+ notify.push(pusharyCodexPath);
453
+ config.notify = notify;
454
+ writeFileSync(codexConfig, stringifyTOML(config), "utf-8");
455
+ }
456
+ };
399
457
  var setupCodex = async (_apiKey) => {
400
458
  console.log(`
401
459
  ${bold2("Setting up Codex")}
@@ -431,29 +489,42 @@ var setupCodex = async (_apiKey) => {
431
489
  config.mcp_servers = mcpServers;
432
490
  writeFileSync(codexConfig, stringifyTOML(config), "utf-8");
433
491
  });
434
- await spinner("Adding notify handler for Codex events", async () => {
435
- const globalPrefix = execNpm("prefix -g --no-workspaces", { timeout: 5e3 }).toString().trim();
436
- const pusharyCodexPath = join(globalPrefix, "bin", "pushary-codex");
437
- if (!existsSync(pusharyCodexPath)) throw new Error("pushary-codex not found at " + pusharyCodexPath);
438
- let raw = "";
439
- try {
440
- raw = readFileSync(codexConfig, "utf-8");
441
- } catch {
442
- }
443
- const config = raw ? parseTOML(raw) : {};
444
- const notify = Array.isArray(config.notify) ? config.notify : [];
445
- if (!notify.some((n) => typeof n === "string" && n.includes("pushary-codex"))) {
446
- notify.push(pusharyCodexPath);
447
- config.notify = notify;
448
- writeFileSync(codexConfig, stringifyTOML(config), "utf-8");
449
- }
450
- });
492
+ const hooksSupported = codexSupportsHooks();
493
+ if (hooksSupported) {
494
+ await spinner("Adding native hooks (~/.codex/hooks.json)", async () => {
495
+ const globalPrefix = execNpm("prefix -g --no-workspaces", { timeout: 5e3 }).toString().trim();
496
+ const hookCommand = join(globalPrefix, "bin", "pushary-codex-hook");
497
+ if (!existsSync(hookCommand)) throw new Error("pushary-codex-hook not found at " + hookCommand);
498
+ const hooksConfig = readJson(CODEX_HOOKS_JSON);
499
+ addCodexHooks(hooksConfig, hookCommand);
500
+ writeJson(CODEX_HOOKS_JSON, hooksConfig);
501
+ });
502
+ await spinner("Removing legacy notify handler", async () => {
503
+ removeCodexNotifyEntry(codexConfig);
504
+ });
505
+ } else {
506
+ console.log(` ${yellow2("!")} This Codex version predates native hooks (needs ${CODEX_HOOKS_MIN_VERSION.join(".")}+).`);
507
+ console.log(` ${dim2("Installing the deprecated notify handler instead. Upgrade Codex and re-run setup")}`);
508
+ console.log(` ${dim2("to get policy enforcement, phone approvals, and session tracking.")}`);
509
+ await spinner("Adding notify handler for Codex events (deprecated)", async () => {
510
+ addCodexNotifyEntry(codexConfig);
511
+ });
512
+ }
451
513
  await installSkillToDir(CODEX_SKILL_DIR, "Installing Pushary skill");
452
514
  console.log();
453
515
  console.log(` ${dim2("What this configured:")}`);
454
516
  console.log(` ${dim2("\u2022")} MCP server: Codex can send notifications and ask questions`);
455
517
  console.log(` ${dim2("\u2022")} Auto-allowed tools: no permission prompts for Pushary MCP calls`);
456
- console.log(` ${dim2("\u2022")} Notify handler: captures turn completions and approval requests`);
518
+ if (hooksSupported) {
519
+ console.log(` ${dim2("\u2022")} Native hooks: phone approvals, policy enforcement, kill switch, session tracking`);
520
+ console.log();
521
+ console.log(` ${bold2("Trust step (required):")}`);
522
+ console.log(` ${dim2("1.")} Open Codex and run ${cyan2("/hooks")}`);
523
+ console.log(` ${dim2("2.")} Review the Pushary hooks and trust them`);
524
+ console.log(` ${dim2("Hooks stay inactive until you trust them inside Codex.")}`);
525
+ } else {
526
+ console.log(` ${dim2("\u2022")} Notify handler (deprecated): captures turn completions and approval requests`);
527
+ }
457
528
  };
458
529
  var resolveBundledPlugin = () => {
459
530
  const dir = dirname(fileURLToPath(import.meta.url));
@@ -568,7 +639,7 @@ var main = async () => {
568
639
  message: "Which agents do you use? " + dim2(hint),
569
640
  choices: [
570
641
  { name: `Claude Code ${dim2("MCP + hooks + auto-allowed tools")}`, value: "claude_code", checked: detected.claude_code },
571
- { name: `Codex ${dim2("MCP + notify handler + auto-allowed tools")}`, value: "codex", checked: detected.codex },
642
+ { name: `Codex ${dim2("MCP + native hooks + auto-allowed tools")}`, value: "codex", checked: detected.codex },
572
643
  { name: `Hermes ${dim2("native plugin + auto-error notifications")}`, value: "hermes", checked: detected.hermes },
573
644
  { name: `Cursor ${dim2("MCP server")}`, value: "cursor", checked: detected.cursor }
574
645
  ]