diff --git a/.claude/settings.json b/.claude/settings.json index 1f3b5886..fe134c20 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -6,7 +6,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook SessionStart", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -18,7 +18,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook SessionEnd", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -30,7 +30,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook UserPromptSubmit", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -42,7 +42,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook PreToolUse", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -54,7 +54,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook PermissionRequest", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -66,7 +66,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook PermissionDenied", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -78,7 +78,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook PostToolUse", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -90,7 +90,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook PostToolUseFailure", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -102,7 +102,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook Notification", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -114,7 +114,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook SubagentStart", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -126,7 +126,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook SubagentStop", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -138,7 +138,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook TaskCreated", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -150,7 +150,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook TaskCompleted", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -162,7 +162,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook Stop", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -174,7 +174,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook StopFailure", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -186,7 +186,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook TeammateIdle", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -198,7 +198,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook InstructionsLoaded", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -210,7 +210,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook ConfigChange", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -222,7 +222,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook CwdChanged", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -234,7 +234,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook FileChanged", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -246,7 +246,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook WorktreeCreate", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -258,7 +258,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook WorktreeRemove", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -270,7 +270,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook PreCompact", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -282,7 +282,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook PostCompact", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -294,7 +294,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook Elicitation", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] @@ -306,7 +306,7 @@ { "type": "command", "command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook ElicitationResult", - "timeout": 60000, + "timeout": 60, "__failproofai_hook__": true } ] diff --git a/.cursor/hooks.json b/.cursor/hooks.json index 6cc9637b..395bb640 100644 --- a/.cursor/hooks.json +++ b/.cursor/hooks.json @@ -5,42 +5,42 @@ { "type": "command", "command": "bun bin/failproofai.mjs --hook sessionStart --cli cursor", - "timeout": 60000 + "timeout": 60 } ], "sessionEnd": [ { "type": "command", "command": "bun bin/failproofai.mjs --hook sessionEnd --cli cursor", - "timeout": 60000 + "timeout": 60 } ], "beforeSubmitPrompt": [ { "type": "command", "command": "bun bin/failproofai.mjs --hook beforeSubmitPrompt --cli cursor", - "timeout": 60000 + "timeout": 60 } ], "preToolUse": [ { "type": "command", "command": "bun bin/failproofai.mjs --hook preToolUse --cli cursor", - "timeout": 60000 + "timeout": 60 } ], "postToolUse": [ { "type": "command", "command": "bun bin/failproofai.mjs --hook postToolUse --cli cursor", - "timeout": 60000 + "timeout": 60 } ], "stop": [ { "type": "command", "command": "bun bin/failproofai.mjs --hook stop --cli cursor", - "timeout": 60000 + "timeout": 60 } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 85b45213..57807d4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.0.13-beta.0 — 2026-07-09 + +### Features +- Sync agent CLI hook harnesses with upstream docs. Append newly-documented hook events: Codex `subagent_start`/`pre_compact`/`post_compact`/`subagent_stop` (with their `CODEX_EVENT_MAP` canonical mappings to `SubagentStart`/`PreCompact`/`PostCompact`/`SubagentStop`); Copilot `PostToolUseFailure`/`ErrorOccurred`/`PreCompact`/`PermissionRequest`/`Notification`; Claude `Setup`. Claude's `MessageDisplay` is documented but deliberately not installed — it is observe-only and fires on every assistant message, so a per-message hook would add cost with no enforcement value. (#485) + +### Fixes +- Correct the Claude and Cursor hook `timeout` unit from `60000` to `60`. Both CLIs read `timeout` in seconds per their upstream docs (Claude: "Seconds before canceling … 60 for agent"; Cursor: "Execution timeout in seconds"), so the old `60000` meant ~16.7h instead of 60s. Gemini's `timeout` is genuinely milliseconds (default 60000) and is left unchanged. (#485) + ## 0.0.12-beta.0 — 2026-07-09 ### Features diff --git a/__tests__/hooks/integrations.test.ts b/__tests__/hooks/integrations.test.ts index b955ace8..b447154b 100644 --- a/__tests__/hooks/integrations.test.ts +++ b/__tests__/hooks/integrations.test.ts @@ -125,7 +125,7 @@ describe("Claude Code integration", () => { const entry = claudeCode.buildHookEntry("/usr/bin/failproofai", "PreToolUse", "user"); expect(entry.command).toBe('"/usr/bin/failproofai" --hook PreToolUse'); expect(entry.command).not.toContain("--cli"); - expect(entry.timeout).toBe(60_000); + expect(entry.timeout).toBe(60); expect(entry[FAILPROOFAI_HOOK_MARKER]).toBe(true); }); @@ -191,7 +191,7 @@ describe("OpenAI Codex integration", () => { expect(codex.scopes).toEqual(["user", "project"]); }); - it("eventTypes are exactly the 6 documented Codex events (snake_case)", () => { + it("eventTypes are exactly the 10 documented Codex events (snake_case)", () => { expect(codex.eventTypes).toEqual(CODEX_HOOK_EVENT_TYPES); // PR 185 omitted permission_request — make sure we have it. expect(codex.eventTypes).toContain("permission_request"); @@ -414,7 +414,7 @@ describe("Cursor Agent integration", () => { const entry = cursor.buildHookEntry("/usr/bin/failproofai", "preToolUse", "user") as Record; expect(entry.type).toBe("command"); expect(entry.command).toBe('"/usr/bin/failproofai" --hook preToolUse --cli cursor'); - expect(entry.timeout).toBe(60_000); + expect(entry.timeout).toBe(60); expect(entry[FAILPROOFAI_HOOK_MARKER]).toBe(true); // Cursor entries use the Claude-style `command` field, not Copilot's bash/powershell split. expect(entry.bash).toBeUndefined(); diff --git a/__tests__/hooks/manager.test.ts b/__tests__/hooks/manager.test.ts index 0956726d..c10f5aa4 100644 --- a/__tests__/hooks/manager.test.ts +++ b/__tests__/hooks/manager.test.ts @@ -65,7 +65,7 @@ describe("hooks/manager", () => { }); describe("installHooks", () => { - it("installs hooks for all 28 event types into empty settings", async () => { + it("installs hooks for all 29 event types into empty settings", async () => { vi.mocked(existsSync).mockReturnValue(true); vi.mocked(readFileSync).mockReturnValue("{}"); @@ -77,14 +77,14 @@ describe("hooks/manager", () => { expect(path).toBe(USER_SETTINGS_PATH); const written = JSON.parse(content as string); - expect(Object.keys(written.hooks)).toHaveLength(28); + expect(Object.keys(written.hooks)).toHaveLength(29); for (const [eventType, matchers] of Object.entries(written.hooks)) { expect(matchers).toHaveLength(1); const hook = (matchers as Array<{ hooks: Array> }>)[0].hooks[0]; expect(hook.__failproofai_hook__).toBe(true); expect(hook.type).toBe("command"); - expect(hook.timeout).toBe(60_000); + expect(hook.timeout).toBe(60); expect(hook.command).toBe(`"/usr/local/bin/failproofai" --hook ${eventType}`); } }); @@ -231,7 +231,7 @@ describe("hooks/manager", () => { expect(writeFileSync).toHaveBeenCalledOnce(); const [, content] = vi.mocked(writeFileSync).mock.calls[0]; const written = JSON.parse(content as string); - expect(Object.keys(written.hooks)).toHaveLength(28); + expect(Object.keys(written.hooks)).toHaveLength(29); }); it("uses 'where' on Windows and handles multi-line output", async () => { diff --git a/src/hooks/integrations.ts b/src/hooks/integrations.ts index 99638229..cb3dc9ef 100644 --- a/src/hooks/integrations.ts +++ b/src/hooks/integrations.ts @@ -154,7 +154,10 @@ export const claudeCode: Integration = { return { type: "command", command, - timeout: 60_000, + // Claude reads `timeout` in SECONDS per https://code.claude.com/docs/en/hooks + // ("Seconds before canceling. Defaults: 600 for command ...; 60 for agent"), + // NOT milliseconds. 60 = 60s; the old 60000 meant ~16.7h. (#482-class unit fix) + timeout: 60, [FAILPROOFAI_HOOK_MARKER]: true, }; }, @@ -285,8 +288,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). + // Codex reads `timeout` in SECONDS (the field is literally `timeout`, + // default 600 per https://developers.openai.com/codex/hooks) — same unit as + // Claude/Cursor/Copilot, and unlike Gemini, whose `timeout` is in + // milliseconds (default 60000). 60 = 60s, not 60000ms (~16.7h). command, timeout: 60, [FAILPROOFAI_HOOK_MARKER]: true, @@ -591,11 +596,13 @@ export const cursor: Integration = { scope === "project" ? `npx -y failproofai --hook ${eventType} --cli cursor` : `"${binaryPath}" --hook ${eventType} --cli cursor`; - // `timeout` is documented as ms in Cursor's schema (matches Claude). + // `timeout` is documented in SECONDS in Cursor's schema per + // https://cursor.com/docs/hooks ("Execution timeout in seconds"; doc examples + // use 30 and 10), NOT milliseconds. 60 = 60s; the old 60000 meant ~16.7h. return { type: "command", command, - timeout: 60_000, + timeout: 60, [FAILPROOFAI_HOOK_MARKER]: true, }; }, diff --git a/src/hooks/types.ts b/src/hooks/types.ts index f9483b7e..7eea07c7 100644 --- a/src/hooks/types.ts +++ b/src/hooks/types.ts @@ -18,6 +18,18 @@ export const CODEX_HOOK_EVENT_TYPES = [ "post_tool_use", "user_prompt_submit", "stop", + // Newly documented upstream (https://developers.openai.com/codex/hooks) — + // snake_case forms of the documented SubagentStart / PreCompact / PostCompact / + // SubagentStop events. Each has an exact 1:1 canonical HookEventType (already + // present in HOOK_EVENT_TYPES), so their CODEX_EVENT_MAP entries below are + // filled in directly. The map is an exhaustive `Record`, so tsc guarantees every event here keeps a mapping — a + // partial sync (event added, mapping missing) fails the build instead of + // silently writing an `undefined` event key into users' .codex/hooks.json. + "subagent_start", + "pre_compact", + "post_compact", + "subagent_stop", ] as const; export type CodexHookEventType = (typeof CODEX_HOOK_EVENT_TYPES)[number]; @@ -28,6 +40,10 @@ export const CODEX_EVENT_MAP: Record = { post_tool_use: "PostToolUse", user_prompt_submit: "UserPromptSubmit", stop: "Stop", + subagent_start: "SubagentStart", + pre_compact: "PreCompact", + post_compact: "PostCompact", + subagent_stop: "SubagentStop", }; /** @@ -92,6 +108,18 @@ export const COPILOT_HOOK_EVENT_TYPES = [ "PostToolUse", "Stop", "SubagentStop", + // Newly documented upstream (cli-hooks-reference), each with an explicit + // PascalCase "VS Code compatible" variant shown in the docs. No COPILOT_EVENT_MAP + // exists (Copilot names are already Pascal), so appending keeps the build green. + // NOTE: `subagentStart` is also newly documented but appears camelCase-ONLY (no + // PascalCase variant listed), so it is deferred to the reviewer checklist rather + // than guessing a casing. `notification`, by contrast, DOES have a documented + // `Notification` PascalCase variant, so it is appended below. + "PostToolUseFailure", + "ErrorOccurred", + "PreCompact", + "PermissionRequest", + "Notification", ] as const; export type CopilotHookEventType = (typeof COPILOT_HOOK_EVENT_TYPES)[number]; @@ -556,6 +584,18 @@ export const HOOK_EVENT_TYPES = [ "ElicitationResult", "UserPromptExpansion", "PostToolBatch", + // Newly documented upstream (https://code.claude.com/docs/en/hooks lifecycle + // table). `Setup` fires only on `--init`/`--maintenance` (low-frequency), so it + // is appended and installed like any other event. + // + // `MessageDisplay` is intentionally NOT appended. The docs mark it observe-only + // (it cannot block or modify anything) and note it fires on *every* assistant + // message display with no matcher support. Since `writeHookEntries` installs a + // hook for every entry in this array, appending it would spawn a failproofai + // subprocess on every message render for zero enforcement value. Deferred to the + // PR reviewer checklist; add it here only if a future observe-only use case + // (e.g. redaction/telemetry) justifies the per-message cost. + "Setup", ] as const; export type HookEventType = (typeof HOOK_EVENT_TYPES)[number];