@ghl-ai/aw 0.1.37-beta.66 → 0.1.37-beta.68

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.
@@ -31,7 +31,21 @@ const CLAUDE_WINDOWS_HOOK_COMMAND = '"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd"
31
31
  const REPO_CLAUDE_SESSION_START_COMMAND = '"$CLAUDE_PROJECT_DIR"/hooks/aw-session-start';
32
32
  const CODEX_HOOK_MATCHER = 'startup|resume';
33
33
  const CODEX_HOOK_COMMAND = 'bash "$HOME/.codex/hooks/aw-session-start.sh"';
34
+ const CODEX_PROMPT_HOOK_COMMAND = 'bash "$HOME/.codex/hooks/aw-user-prompt-submit.sh"';
35
+ const CODEX_PRE_TOOL_HOOK_COMMAND = 'bash "$HOME/.codex/hooks/aw-pre-tool-use.sh"';
36
+ const CODEX_POST_TOOL_HOOK_COMMAND = 'bash "$HOME/.codex/hooks/aw-post-tool-use.sh"';
37
+ const CODEX_STOP_HOOK_COMMAND = 'bash "$HOME/.codex/hooks/aw-stop.sh"';
34
38
  const CURSOR_HOOK_COMMAND = 'node .cursor/hooks/session-start.js';
39
+ const CURSOR_BEFORE_PROMPT_COMMAND = 'node .cursor/hooks/before-submit-prompt.js';
40
+
41
+ function hasManagedHookCommand(entry, command) {
42
+ return Array.isArray(entry?.hooks)
43
+ && entry.hooks.some(hook => hook?.type === 'command' && hook?.command === command);
44
+ }
45
+
46
+ function formatMissingParts(parts) {
47
+ return parts.length > 0 ? parts.join(', ') : 'none';
48
+ }
35
49
 
36
50
  function projectRelinkFix(homeDir, cwd, targetDescription) {
37
51
  return cwd !== homeDir
@@ -245,6 +259,110 @@ function hasManagedCursorSessionStart(filePath) {
245
259
  );
246
260
  }
247
261
 
