Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions .codex/hooks.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
{
"version": 1,
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "bun bin/failproofai.mjs --hook SessionStart --cli codex",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -19,7 +18,7 @@
{
"type": "command",
"command": "bun bin/failproofai.mjs --hook PreToolUse --cli codex",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -31,7 +30,7 @@
{
"type": "command",
"command": "bun bin/failproofai.mjs --hook PermissionRequest --cli codex",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -43,7 +42,7 @@
{
"type": "command",
"command": "bun bin/failproofai.mjs --hook PostToolUse --cli codex",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -55,7 +54,7 @@
{
"type": "command",
"command": "bun bin/failproofai.mjs --hook UserPromptSubmit --cli codex",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -67,7 +66,7 @@
{
"type": "command",
"command": "bun bin/failproofai.mjs --hook Stop --cli codex",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.

Comment on lines +3 to +7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the CHANGELOG version matches package.json
rg '"version"' package.json | head -1

Repository: FailproofAI/failproofai

Length of output: 191


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== package.json version ==\n'
rg -n '"version"' package.json | head -1

printf '\n== CHANGELOG.md excerpt ==\n'
cat -n CHANGELOG.md | sed -n '1,20p'

Repository: FailproofAI/failproofai

Length of output: 4098


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
text = Path('CHANGELOG.md').read_text()
print('PR marker present:', '(#' in text or 'PR' in text)
PY

Repository: FailproofAI/failproofai

Length of output: 185


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

text = Path('CHANGELOG.md').read_text()
lines = text.splitlines()
for i, line in enumerate(lines[:20], 1):
    if re.search(r'^\s*-\s+', line):
        print(f'bullet@{i}: {line}')
PY

Repository: FailproofAI/failproofai

Length of output: 3862


Add the PR number to this changelog entry.
The 0.0.12-beta.0 release header matches package.json; the bullet still needs a (#<PR_NUMBER>) suffix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 3 - 7, Add the missing PR reference to the
changelog entry by updating the 0.0.12-beta.0 bullet in CHANGELOG.md to include
the appropriate (#<PR_NUMBER>) suffix; keep the existing release note text
intact and only amend the bullet that describes the Codex hooks fix.

Source: Coding guidelines

## 0.0.11 — 2026-06-26

### Breaking
Expand Down
2 changes: 1 addition & 1 deletion __tests__/e2e/hooks/codex-integration.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
expect(settings.version).toBe(1);
expect(settings.version).toBeUndefined();
const hooks = settings.hooks as Record<string, unknown[]>;
// Codex stores under PascalCase keys
expect(hooks.PreToolUse).toBeDefined();
Expand Down
25 changes: 19 additions & 6 deletions __tests__/hooks/integrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -220,16 +226,23 @@ 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("writeHookEntries removes version on existing files if present", () => {
const settings: Record<string, unknown> = { 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({ hooks: {} }));
const read = codex.readSettings(settingsPath);
expect(read.version).toBe(1);
writeFileSync(settingsPath, JSON.stringify({ version: 1, hooks: {} }));
codex.removeHooksFromFile(settingsPath);
const read = JSON.parse(readFileSync(settingsPath, "utf-8"));
expect(read.version).toBeUndefined();
});

it("re-running writeHookEntries is idempotent", () => {
Expand Down
31 changes: 22 additions & 9 deletions src/hooks/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ function isMarkedHook(hook: unknown): boolean {
return cmd.includes("failproofai") && cmd.includes("--hook");
}

function stripLegacyVersion(settings: Record<string, unknown>): 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}`;
Expand Down Expand Up @@ -232,10 +240,9 @@ export const claudeCode: Integration = {
// • Settings paths: ~/.codex/hooks.json (user) and <cwd>/.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`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Info (doc consistency): This line correctly updates the Codex header to note it no longer carries a top-level version. But two parallel comments in this same file still describe Codex as carrying version: 1 and are now stale:

  • src/hooks/integrations.ts:390 (Copilot header): `{ "version": 1, "hooks": {...} }`, mirroring Codex. — Codex no longer mirrors this shape.
  • src/hooks/integrations.ts:541 (Cursor header): carries `version: 1` like Codex/Copilot. — should read like Copilot.

The PR already fixed the exact twins of these in src/hooks/types.ts (Copilot: dropped “like Codex’s hooks.json”; Cursor: “like Codex/Copilot” → “like Copilot”), so these two in integrations.ts were just missed.

Fix: drop the “mirroring Codex” / “like Codex” references at lines 390 and 541. Documentation-only — not blocking.


interface CodexSettingsFile {
version?: number;
hooks?: Record<string, ClaudeHookMatcher[]>;
[key: string]: unknown;
}
Expand All @@ -261,9 +268,7 @@ export const codex: Integration = {
},

readSettings(settingsPath) {
const raw = readJsonFile(settingsPath);
if (raw.version === undefined) raw.version = 1;
return raw;
return readJsonFile(settingsPath);
},

writeSettings(settingsPath, settings) {
Expand All @@ -280,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,
};
},
Expand All @@ -290,7 +297,7 @@ export const codex: Integration = {

writeHookEntries(settings, binaryPath, scope) {
const s = settings as CodexSettingsFile;
if (s.version === undefined) s.version = 1;
stripLegacyVersion(s as Record<string, unknown>);
if (!s.hooks) s.hooks = {};

for (const eventType of CODEX_HOOK_EVENT_TYPES) {
Expand All @@ -315,7 +322,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<string, unknown>);
if (!settings.hooks) {
if (hadVersion) this.writeSettings(settingsPath, settings as Record<string, unknown>);
return 0;
}

let removed = 0;
for (const eventType of Object.keys(settings.hooks)) {
Expand All @@ -333,7 +344,9 @@ export const codex: Integration = {
}
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;

this.writeSettings(settingsPath, settings as Record<string, unknown>);
if (removed > 0 || hadVersion) {
this.writeSettings(settingsPath, settings as Record<string, unknown>);
}
return removed;
},

Expand Down
4 changes: 2 additions & 2 deletions src/hooks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export const CODEX_TOOL_MAP: Record<string, string> = {
// Settings paths:
// user → ~/.copilot/hooks/failproofai.json
// project → <cwd>/.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];
Expand Down Expand Up @@ -159,7 +159,7 @@ export const COPILOT_TOOL_MAP: Record<string, string> = {
// Settings paths:
// user → ~/.cursor/hooks.json
// project → <cwd>/.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];
Expand Down