From 173d3c0ff1527e6e408f0bea22a69ec82274cf26 Mon Sep 17 00:00:00 2001 From: chhhee10 Date: Sat, 4 Jul 2026 08:48:42 +0530 Subject: [PATCH 1/3] fix(codex): remove top-level version field from codex hooks config Codex CLI strictly expects only 'hooks' at the top level and fails to parse 'version'. This removes the version injection for Codex configuration and handles existing config files with a version field. --- __tests__/hooks/integrations.test.ts | 10 +++++----- src/hooks/integrations.ts | 9 +++++---- src/hooks/types.ts | 4 ++-- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/__tests__/hooks/integrations.test.ts b/__tests__/hooks/integrations.test.ts index 4bf98d37..f05f1e8a 100644 --- a/__tests__/hooks/integrations.test.ts +++ b/__tests__/hooks/integrations.test.ts @@ -220,16 +220,16 @@ describe("OpenAI Codex integration", () => { // Snake-case keys must NOT be present (Codex stores under Pascal) expect(hooks[snake]).toBeUndefined(); } - // Settings file carries version: 1 - expect(settings.version).toBe(1); + // Settings file does NOT carry version (Codex strictly expects only `hooks`) + expect(settings.version).toBeUndefined(); }); - it("readSettings backfills version: 1 on existing files without it", () => { + it("readSettings removes version on existing files if present", () => { const settingsPath = resolve(tempDir, ".codex", "hooks.json"); mkdirSync(resolve(tempDir, ".codex"), { recursive: true }); - writeFileSync(settingsPath, JSON.stringify({ hooks: {} })); + writeFileSync(settingsPath, JSON.stringify({ version: 1, hooks: {} })); const read = codex.readSettings(settingsPath); - expect(read.version).toBe(1); + expect(read.version).toBeUndefined(); }); it("re-running writeHookEntries is idempotent", () => { diff --git a/src/hooks/integrations.ts b/src/hooks/integrations.ts index 7a9a9817..75a3fea4 100644 --- a/src/hooks/integrations.ts +++ b/src/hooks/integrations.ts @@ -232,10 +232,9 @@ export const claudeCode: Integration = { // • Settings paths: ~/.codex/hooks.json (user) and /.codex/hooks.json (project) // • Stdin event names arrive snake_case (pre_tool_use); we canonicalize to PascalCase before policy lookup // • No "local" scope -// • Settings file carries a top-level "version": 1 marker +// • Settings file does NOT carry a top-level "version" marker (Codex strictly expects only `hooks`) interface CodexSettingsFile { - version?: number; hooks?: Record; [key: string]: unknown; } @@ -262,7 +261,8 @@ export const codex: Integration = { readSettings(settingsPath) { const raw = readJsonFile(settingsPath); - if (raw.version === undefined) raw.version = 1; + // Remove version if it was injected by an older version of failproofai + if ("version" in raw) delete raw.version; return raw; }, @@ -290,7 +290,8 @@ export const codex: Integration = { writeHookEntries(settings, binaryPath, scope) { const s = settings as CodexSettingsFile; - if (s.version === undefined) s.version = 1; + // Remove version if it was injected by an older version of failproofai + if ("version" in s) delete s.version; if (!s.hooks) s.hooks = {}; for (const eventType of CODEX_HOOK_EVENT_TYPES) { diff --git a/src/hooks/types.ts b/src/hooks/types.ts index cc71009b..f9483b7e 100644 --- a/src/hooks/types.ts +++ b/src/hooks/types.ts @@ -79,7 +79,7 @@ export const CODEX_TOOL_MAP: Record = { // Settings paths: // user → ~/.copilot/hooks/failproofai.json // project → /.github/hooks/failproofai.json (also where the cloud agent reads) -// Settings file carries `version: 1` like Codex's hooks.json. +// Settings file carries `version: 1`. export const COPILOT_HOOK_SCOPES = ["user", "project"] as const; export type CopilotHookScope = (typeof COPILOT_HOOK_SCOPES)[number]; @@ -159,7 +159,7 @@ export const COPILOT_TOOL_MAP: Record = { // Settings paths: // user → ~/.cursor/hooks.json // project → /.cursor/hooks.json -// Settings file carries `version: 1` like Codex/Copilot. +// Settings file carries `version: 1` like Copilot. export const CURSOR_HOOK_SCOPES = ["user", "project"] as const; export type CursorHookScope = (typeof CURSOR_HOOK_SCOPES)[number]; From baef4cdaeef23d522459a0b665551ee77ba26baf Mon Sep 17 00:00:00 2001 From: chhhee10 Date: Thu, 9 Jul 2026 11:13:58 +0530 Subject: [PATCH 2/3] fix(codex): address nitpick to extract legacy version stripping logic --- .codex/hooks.json | 1 - .../e2e/hooks/codex-integration.e2e.test.ts | 2 +- __tests__/hooks/integrations.test.ts | 11 ++++++-- src/hooks/integrations.ts | 26 +++++++++++++------ 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/.codex/hooks.json b/.codex/hooks.json index 8314f336..fd7655fa 100644 --- a/.codex/hooks.json +++ b/.codex/hooks.json @@ -1,5 +1,4 @@ { - "version": 1, "hooks": { "SessionStart": [ { diff --git a/__tests__/e2e/hooks/codex-integration.e2e.test.ts b/__tests__/e2e/hooks/codex-integration.e2e.test.ts index 85204d90..f3b573dd 100644 --- a/__tests__/e2e/hooks/codex-integration.e2e.test.ts +++ b/__tests__/e2e/hooks/codex-integration.e2e.test.ts @@ -177,7 +177,7 @@ describe("E2E: Codex integration — install/uninstall", () => { const hooksPath = resolve(env.cwd, ".codex", "hooks.json"); expect(existsSync(hooksPath)).toBe(true); const settings = JSON.parse(readFileSync(hooksPath, "utf-8")) as Record; - expect(settings.version).toBe(1); + expect(settings.version).toBeUndefined(); const hooks = settings.hooks as Record; // Codex stores under PascalCase keys expect(hooks.PreToolUse).toBeDefined(); diff --git a/__tests__/hooks/integrations.test.ts b/__tests__/hooks/integrations.test.ts index f05f1e8a..0c3799ee 100644 --- a/__tests__/hooks/integrations.test.ts +++ b/__tests__/hooks/integrations.test.ts @@ -224,11 +224,18 @@ describe("OpenAI Codex integration", () => { expect(settings.version).toBeUndefined(); }); - it("readSettings removes version on existing files if present", () => { + it("writeHookEntries removes version on existing files if present", () => { + const settings: Record = { version: 1, hooks: {} }; + codex.writeHookEntries(settings, "/usr/bin/failproofai", "user"); + expect(settings.version).toBeUndefined(); + }); + + it("removeHooksFromFile removes version on existing files even if no failproofai hooks are present", () => { const settingsPath = resolve(tempDir, ".codex", "hooks.json"); mkdirSync(resolve(tempDir, ".codex"), { recursive: true }); writeFileSync(settingsPath, JSON.stringify({ version: 1, hooks: {} })); - const read = codex.readSettings(settingsPath); + codex.removeHooksFromFile(settingsPath); + const read = JSON.parse(readFileSync(settingsPath, "utf-8")); expect(read.version).toBeUndefined(); }); diff --git a/src/hooks/integrations.ts b/src/hooks/integrations.ts index 75a3fea4..59aa8235 100644 --- a/src/hooks/integrations.ts +++ b/src/hooks/integrations.ts @@ -59,6 +59,14 @@ function isMarkedHook(hook: unknown): boolean { return cmd.includes("failproofai") && cmd.includes("--hook"); } +function stripLegacyVersion(settings: Record): boolean { + if ("version" in settings) { + delete settings.version; + return true; + } + return false; +} + function binaryExists(name: string): boolean { try { const cmd = process.platform === "win32" ? `where ${name}` : `which ${name}`; @@ -260,10 +268,7 @@ export const codex: Integration = { }, readSettings(settingsPath) { - const raw = readJsonFile(settingsPath); - // Remove version if it was injected by an older version of failproofai - if ("version" in raw) delete raw.version; - return raw; + return readJsonFile(settingsPath); }, writeSettings(settingsPath, settings) { @@ -290,8 +295,7 @@ export const codex: Integration = { writeHookEntries(settings, binaryPath, scope) { const s = settings as CodexSettingsFile; - // Remove version if it was injected by an older version of failproofai - if ("version" in s) delete s.version; + stripLegacyVersion(s as Record); if (!s.hooks) s.hooks = {}; for (const eventType of CODEX_HOOK_EVENT_TYPES) { @@ -316,7 +320,11 @@ export const codex: Integration = { removeHooksFromFile(settingsPath) { const settings = this.readSettings(settingsPath) as CodexSettingsFile; - if (!settings.hooks) return 0; + const hadVersion = stripLegacyVersion(settings as Record); + if (!settings.hooks) { + if (hadVersion) this.writeSettings(settingsPath, settings as Record); + return 0; + } let removed = 0; for (const eventType of Object.keys(settings.hooks)) { @@ -334,7 +342,9 @@ export const codex: Integration = { } if (Object.keys(settings.hooks).length === 0) delete settings.hooks; - this.writeSettings(settingsPath, settings as Record); + if (removed > 0 || hadVersion) { + this.writeSettings(settingsPath, settings as Record); + } return removed; }, From b2d2d3e7b5bb85cc32fa707e2b78825096766494 Mon Sep 17 00:00:00 2001 From: chhhee10 Date: Thu, 9 Jul 2026 12:09:44 +0530 Subject: [PATCH 3/3] fix(codex): correct hook timeout unit (60000ms -> 60s) and add changelog Codex reads the hooks `timeout` field in SECONDS (its `timeout_sec` field, default 600), not milliseconds like Claude/Cursor/Gemini. The old 60000 meant ~16.7h, not 60s. Fix `codex.buildHookEntry` and regenerate the dogfood `.codex/hooks.json` to `timeout: 60`, add a unit assertion, and add the 0.0.12-beta.0 CHANGELOG entry covering both the version-field drop and this timeout correction. Co-Authored-By: Claude Opus 4.8 (1M context) --- .codex/hooks.json | 12 ++++++------ CHANGELOG.md | 5 +++++ __tests__/hooks/integrations.test.ts | 6 ++++++ src/hooks/integrations.ts | 4 +++- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/.codex/hooks.json b/.codex/hooks.json index fd7655fa..20bc2fc8 100644 --- a/.codex/hooks.json +++ b/.codex/hooks.json @@ -6,7 +6,7 @@ { "type": "command", "command": "bun bin/failproofai.mjs --hook SessionStart --cli codex", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -18,7 +18,7 @@ { "type": "command", "command": "bun bin/failproofai.mjs --hook PreToolUse --cli codex", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -30,7 +30,7 @@ { "type": "command", "command": "bun bin/failproofai.mjs --hook PermissionRequest --cli codex", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -42,7 +42,7 @@ { "type": "command", "command": "bun bin/failproofai.mjs --hook PostToolUse --cli codex", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -54,7 +54,7 @@ { "type": "command", "command": "bun bin/failproofai.mjs --hook UserPromptSubmit --cli codex", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -66,7 +66,7 @@ { "type": "command", "command": "bun bin/failproofai.mjs --hook Stop --cli codex", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] diff --git a/CHANGELOG.md b/CHANGELOG.md index b12cba8c..65155732 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.0.12-beta.0 — 2026-07-09 + +### Fixes +- Codex hooks: drop the invalid top-level `version` field from `.codex/hooks.json` (Codex CLI v0.142+ rejects it with `unknown field 'version'`, refusing to start any session), and strip any leftover `version` on the next install/uninstall so previously-broken configs self-heal. Also correct the Codex `timeout` unit from `60000` to `60` — Codex reads `timeout` in seconds (its `timeout_sec` field), so the old value meant ~16.7h instead of 60s. Copilot and Cursor legitimately carry `version: 1` in their own schemas and are untouched. + ## 0.0.11 — 2026-06-26 ### Breaking diff --git a/__tests__/hooks/integrations.test.ts b/__tests__/hooks/integrations.test.ts index 0c3799ee..b955ace8 100644 --- a/__tests__/hooks/integrations.test.ts +++ b/__tests__/hooks/integrations.test.ts @@ -204,6 +204,12 @@ describe("OpenAI Codex integration", () => { expect(entry[FAILPROOFAI_HOOK_MARKER]).toBe(true); }); + it("buildHookEntry sets timeout in SECONDS (60), not milliseconds", () => { + // Codex reads `timeout` as seconds (its `timeout_sec` field); 60000 would be ~16.7h. + const entry = codex.buildHookEntry("/usr/bin/failproofai", "pre_tool_use", "user"); + expect(entry.timeout).toBe(60); + }); + it("project scope uses npx -y failproofai", () => { const entry = codex.buildHookEntry("/usr/bin/failproofai", "pre_tool_use", "project"); expect(entry.command).toBe("npx -y failproofai --hook pre_tool_use --cli codex"); diff --git a/src/hooks/integrations.ts b/src/hooks/integrations.ts index 59aa8235..99638229 100644 --- a/src/hooks/integrations.ts +++ b/src/hooks/integrations.ts @@ -285,8 +285,10 @@ export const codex: Integration = { : `"${binaryPath}" --hook ${eventType} --cli codex`; return { type: "command", + // Codex reads `timeout` in SECONDS (its `timeout_sec` field, default 600) — + // NOT milliseconds like Claude/Cursor/Gemini. 60 = 60s, not 60000ms (~16.7h). command, - timeout: 60_000, + timeout: 60, [FAILPROOFAI_HOOK_MARKER]: true, }; },