262
+ function getClaudeHomeHookCoverage(homeDir) {
263
+ const hooksPath = join(homeDir, '.claude', 'hooks', 'hooks.json');
264
+ if (!existsSync(hooksPath)) {
265
+ return { ok: false, missingPhases: ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop'] };
266
+ }
267
+
268
+ const hooksConfig = readJson(hooksPath, {});
269
+ const sessionStartStatus = getClaudeHomeSessionStartStatus(homeDir);
270
+ const phases = {
271
+ SessionStart: sessionStartStatus.present && sessionStartStatus.ok && sessionStartStatus.reason !== 'missing',
272
+ UserPromptSubmit: Array.isArray(hooksConfig?.hooks?.UserPromptSubmit)
273
+ && hooksConfig.hooks.UserPromptSubmit.some(entry =>
274
+ Array.isArray(entry?.hooks)
275
+ && entry.hooks.some(hook => String(hook?.command || '').includes('session-start-rules-context.sh'))
276
+ ),
277
+ PreToolUse: Array.isArray(hooksConfig?.hooks?.PreToolUse) && hooksConfig.hooks.PreToolUse.length > 0,
278
+ PostToolUse: Array.isArray(hooksConfig?.hooks?.PostToolUse) && hooksConfig.hooks.PostToolUse.length > 0,
279
+ Stop: Array.isArray(hooksConfig?.hooks?.Stop) && hooksConfig.hooks.Stop.length > 0,
280
+ };
281
+
282
+ const missingPhases = Object.entries(phases)
283
+ .filter(([, present]) => !present)
284
+ .map(([phase]) => phase);
285
+
286
+ return { ok: missingPhases.length === 0, missingPhases };
287
+ }
288
+
289
+ function getCodexHomeHookCoverage(homeDir) {
290
+ const hooksPath = join(homeDir, '.codex', 'hooks.json');
291
+ const hooksConfig = readJson(hooksPath, {});
292
+ const phases = {
293
+ SessionStart: Array.isArray(hooksConfig?.hooks?.SessionStart)
294
+ && hooksConfig.hooks.SessionStart.some(entry =>
295
+ entry?.matcher === CODEX_HOOK_MATCHER
296
+ && hasManagedHookCommand(entry, CODEX_HOOK_COMMAND)
297
+ ),
298
+ UserPromptSubmit: Array.isArray(hooksConfig?.hooks?.UserPromptSubmit)
299
+ && hooksConfig.hooks.UserPromptSubmit.some(entry => hasManagedHookCommand(entry, CODEX_PROMPT_HOOK_COMMAND)),
300
+ PreToolUse: Array.isArray(hooksConfig?.hooks?.PreToolUse)
301
+ && hooksConfig.hooks.PreToolUse.some(entry => entry?.matcher === '*' && hasManagedHookCommand(entry, CODEX_PRE_TOOL_HOOK_COMMAND)),
302
+ PostToolUse: Array.isArray(hooksConfig?.hooks?.PostToolUse)
303
+ && hooksConfig.hooks.PostToolUse.some(entry => entry?.matcher === '*' && hasManagedHookCommand(entry, CODEX_POST_TOOL_HOOK_COMMAND)),
304
+ Stop: Array.isArray(hooksConfig?.hooks?.Stop)
305
+ && hooksConfig.hooks.Stop.some(entry => hasManagedHookCommand(entry, CODEX_STOP_HOOK_COMMAND)),
306
+ };
307
+
308
+ const scripts = {
309
+ SessionStart: existsSync(join(homeDir, '.codex', 'hooks', 'aw-session-start.sh')),
310
+ UserPromptSubmit: existsSync(join(homeDir, '.codex', 'hooks', 'aw-user-prompt-submit.sh')),
311
+ PreToolUse: existsSync(join(homeDir, '.codex', 'hooks', 'aw-pre-tool-use.sh')),
312
+ PostToolUse: existsSync(join(homeDir, '.codex', 'hooks', 'aw-post-tool-use.sh')),
313
+ Stop: existsSync(join(homeDir, '.codex', 'hooks', 'aw-stop.sh')),
314
+ };
315
+
316
+ const missingPhases = Object.entries(phases)
317
+ .filter(([, present]) => !present)
318
+ .map(([phase]) => phase);
319
+ const missingScripts = Object.entries(scripts)
320
+ .filter(([, present]) => !present)
321
+ .map(([phase]) => phase);
322
+
323
+ return { ok: missingPhases.length === 0 && missingScripts.length === 0, missingPhases, missingScripts };
324
+ }
325
+
326
+ function getCursorHomeHookCoverage(homeDir) {
327
+ const hooksPath = join(homeDir, '.cursor', 'hooks.json');
328
+ const hooksConfig = readJson(hooksPath, {});
329
+ const phases = {
330
+ SessionStart: Array.isArray(hooksConfig?.hooks?.sessionStart) && hooksConfig.hooks.sessionStart.length > 0,
331
+ UserPromptSubmit: Array.isArray(hooksConfig?.hooks?.beforeSubmitPrompt) && hooksConfig.hooks.beforeSubmitPrompt.length > 0,
332
+ PreToolUse: Array.isArray(hooksConfig?.hooks?.beforeShellExecution)
333
+ && hooksConfig.hooks.beforeShellExecution.length > 0
334
+ && Array.isArray(hooksConfig?.hooks?.beforeMCPExecution)
335
+ && hooksConfig.hooks.beforeMCPExecution.length > 0,
336
+ PostToolUse: Array.isArray(hooksConfig?.hooks?.afterShellExecution)
337
+ && hooksConfig.hooks.afterShellExecution.length > 0
338
+ && Array.isArray(hooksConfig?.hooks?.afterFileEdit)
339
+ && hooksConfig.hooks.afterFileEdit.length > 0
340
+ && Array.isArray(hooksConfig?.hooks?.afterMCPExecution)
341
+ && hooksConfig.hooks.afterMCPExecution.length > 0,
342
+ Stop: Array.isArray(hooksConfig?.hooks?.stop) && hooksConfig.hooks.stop.length > 0,
343
+ };
344
+
345
+ const scripts = {
346
+ SessionStart: existsSync(join(homeDir, '.cursor', 'hooks', 'session-start.js')),
347
+ UserPromptSubmit: existsSync(join(homeDir, '.cursor', 'hooks', 'before-submit-prompt.js')),
348
+ PreToolUse: existsSync(join(homeDir, '.cursor', 'hooks', 'before-shell-execution.js'))
349
+ && existsSync(join(homeDir, '.cursor', 'hooks', 'before-mcp-execution.js')),
350
+ PostToolUse: existsSync(join(homeDir, '.cursor', 'hooks', 'after-shell-execution.js'))
351
+ && existsSync(join(homeDir, '.cursor', 'hooks', 'after-file-edit.js'))
352
+ && existsSync(join(homeDir, '.cursor', 'hooks', 'after-mcp-execution.js')),
353
+ Stop: existsSync(join(homeDir, '.cursor', 'hooks', 'stop.js')),
354
+ };
355
+
356
+ const missingPhases = Object.entries(phases)
357
+ .filter(([, present]) => !present)
358
+ .map(([phase]) => phase);
359
+ const missingScripts = Object.entries(scripts)
360
+ .filter(([, present]) => !present)
361
+ .map(([phase]) => phase);
362
+
363
+ return { ok: missingPhases.length === 0 && missingScripts.length === 0, missingPhases, missingScripts };
364
+ }
365
+
248
366
  function getClaudePluginSessionStartStatus(pluginRoot) {
249
367
  const hooksConfig = readJson(join(pluginRoot, 'hooks', 'hooks.json'), {});
250
368
  if (!Array.isArray(hooksConfig?.hooks?.SessionStart)) {
@@ -556,6 +674,7 @@ function buildDoctorChecks(homeDir, cwd) {
556
674
 
557
675
  const claudeLegacyHooks = parseLegacyClaudeHookTargets(claudeSettings?.hooks?.SessionStart || []);
558
676
  const claudeHomeSessionStartStatus = getClaudeHomeSessionStartStatus(homeDir);
677
+ const claudeHomeHookCoverage = getClaudeHomeHookCoverage(homeDir);
559
678
  if (claudeLegacyHooks.length > 0) {
560
679
  const brokenTargets = claudeLegacyHooks.filter(target => !target.path || !existsSync(target.path));
561
680
  checks.push(makeCheck(
@@ -583,6 +702,18 @@ function buildDoctorChecks(homeDir, cwd) {
583
702
  checks.push(makeCheck('claude-session-start', 'Claude session-start hook', 'pass', 'No stale Claude SessionStart override detected'));
584
703
  }
585
704
 
705
+ checks.push(
706
+ claudeHomeHookCoverage.ok
707
+ ? makeCheck('claude-home-hooks', 'Claude home hook phases', 'pass', 'Claude home hooks cover SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, and Stop')
708
+ : makeCheck(
709
+ 'claude-home-hooks',
710
+ 'Claude home hook phases',
711
+ 'fail',
712
+ `Claude home hooks are missing core phases: ${formatMissingParts(claudeHomeHookCoverage.missingPhases)}`,
713
+ 'Run `aw init` to refresh ~/.claude/hooks/hooks.json with the full home-level AW hook phase set.',
714
+ ),
715
+ );
716
+
586
717
  const claudeInstallStatePath = join(homeDir, '.claude', 'ecc', 'install-state.json');
587
718
  checks.push(existsSync(claudeInstallStatePath)
588
719
  ? makeCheck('claude-install-state', 'Claude install state', 'pass', 'Claude install-state file is present')
@@ -675,14 +806,15 @@ function buildDoctorChecks(homeDir, cwd) {
675
806
  const codexConfigPath = join(homeDir, '.codex', 'config.toml');
676
807
  const codexHooksPath = join(homeDir, '.codex', 'hooks.json');
677
808
  const codexRuntimePath = join(homeDir, '.codex', 'hooks', 'aw-session-start.sh');
678
- const codexHealthy = startup.codexHooksEnabled && hasManagedCodexSessionStart(codexHooksPath) && startup.codexSessionStartScriptInstalled;
809
+ const codexHomeHookCoverage = getCodexHomeHookCoverage(homeDir);
810
+ const codexHealthy = startup.codexHooksEnabled && codexHomeHookCoverage.ok;
679
811
  checks.push(codexHealthy
680
- ? makeCheck('codex-routing', 'Codex routing', 'pass', 'Codex hooks, config, and runtime script are installed')
812
+ ? makeCheck('codex-routing', 'Codex routing', 'pass', 'Codex home hooks cover SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, and Stop')
681
813
  : makeCheck(
682
814
  'codex-routing',
683
815
  'Codex routing',
684
816
  startup.mode === 'disabled' ? 'warn' : 'fail',
685
- `Codex routing incomplete (codex_hooks=${parseCodexHooksFile(codexConfigPath)}, hooks.json=${existsSync(codexHooksPath)}, runtime=${existsSync(codexRuntimePath)})`,
817
+ `Codex routing incomplete (codex_hooks=${parseCodexHooksFile(codexConfigPath)}, missing phases=${formatMissingParts(codexHomeHookCoverage.missingPhases)}, missing scripts=${formatMissingParts(codexHomeHookCoverage.missingScripts)})`,
686
818
  'Run `aw routing enable` or `aw init` to restore Codex startup wiring.',
687
819
  ),
688
820
  );
@@ -766,14 +898,15 @@ function buildDoctorChecks(homeDir, cwd) {
766
898
 
767
899
  const cursorHooksPath = join(homeDir, '.cursor', 'hooks.json');
768
900
  const cursorRuntimePath = join(homeDir, '.cursor', 'hooks', 'session-start.js');
769
- const cursorHealthy = hasManagedCursorSessionStart(cursorHooksPath) && startup.cursorSessionStartScriptInstalled;
901
+ const cursorHomeHookCoverage = getCursorHomeHookCoverage(homeDir);
902
+ const cursorHealthy = cursorHomeHookCoverage.ok;
770
903
  checks.push(cursorHealthy
771
- ? makeCheck('cursor-routing', 'Cursor routing', 'pass', 'Cursor sessionStart hook and runtime script are installed')
904
+ ? makeCheck('cursor-routing', 'Cursor routing', 'pass', 'Cursor home hooks cover sessionStart, beforeSubmitPrompt, pre-tool, post-tool, and stop phases')
772
905
  : makeCheck(
773
906
  'cursor-routing',
774
907
  'Cursor routing',
775
908
  startup.mode === 'disabled' ? 'warn' : 'fail',
776
- `Cursor routing incomplete (hooks.json=${existsSync(cursorHooksPath)}, runtime=${existsSync(cursorRuntimePath)})`,
909
+ `Cursor routing incomplete (missing phases=${formatMissingParts(cursorHomeHookCoverage.missingPhases)}, missing scripts=${formatMissingParts(cursorHomeHookCoverage.missingScripts)})`,
777
910
  'Run `aw routing enable` or `aw init` to restore Cursor startup wiring.',
778
911
  ),
779
912
  );
package/commands/init.mjs CHANGED
@@ -277,6 +277,7 @@ export async function initCommand(args) {
277
277
  ensureAwRuntimeHook(HOME);
278
278
  syncHomeAndProjectInstructions(cwd, freshCfg?.namespace || team);
279
279
  await setupMcp(HOME, freshCfg?.namespace || team, { silent });
280
+ applyStoredStartupPreferences(HOME);
280
281
  const removedLegacyStartupFiles = cwd !== HOME ? removeWorkspaceHookDefaults(cwd) : [];
281
282
  installGlobalHooks();
282
283
 
package/integrate.mjs CHANGED
@@ -106,6 +106,8 @@ function shouldResetHomeInstructionFile(content, file) {
106
106
  : [
107
107
  '# AGENTS.md — ',
108
108
  '# ECC for Codex CLI',
109
+ '# AW SDLC Repo Instructions',
110
+ 'Use the repo-local AW SDLC files as the source of truth for routing and stage behavior.',
109
111
  '<!-- BEGIN ECC -->',
110
112
  ];
111
113
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.37-beta.66",
3
+ "version": "0.1.37-beta.68",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {
package/startup.mjs CHANGED
@@ -11,6 +11,14 @@ const CODEX_HOOK_MATCHER = 'startup|resume';
11
11
  const CODEX_HOOK_COMMAND = 'bash "$HOME/.codex/hooks/aw-session-start.sh"';
12
12
  const CODEX_HOOK_STATUS = 'Loading AW router';
13
13
  const CODEX_HOOK_MARKER = '# aw-managed: codex-global-session-start';
14
+ const CODEX_PROMPT_HOOK_COMMAND = 'bash "$HOME/.codex/hooks/aw-user-prompt-submit.sh"';
15
+ const CODEX_PRE_TOOL_HOOK_COMMAND = 'bash "$HOME/.codex/hooks/aw-pre-tool-use.sh"';
16
+ const CODEX_POST_TOOL_HOOK_COMMAND = 'bash "$HOME/.codex/hooks/aw-post-tool-use.sh"';
17
+ const CODEX_STOP_HOOK_COMMAND = 'bash "$HOME/.codex/hooks/aw-stop.sh"';
18
+ const CODEX_PROMPT_HOOK_MARKER = '# aw-managed: codex-global-user-prompt-submit';
19
+ const CODEX_PRE_TOOL_HOOK_MARKER = '# aw-managed: codex-global-pre-tool-use';
20
+ const CODEX_POST_TOOL_HOOK_MARKER = '# aw-managed: codex-global-post-tool-use';
21
+ const CODEX_STOP_HOOK_MARKER = '# aw-managed: codex-global-stop';
14
22
  const CURSOR_SESSION_START_COMMAND = 'node .cursor/hooks/session-start.js';
15
23
  const CURSOR_SESSION_START_DESCRIPTION = 'Load previous context and detect environment';
16
24
  const REPO_CURSOR_SESSION_START_COMMAND = 'bash "$(git rev-parse --show-toplevel)/hooks/aw-session-start"';
@@ -40,6 +48,32 @@ JSON_CONTEXT=$(printf '%s' "\$CONTEXT" | python3 -c 'import json, sys; print(jso
40
48
  echo "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"SessionStart\\",\\"additionalContext\\":\${JSON_CONTEXT}}}"
41
49
  `;
42
50
 
51
+ const CODEX_PROMPT_HOOK_SCRIPT = `#!/usr/bin/env bash
52
+ ${CODEX_PROMPT_HOOK_MARKER}
53
+ set -euo pipefail
54
+
55
+ TARGET="$HOME/.aw-ecc/scripts/hooks/session-start-rules-context.sh"
56
+ if [[ -f "$TARGET" ]]; then
57
+ exec bash "$TARGET"
58
+ fi
59
+
60
+ exit 0
61
+ `;
62
+
63
+ function buildCodexLifecycleNoopScript(marker, label) {
64
+ return `#!/usr/bin/env bash
65
+ ${marker}
66
+ set -euo pipefail
67
+
68
+ # Reserved AW ${label} phase for Codex home routing.
69
+ exit 0
70
+ `;
71
+ }
72
+
73
+ const CODEX_PRE_TOOL_HOOK_SCRIPT = buildCodexLifecycleNoopScript(CODEX_PRE_TOOL_HOOK_MARKER, 'PreToolUse');
74
+ const CODEX_POST_TOOL_HOOK_SCRIPT = buildCodexLifecycleNoopScript(CODEX_POST_TOOL_HOOK_MARKER, 'PostToolUse');
75
+ const CODEX_STOP_HOOK_SCRIPT = buildCodexLifecycleNoopScript(CODEX_STOP_HOOK_MARKER, 'Stop');
76
+
43
77
  function startupPrefsPath(homeDir = homedir()) {
44
78
  return join(homeDir, '.aw', STARTUP_PREFS_FILENAME);
45
79
  }
@@ -52,6 +86,22 @@ function codexHookScriptPath(homeDir = homedir()) {
52
86
  return join(homeDir, '.codex', 'hooks', 'aw-session-start.sh');
53
87
  }
54
88
 
89
+ function codexPromptHookScriptPath(homeDir = homedir()) {
90
+ return join(homeDir, '.codex', 'hooks', 'aw-user-prompt-submit.sh');
91
+ }
92
+
93
+ function codexPreToolHookScriptPath(homeDir = homedir()) {
94
+ return join(homeDir, '.codex', 'hooks', 'aw-pre-tool-use.sh');
95
+ }
96
+
97
+ function codexPostToolHookScriptPath(homeDir = homedir()) {
98
+ return join(homeDir, '.codex', 'hooks', 'aw-post-tool-use.sh');
99
+ }
100
+
101
+ function codexStopHookScriptPath(homeDir = homedir()) {
102
+ return join(homeDir, '.codex', 'hooks', 'aw-stop.sh');
103
+ }
104
+
55
105
  function resolveRegistryRoot(homeDir = homedir()) {
56
106
  return [
57
107
  join(homeDir, '.aw_registry'),
@@ -77,6 +127,27 @@ function writeJson(filePath, value) {
77
127
  writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
78
128
  }
79
129
 
130
+ function ensureManagedFile(filePath, content, updatedFiles) {
131
+ const current = existsSync(filePath) ? readFileSync(filePath, 'utf8') : '';
132
+ if (!existsSync(filePath) || current !== content) {
133
+ mkdirSync(dirname(filePath), { recursive: true });
134
+ writeFileSync(filePath, content);
135
+ updatedFiles.push(filePath);
136
+ }
137
+ }
138
+
139
+ function removeManagedFile(filePath, marker, updatedFiles) {
140
+ if (!existsSync(filePath)) return;
141
+ try {
142
+ const current = readFileSync(filePath, 'utf8');
143
+ if (!current.includes(marker)) return;
144
+ rmSync(filePath, { force: true });
145
+ updatedFiles.push(filePath);
146
+ } catch {
147
+ /* best effort */
148
+ }
149
+ }
150
+
80
151
  function isObject(value) {
81
152
  return value !== null && typeof value === 'object' && !Array.isArray(value);
82
153
  }
@@ -121,22 +192,43 @@ function isLegacyClaudeSessionStartEntry(entry) {
121
192
  );
122
193
  }
123
194
 
124
- function isManagedCodexSessionStartEntry(entry) {
125
- return entry?.matcher === CODEX_HOOK_MATCHER
126
- && Array.isArray(entry?.hooks)
195
+ function hasCommandHook(entry, command) {
196
+ return Array.isArray(entry?.hooks)
127
197
  && entry.hooks.some(hook =>
128
198
  hook?.type === 'command'
129
- && hook?.command === CODEX_HOOK_COMMAND
199
+ && hook?.command === command
130
200
  );
131
201
  }
132
202
 
203
+ function isManagedCodexSessionStartEntry(entry) {
204
+ return entry?.matcher === CODEX_HOOK_MATCHER && hasCommandHook(entry, CODEX_HOOK_COMMAND);
205
+ }
206
+
133
207
  function isLegacyCodexSessionStartEntry(entry) {
134
208
  return Array.isArray(entry?.hooks)
135
- && entry.hooks.some(hook =>
136
- hook?.type === 'command'
137
- && typeof hook?.command === 'string'
138
- && hook.command.includes('.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh')
139
- );
209
+ && entry.hooks.some(hook => {
210
+ const command = String(hook?.command || '');
211
+ return (
212
+ command.includes('.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh')
213
+ || (command.includes('.codex/hooks/aw-session-start.sh') && entry?.matcher !== CODEX_HOOK_MATCHER)
214
+ );
215
+ });
216
+ }
217
+
218
+ function isManagedCodexPromptSubmitEntry(entry) {
219
+ return hasCommandHook(entry, CODEX_PROMPT_HOOK_COMMAND);
220
+ }
221
+
222
+ function isManagedCodexPreToolUseEntry(entry) {
223
+ return entry?.matcher === '*' && hasCommandHook(entry, CODEX_PRE_TOOL_HOOK_COMMAND);
224
+ }
225
+
226
+ function isManagedCodexPostToolUseEntry(entry) {
227
+ return entry?.matcher === '*' && hasCommandHook(entry, CODEX_POST_TOOL_HOOK_COMMAND);
228
+ }
229
+
230
+ function isManagedCodexStopEntry(entry) {
231
+ return hasCommandHook(entry, CODEX_STOP_HOOK_COMMAND);
140
232
  }
141
233
 
142
234
  function disableClaudeStartup(homeDir = homedir()) {
@@ -270,13 +362,11 @@ function enableCodexStartup(homeDir = homedir()) {
270
362
  updatedFiles.push(configPath);
271
363
  }
272
364
 
273
- const hookScriptPath = codexHookScriptPath(homeDir);
274
- const currentHookScript = existsSync(hookScriptPath) ? readFileSync(hookScriptPath, 'utf8') : '';
275
- if (!existsSync(hookScriptPath) || currentHookScript !== CODEX_HOOK_SCRIPT) {
276
- mkdirSync(dirname(hookScriptPath), { recursive: true });
277
- writeFileSync(hookScriptPath, CODEX_HOOK_SCRIPT);
278
- updatedFiles.push(hookScriptPath);
279
- }
365
+ ensureManagedFile(codexHookScriptPath(homeDir), CODEX_HOOK_SCRIPT, updatedFiles);
366
+ ensureManagedFile(codexPromptHookScriptPath(homeDir), CODEX_PROMPT_HOOK_SCRIPT, updatedFiles);
367
+ ensureManagedFile(codexPreToolHookScriptPath(homeDir), CODEX_PRE_TOOL_HOOK_SCRIPT, updatedFiles);
368
+ ensureManagedFile(codexPostToolHookScriptPath(homeDir), CODEX_POST_TOOL_HOOK_SCRIPT, updatedFiles);
369
+ ensureManagedFile(codexStopHookScriptPath(homeDir), CODEX_STOP_HOOK_SCRIPT, updatedFiles);
280
370
 
281
371
  const hooksPath = join(homeDir, '.codex', 'hooks.json');
282
372
  const config = readJson(hooksPath, {});
@@ -285,24 +375,98 @@ function enableCodexStartup(homeDir = homedir()) {
285
375
  config.hooks = {};
286
376
  }
287
377
 
288
- const current = Array.isArray(config.hooks.SessionStart) ? config.hooks.SessionStart : [];
289
- const cleaned = current.filter(entry => !isLegacyCodexSessionStartEntry(entry) && !isManagedCodexSessionStartEntry(entry));
290
- const hasManagedEntry = current.some(isManagedCodexSessionStartEntry);
291
-
292
- if (!hasManagedEntry || cleaned.length !== current.length) {
293
- config.hooks.SessionStart = [
294
- ...cleaned,
295
- {
296
- matcher: CODEX_HOOK_MATCHER,
297
- hooks: [
298
- {
299
- type: 'command',
300
- command: CODEX_HOOK_COMMAND,
301
- statusMessage: CODEX_HOOK_STATUS,
302
- },
303
- ],
304
- },
305
- ];
378
+ let hooksChanged = false;
379
+
380
+ const currentSessionStart = Array.isArray(config.hooks.SessionStart) ? config.hooks.SessionStart : [];
381
+ const nextSessionStart = [
382
+ ...currentSessionStart.filter(entry => !isLegacyCodexSessionStartEntry(entry) && !isManagedCodexSessionStartEntry(entry)),
383
+ {
384
+ matcher: CODEX_HOOK_MATCHER,
385
+ hooks: [
386
+ {
387
+ type: 'command',
388
+ command: CODEX_HOOK_COMMAND,
389
+ statusMessage: CODEX_HOOK_STATUS,
390
+ },
391
+ ],
392
+ },
393
+ ];
394
+ if (JSON.stringify(nextSessionStart) !== JSON.stringify(currentSessionStart)) {
395
+ config.hooks.SessionStart = nextSessionStart;
396
+ hooksChanged = true;
397
+ }
398
+
399
+ const currentPromptSubmit = Array.isArray(config.hooks.UserPromptSubmit) ? config.hooks.UserPromptSubmit : [];
400
+ const nextPromptSubmit = [
401
+ ...currentPromptSubmit.filter(entry => !isManagedCodexPromptSubmitEntry(entry)),
402
+ {
403
+ hooks: [
404
+ {
405
+ type: 'command',
406
+ command: CODEX_PROMPT_HOOK_COMMAND,
407
+ },
408
+ ],
409
+ },
410
+ ];
411
+ if (JSON.stringify(nextPromptSubmit) !== JSON.stringify(currentPromptSubmit)) {
412
+ config.hooks.UserPromptSubmit = nextPromptSubmit;
413
+ hooksChanged = true;
414
+ }
415
+
416
+ const currentPreToolUse = Array.isArray(config.hooks.PreToolUse) ? config.hooks.PreToolUse : [];
417
+ const nextPreToolUse = [
418
+ ...currentPreToolUse.filter(entry => !isManagedCodexPreToolUseEntry(entry)),
419
+ {
420
+ matcher: '*',
421
+ hooks: [
422
+ {
423
+ type: 'command',
424
+ command: CODEX_PRE_TOOL_HOOK_COMMAND,
425
+ },
426
+ ],
427
+ },
428
+ ];
429
+ if (JSON.stringify(nextPreToolUse) !== JSON.stringify(currentPreToolUse)) {
430
+ config.hooks.PreToolUse = nextPreToolUse;
431
+ hooksChanged = true;
432
+ }
433
+
434
+ const currentPostToolUse = Array.isArray(config.hooks.PostToolUse) ? config.hooks.PostToolUse : [];
435
+ const nextPostToolUse = [
436
+ ...currentPostToolUse.filter(entry => !isManagedCodexPostToolUseEntry(entry)),
437
+ {
438
+ matcher: '*',
439
+ hooks: [
440
+ {
441
+ type: 'command',
442
+ command: CODEX_POST_TOOL_HOOK_COMMAND,
443
+ },
444
+ ],
445
+ },
446
+ ];
447
+ if (JSON.stringify(nextPostToolUse) !== JSON.stringify(currentPostToolUse)) {
448
+ config.hooks.PostToolUse = nextPostToolUse;
449
+ hooksChanged = true;
450
+ }
451
+
452
+ const currentStop = Array.isArray(config.hooks.Stop) ? config.hooks.Stop : [];
453
+ const nextStop = [
454
+ ...currentStop.filter(entry => !isManagedCodexStopEntry(entry)),
455
+ {
456
+ hooks: [
457
+ {
458
+ type: 'command',
459
+ command: CODEX_STOP_HOOK_COMMAND,
460
+ },
461
+ ],
462
+ },
463
+ ];
464
+ if (JSON.stringify(nextStop) !== JSON.stringify(currentStop)) {
465
+ config.hooks.Stop = nextStop;
466
+ hooksChanged = true;
467
+ }
468
+
469
+ if (hooksChanged) {
306
470
  writeJson(hooksPath, config);
307
471
  updatedFiles.push(hooksPath);
308
472
  }
@@ -312,38 +476,84 @@ function enableCodexStartup(homeDir = homedir()) {
312
476
 
313
477
  function disableCodexStartup(homeDir = homedir()) {
314
478
  const updatedFiles = [];
315
- const hookScriptPath = codexHookScriptPath(homeDir);
316
- if (existsSync(hookScriptPath)) {
317
- try {
318
- const currentHookScript = readFileSync(hookScriptPath, 'utf8');
319
- if (currentHookScript.includes(CODEX_HOOK_MARKER)) {
320
- rmSync(hookScriptPath, { force: true });
321
- updatedFiles.push(hookScriptPath);
322
- }
323
- } catch {
324
- /* best effort */
325
- }
326
- }
479
+ removeManagedFile(codexHookScriptPath(homeDir), CODEX_HOOK_MARKER, updatedFiles);
480
+ removeManagedFile(codexPromptHookScriptPath(homeDir), CODEX_PROMPT_HOOK_MARKER, updatedFiles);
481
+ removeManagedFile(codexPreToolHookScriptPath(homeDir), CODEX_PRE_TOOL_HOOK_MARKER, updatedFiles);
482
+ removeManagedFile(codexPostToolHookScriptPath(homeDir), CODEX_POST_TOOL_HOOK_MARKER, updatedFiles);
483
+ removeManagedFile(codexStopHookScriptPath(homeDir), CODEX_STOP_HOOK_MARKER, updatedFiles);
327
484
 
328
485
  const hooksPath = join(homeDir, '.codex', 'hooks.json');
329
486
  if (!existsSync(hooksPath)) return updatedFiles;
330
487
 
331
488
  const config = readJson(hooksPath, {});
332
- if (!isObject(config.hooks) || !Array.isArray(config.hooks.SessionStart)) {
489
+ if (!isObject(config.hooks)) {
333
490
  return updatedFiles;
334
491
  }
335
492
 
336
- const filtered = config.hooks.SessionStart.filter(entry => !isManagedCodexSessionStartEntry(entry) && !isLegacyCodexSessionStartEntry(entry));
337
- if (filtered.length === config.hooks.SessionStart.length) {
338
- return updatedFiles;
493
+ let hooksChanged = false;
494
+
495
+ if (Array.isArray(config.hooks.SessionStart)) {
496
+ const filtered = config.hooks.SessionStart.filter(entry => !isManagedCodexSessionStartEntry(entry) && !isLegacyCodexSessionStartEntry(entry));
497
+ if (filtered.length !== config.hooks.SessionStart.length) {
498
+ hooksChanged = true;
499
+ if (filtered.length > 0) {
500
+ config.hooks.SessionStart = filtered;
501
+ } else {
502
+ delete config.hooks.SessionStart;
503
+ }
504
+ }
339
505
  }
340
506
 
341
- if (filtered.length > 0) {
342
- config.hooks.SessionStart = filtered;
343
- } else {
344
- delete config.hooks.SessionStart;
507
+ if (Array.isArray(config.hooks.UserPromptSubmit)) {
508
+ const filtered = config.hooks.UserPromptSubmit.filter(entry => !isManagedCodexPromptSubmitEntry(entry));
509
+ if (filtered.length !== config.hooks.UserPromptSubmit.length) {
510
+ hooksChanged = true;
511
+ if (filtered.length > 0) {
512
+ config.hooks.UserPromptSubmit = filtered;
513
+ } else {
514
+ delete config.hooks.UserPromptSubmit;
515
+ }
516
+ }
345
517
  }
346
518
 
519
+ if (Array.isArray(config.hooks.PreToolUse)) {
520
+ const filtered = config.hooks.PreToolUse.filter(entry => !isManagedCodexPreToolUseEntry(entry));
521
+ if (filtered.length !== config.hooks.PreToolUse.length) {
522
+ hooksChanged = true;
523
+ if (filtered.length > 0) {
524
+ config.hooks.PreToolUse = filtered;
525
+ } else {
526
+ delete config.hooks.PreToolUse;
527
+ }
528
+ }
529
+ }
530
+
531
+ if (Array.isArray(config.hooks.PostToolUse)) {
532
+ const filtered = config.hooks.PostToolUse.filter(entry => !isManagedCodexPostToolUseEntry(entry));
533
+ if (filtered.length !== config.hooks.PostToolUse.length) {
534
+ hooksChanged = true;
535
+ if (filtered.length > 0) {
536
+ config.hooks.PostToolUse = filtered;
537
+ } else {
538
+ delete config.hooks.PostToolUse;
539
+ }
540
+ }
541
+ }
542
+
543
+ if (Array.isArray(config.hooks.Stop)) {
544
+ const filtered = config.hooks.Stop.filter(entry => !isManagedCodexStopEntry(entry));
545
+ if (filtered.length !== config.hooks.Stop.length) {
546
+ hooksChanged = true;
547
+ if (filtered.length > 0) {
548
+ config.hooks.Stop = filtered;
549
+ } else {
550
+ delete config.hooks.Stop;
551
+ }
552
+ }
553
+ }
554
+
555
+ if (!hooksChanged) return updatedFiles;
556
+
347
557
  if (isEmptyObject(config.hooks)) {
348
558
  delete config.hooks;
349
559
  }