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
52 changes: 26 additions & 26 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook SessionStart",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -18,7 +18,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook SessionEnd",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -30,7 +30,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook UserPromptSubmit",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -42,7 +42,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook PreToolUse",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -54,7 +54,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook PermissionRequest",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -66,7 +66,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook PermissionDenied",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -78,7 +78,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook PostToolUse",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -90,7 +90,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook PostToolUseFailure",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -102,7 +102,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook Notification",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -114,7 +114,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook SubagentStart",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -126,7 +126,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook SubagentStop",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -138,7 +138,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook TaskCreated",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -150,7 +150,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook TaskCompleted",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -162,7 +162,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook Stop",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -174,7 +174,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook StopFailure",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -186,7 +186,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook TeammateIdle",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -198,7 +198,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook InstructionsLoaded",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -210,7 +210,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook ConfigChange",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -222,7 +222,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook CwdChanged",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -234,7 +234,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook FileChanged",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -246,7 +246,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook WorktreeCreate",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -258,7 +258,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook WorktreeRemove",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -270,7 +270,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook PreCompact",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -282,7 +282,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook PostCompact",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -294,7 +294,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook Elicitation",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand All @@ -306,7 +306,7 @@
{
"type": "command",
"command": "bun $CLAUDE_PROJECT_DIR/bin/failproofai.mjs --hook ElicitationResult",
"timeout": 60000,
"timeout": 60,
"__failproofai_hook__": true
}
]
Expand Down
12 changes: 6 additions & 6 deletions .cursor/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
}
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 3 additions & 3 deletions __tests__/hooks/integrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down Expand Up @@ -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)", () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Minor (test quality): the description now says "exactly the 10 documented Codex events", but the assertion below (expect(codex.eventTypes).toEqual(CODEX_HOOK_EVENT_TYPES)) is tautological — codex.eventTypes is CODEX_HOOK_EVENT_TYPES (see integrations.ts:257), so it passes for any array of any length and never actually pins the count at 10.

If the intent is to guard the count, assert it directly:

expect(codex.eventTypes).toHaveLength(10);

Not blocking, but worth tightening while you're touching this line — otherwise a future accidental add/remove of a Codex event slips through this test silently.

expect(codex.eventTypes).toEqual(CODEX_HOOK_EVENT_TYPES);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Nit (non-blocking, test hygiene) — this assertion is tautological. codex.eventTypes is CODEX_HOOK_EVENT_TYPES (src/hooks/integrations.ts:427), so toEqual(CODEX_HOOK_EVENT_TYPES) compares the array to itself and can never fail. The title now claims "exactly the 10", but nothing pins the count — if an 11th Codex event were appended, this test stays green.

Fix — assert the length so the title's invariant is actually enforced:

expect(codex.eventTypes).toHaveLength(10);
expect(codex.eventTypes).toContain("permission_request");

Separately, the comment two tests below (line ~208, buildHookEntry sets timeout in SECONDS) still reads "its timeout_sec field". Per the docs this PR cites (developers.openai.com/codex/hooks), the field is literally timeout — the source comment in integrations.ts was corrected in 2eaadd0, but this parallel test comment was missed. Both are cosmetic; neither blocks merge.

// PR 185 omitted permission_request — make sure we have it.
expect(codex.eventTypes).toContain("permission_request");
Expand Down Expand Up @@ -414,7 +414,7 @@ describe("Cursor Agent integration", () => {
const entry = cursor.buildHookEntry("/usr/bin/failproofai", "preToolUse", "user") as Record<string, unknown>;
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();
Expand Down
8 changes: 4 additions & 4 deletions __tests__/hooks/manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("{}");

Expand All @@ -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<Record<string, unknown>> }>)[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}`);
}
});
Expand Down Expand Up @@ -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 () => {
Expand Down
17 changes: 12 additions & 5 deletions src/hooks/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
},
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Comment on lines +599 to +605

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Count events in .cursor/hooks.json and check Cursor event types in types.ts
cat .cursor/hooks.json | jq '.hooks | keys | length'
rg -n 'cursor' src/hooks/types.ts -i

Repository: FailproofAI/failproofai

Length of output: 2224


🏁 Script executed:

#!/bin/bash
# View the CURSOR_HOOK_EVENT_TYPES array definition
sed -n '187,196p' src/hooks/types.ts

echo "---"
# View the .cursor/hooks.json to see exactly which events are configured
cat .cursor/hooks.json

Repository: FailproofAI/failproofai

Length of output: 1473


🏁 Script executed:

# Search for any mention of "7" in the file or "7-event" in PR context
rg '7\s*event|7-event' src/hooks/integrations.ts -i -A 2 -B 2
rg '7' src/hooks/types.ts -A 1 -B 1

Repository: FailproofAI/failproofai

Length of output: 1205


Cursor timeout fix is correct; verify event count discrepancy.

The timeout correction from 60_000 to 60 with seconds-based comments is consistent with the Cursor builder, .cursor/hooks.json, and integrations.test.ts:417. The PR objectives correctly reference a "7-event subset"; src/hooks/integrations.ts:173 confirms this is "6 events that align with Claude's canonical types plus subagentStop". However, .cursor/hooks.json configures only 6 events (sessionStart, sessionEnd, beforeSubmitPrompt, preToolUse, postToolUse, stop), omitting subagentStop despite CURSOR_HOOK_EVENT_TYPES in types.ts:187 including it. Clarify whether the subagentStop hook configuration is intentionally excluded or should be added to the .cursor/hooks.json file.

🤖 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 `@src/hooks/integrations.ts` around lines 598 - 604, The timeout change is
fine, but the Cursor hook event set is inconsistent: `CURSOR_HOOK_EVENT_TYPES`
includes `subagentStop`, while the `.cursor/hooks.json` configuration only wires
up six events and omits it. Update the hook setup in `src/hooks/integrations.ts`
and the generated Cursor config so the event count matches the intended “7-event
subset,” or explicitly keep `subagentStop` excluded if that is intentional and
make the mapping/comments consistent.

[FAILPROOFAI_HOOK_MARKER]: true,
};
},
Expand Down
Loading