[hooks] add OpenAI Codex hook integration alongside Claude Code#220
Conversation
Introduces a per-CLI Integration adapter (src/hooks/integrations.ts) so
failproofai can register, dispatch, and protect both Claude Code and
OpenAI Codex sessions out of the box. Codex's hook protocol is
Claude-compatible by design — same stdin field names, same exit-code +
JSON output (`hookSpecificOutput.permissionDecision`, `additionalContext`,
exit 2 + stderr for Stop), same `{hooks: {EventName: [...]}}` settings
shape. The work is concentrated in install plumbing and a small set of
agent-agnostic-ifications.
Highlights:
- All six documented Codex events (SessionStart, PreToolUse,
PermissionRequest, PostToolUse, UserPromptSubmit, Stop). PermissionRequest
emits the Codex-specific hookSpecificOutput.decision.behavior shape and
is wired into block-sudo so sandbox-escalation requests are guarded too.
- Settings paths: ~/.codex/hooks.json (user) and <cwd>/.codex/hooks.json
(project). Codex stores hook entries under PascalCase event keys; we
canonicalize the snake_case stdin name to PascalCase before policy lookup.
- New `--cli claude-code|codex` flag on `policies --install/--uninstall`
and `--hook EVENT`. Repeatable / comma-separated. Default behavior is
interactive: detect installed CLIs (probe `which`) and prompt; auto-pick
if only one is found; back-compat default of claude-code when no flag
and no agent detected.
- Permission mode for Codex resolved by walking
~/.codex/sessions/<…>/<sessionId>.jsonl for turn_context.payload
.approval_policy and mapping never→full-auto, on-request→default.
- Generalized isAgentInternalPath / isAgentSettingsFile to cover
~/.codex/ and .codex/hooks.json so existing path-protection rules apply.
- Telemetry: every hook decision and install/remove now includes the
originating cli/integration; activity dashboard renders a per-CLI badge.
- README: added supported-CLIs section with Claude Code + OpenAI Codex
badges and "more coming soon".
- Tests: 24 new unit tests for integrations.ts + 10 new E2E tests for the
full Codex install→fire→deny flow (PreToolUse, PostToolUse,
PermissionRequest, snake_case canonicalization, --scope local rejection,
uninstall idempotence). All 1066 unit + 217 E2E tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Companion docs update for the OpenAI Codex hook integration. Spells out the per-CLI settings file locations (~/.claude/settings.json vs ~/.codex/hooks.json) and shows how to target one or both agents via `--cli claude-code|codex` (repeatable / comma-separated). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (3)
📒 Files selected for processing (21)
📝 WalkthroughWalkthroughThis PR adds comprehensive OpenAI Codex hook integration alongside existing Claude Code support. It introduces a new integration registry system, CLI-specific settings file handling with proper scope management, event name canonicalization (snake_case to PascalCase), permission-mode resolution from Codex transcripts, and activity tracking by originating CLI. The implementation spans the hook handler, policy evaluator, installer, CLI interface, and test infrastructure. Changes
Sequence DiagramsequenceDiagram
participant CLI as Codex CLI
participant Handler as Hook Handler
participant Resolver as Event Resolver
participant Evaluator as Policy Evaluator
participant Store as Activity Store
participant Integrations as Integration Registry
CLI->>Handler: --hook PermissionRequest --cli codex
Handler->>Handler: Parse cli parameter
Handler->>Resolver: canonicalizeEventType('permission_request', 'codex')
Resolver-->>Handler: 'PermissionRequest' (PascalCase)
Handler->>Evaluator: evaluatePolicies(PermissionRequest, context)
Evaluator->>Evaluator: Evaluate deny policy<br/>(applies to PermissionRequest)
Evaluator-->>Handler: exitCode: 0, decision.behavior: 'deny'
Handler->>Store: persistActivity({<br/>event: 'PermissionRequest',<br/>integration: 'codex',<br/>...})
Store-->>Handler: Activity persisted
Handler-->>CLI: JSON response with<br/>hookSpecificOutput.decision.behavior
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly Related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
CI's E2E runner does not run `npm install -g` or `bun link` for failproofai, so the new Codex install/uninstall E2E tests failed at the `which failproofai` probe. Add an override env var that bypasses the PATH probe — tests now point it at bin/failproofai.mjs in the workspace. No production behavior change: if the override is unset or empty, the existing `which failproofai` path runs unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/hooks/handler.ts (1)
121-128:⚠️ Potential issue | 🟡 MinorNormalize
custom_hook_error.event_typetoo.This is the one telemetry path still sending raw
eventTypewhile the rest of the handler switched tocanonicalEventType. For Codex hook failures that splits the same event across values likepre_tool_useandPreToolUse, which makes error analysis noisier than the success path.Suggested fix
void trackHookEvent(getInstanceId(), "custom_hook_error", { hook_name: hookName, error_type: isTimeout ? "timeout" : "exception", - event_type: eventType, + event_type: canonicalEventType, cli, is_convention_policy: isConvention, convention_scope: conventionScope ?? null, });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/handler.ts` around lines 121 - 128, The telemetry call to trackHookEvent is still sending the raw eventType which causes noisy error telemetry; update the payload in the trackHookEvent call (the object with keys hook_name, error_type, event_type, cli, is_convention_policy, convention_scope) to use the normalized canonicalEventType instead of eventType (e.g., set event_type: canonicalEventType or fallback to canonicalEventType ?? eventType), leaving other fields (hookName, isTimeout, cli, isConvention, conventionScope) unchanged so error events match the success path.
🧹 Nitpick comments (3)
src/hooks/integrations.ts (1)
49-57: Consider adding a timeout tobinaryExiststo prevent hangs.The
execSynccall inbinaryExistscould hang indefinitely ifwhich/wherestalls. This is unlikely but could cause poor UX during CLI startup.♻️ Suggested improvement
function binaryExists(name: string): boolean { try { const cmd = process.platform === "win32" ? `where ${name}` : `which ${name}`; - execSync(cmd, { encoding: "utf8", stdio: "pipe" }); + execSync(cmd, { encoding: "utf8", stdio: "pipe", timeout: 5000 }); return true; } catch { return false; } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/integrations.ts` around lines 49 - 57, The binaryExists function can hang because execSync is called without a timeout; update binaryExists to pass a timeout option to execSync (e.g., { encoding: "utf8", stdio: "pipe", timeout: 2000 }) so which/where calls are killed after a short period, and ensure the catch still returns false on timeout or other errors; reference the binaryExists function, the execSync call, and the use of process.platform/cmd when making this change.src/hooks/manager.ts (1)
246-256: Duplicate-scope warning only covers Claude Code — consider expanding or clarifying.The warning uses
HOOK_SCOPESandhooksInstalledInSettingswhich are Claude Code-specific. If a user installs hooks for both Claude Code and Codex, they won't be warned about Codex duplicate scopes.This may be intentional (comment says "Claude Code only"), but consider:
- Adding a similar check for Codex scopes, or
- Adding a comment explaining why Codex is excluded (e.g., "Codex only supports 2 scopes, so duplicates are less likely")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/manager.ts` around lines 246 - 256, The duplicate-scope warning only checks HOOK_SCOPES (Claude Code); extend the same duplicate-detection logic to Codex hook scopes or clearly document the exclusion. Concretely, replicate the block that uses deduplicateScopes(HOOK_SCOPES, cwd), hooksInstalledInSettings(...), scopeLabel(...) and the scopeList/duplicates handling for the Codex scopes constant (e.g., CODEX_HOOK_SCOPES or the actual Codex scopes identifier) so users get the same warning for Codex, or add a clarifying comment above this block explaining why Codex is intentionally excluded.bin/failproofai.mjs (1)
236-252: Consider extracting duplicated--cliparsing logic into a helper.The
--cliflag parsing and validation logic is nearly identical between the install (lines 236-252) and uninstall (lines 309-325) flows. This duplication increases maintenance burden.♻️ Suggested refactor
// Add helper function before the runCli() function: function parseCliFlags(subArgs) { const cliFlagIdxs = subArgs.map((a, i) => (a === "--cli" ? i : -1)).filter((i) => i >= 0); const cliFlagValues = []; const cliConsumedIdxs = new Set(); const VALID_CLIS = new Set(["claude-code", "codex"]); for (const idx of cliFlagIdxs) { const value = subArgs[idx + 1]; if (!value || value.startsWith("-")) { throw new CliError("Missing value for --cli. Valid values: claude-code, codex"); } for (const part of value.split(",").map((s) => s.trim()).filter(Boolean)) { if (!VALID_CLIS.has(part)) { throw new CliError(`Invalid --cli value: ${part}. Valid: claude-code, codex`); } cliFlagValues.push(part); } cliConsumedIdxs.add(idx + 1); } return { cliFlagValues, cliConsumedIdxs }; }Also applies to: 309-325
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@bin/failproofai.mjs` around lines 236 - 252, Extract the duplicated --cli parsing/validation into a helper function (e.g., parseCliFlags) that accepts subArgs and returns { cliFlagValues, cliConsumedIdxs }; move the constants/validation (VALID_CLIS) and error throws (CliError) into that helper and call it from both the install and uninstall flows (replacing the inline loops at the blocks that currently compute cliFlagIdxs/cliFlagValues/cliConsumedIdxs), ensuring the helper reads subArgs, checks for missing values and invalid parts (same messages), splits comma lists and populates cliFlagValues and cliConsumedIdxs so both places reuse the logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@__tests__/e2e/hooks/codex-integration.e2e.test.ts`:
- Around line 173-176: The failing E2E tests call execSync directly (using
BINARY_PATH) without setting FAILPROOFAI_DIST_PATH, causing
resolveFailproofaiBinary() to not find the binary; update each execSync
invocation in the install/uninstall suite (the calls around the execSync using
`bun ${BINARY_PATH} policies --install` / uninstall) to include
FAILPROOFAI_DIST_PATH in the env (alongside HOME and
FAILPROOFAI_TELEMETRY_DISABLED), or refactor by adding a helper (e.g.,
runPoliciesCmd) that wraps execSync and injects FAILPROOFAI_DIST_PATH (resolved
via the repo root) so all calls use the same env trusted by runHook().
In `@app/actions/install-hooks-web.ts`:
- Around line 27-31: removeHooksWebAction currently forwards cli as undefined
into removeHooks which triggers the default fallback in src/hooks/manager.ts
(["claude-code"]) and can leave unexpected hooks behind; change
removeHooksWebAction so that when cli is omitted it resolves the same CLI list
used by the install-side path (i.e. replicate the install resolution logic or
call the same helper used by the installer) and pass that resolved
IntegrationType[] into removeHooks instead of undefined, referencing
removeHooksWebAction and removeHooks (and the fallback in src/hooks/manager.ts)
to locate where to apply the fix.
In `@CHANGELOG.md`:
- Around line 5-6: Update the CHANGELOG.md bullet that starts "Add OpenAI Codex
hook integration." by appending the PR trailer " (`#220`)" to the end of that
bullet so it matches the repository changelog format (i.e., change the existing
Features entry line to end with " (`#220`)").
In `@docs/configuration.mdx`:
- Around line 199-206: Update the docs paragraph describing the --cli fallback
to include the two missing branches: state that when no agent binaries are
detected by the "which claude / which codex" checks the installer defaults to
targeting "claude-code", and state that in non-interactive runs (CI/TTY-less)
the installer will skip prompting and target both detected CLIs rather than
prompting; keep references to the existing examples/commands (e.g., failproofai
policies --install --cli) and the detection logic so readers can find the
matching implementation paths.
In `@README.md`:
- Around line 26-36: Change the heading level for "Supported agent CLIs" from h3
to h2 to avoid skipping h2 (replace "### Supported agent CLIs" with "##
Supported agent CLIs"), or alternatively move the badges into an existing h2
section so the document hierarchy remains sequential; update the heading text
exactly "Supported agent CLIs" and ensure any surrounding TOC or references
match the new level.
In `@src/hooks/install-prompt.ts`:
- Around line 72-86: Change the onKey handler in install-prompt.ts to accept the
(str, key) signature and detect Ctrl+C via key.ctrl && key.name === "c" (same
pattern as promptPolicySelection()), restoring terminal state (reset setRawMode
to wasRaw, removeListener("keypress", onKey), and write a newline) before
aborting; on Ctrl+C either reject the Promise with an AbortError or call
process.exit(1) instead of falling through to resolve(detected). Also keep the
existing branches for "c" and "d" (after normalizing str) so ordinary letter
input still resolves correctly.
---
Outside diff comments:
In `@src/hooks/handler.ts`:
- Around line 121-128: The telemetry call to trackHookEvent is still sending the
raw eventType which causes noisy error telemetry; update the payload in the
trackHookEvent call (the object with keys hook_name, error_type, event_type,
cli, is_convention_policy, convention_scope) to use the normalized
canonicalEventType instead of eventType (e.g., set event_type:
canonicalEventType or fallback to canonicalEventType ?? eventType), leaving
other fields (hookName, isTimeout, cli, isConvention, conventionScope) unchanged
so error events match the success path.
---
Nitpick comments:
In `@bin/failproofai.mjs`:
- Around line 236-252: Extract the duplicated --cli parsing/validation into a
helper function (e.g., parseCliFlags) that accepts subArgs and returns {
cliFlagValues, cliConsumedIdxs }; move the constants/validation (VALID_CLIS) and
error throws (CliError) into that helper and call it from both the install and
uninstall flows (replacing the inline loops at the blocks that currently compute
cliFlagIdxs/cliFlagValues/cliConsumedIdxs), ensuring the helper reads subArgs,
checks for missing values and invalid parts (same messages), splits comma lists
and populates cliFlagValues and cliConsumedIdxs so both places reuse the logic.
In `@src/hooks/integrations.ts`:
- Around line 49-57: The binaryExists function can hang because execSync is
called without a timeout; update binaryExists to pass a timeout option to
execSync (e.g., { encoding: "utf8", stdio: "pipe", timeout: 2000 }) so
which/where calls are killed after a short period, and ensure the catch still
returns false on timeout or other errors; reference the binaryExists function,
the execSync call, and the use of process.platform/cmd when making this change.
In `@src/hooks/manager.ts`:
- Around line 246-256: The duplicate-scope warning only checks HOOK_SCOPES
(Claude Code); extend the same duplicate-detection logic to Codex hook scopes or
clearly document the exclusion. Concretely, replicate the block that uses
deduplicateScopes(HOOK_SCOPES, cwd), hooksInstalledInSettings(...),
scopeLabel(...) and the scopeList/duplicates handling for the Codex scopes
constant (e.g., CODEX_HOOK_SCOPES or the actual Codex scopes identifier) so
users get the same warning for Codex, or add a clarifying comment above this
block explaining why Codex is intentionally excluded.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 55374b66-3007-470b-ad25-792f6ac7b79a
📒 Files selected for processing (21)
CHANGELOG.mdREADME.md__tests__/e2e/helpers/hook-runner.ts__tests__/e2e/helpers/payloads.ts__tests__/e2e/hooks/codex-integration.e2e.test.ts__tests__/hooks/handler.test.ts__tests__/hooks/integrations.test.tsapp/actions/install-hooks-web.tsapp/policies/hooks-client.tsxbin/failproofai.mjsdocs/configuration.mdxsrc/hooks/builtin-policies.tssrc/hooks/handler.tssrc/hooks/hook-activity-store.tssrc/hooks/install-prompt.tssrc/hooks/integrations.tssrc/hooks/manager.tssrc/hooks/policy-evaluator.tssrc/hooks/policy-types.tssrc/hooks/resolve-permission-mode.tssrc/hooks/types.ts
…d .codex/hooks.json Three follow-up changes from PR review feedback: 1. Integration ID renamed `claude-code` → `claude`. The CLI binary is `claude` (per `claude --help`), so the integration key now matches. `displayName` stays "Claude Code" in user-facing strings; only the machine-readable id changed. Sweep covers types/handler/manager/install-prompt/resolve-permission-mode, the dashboard's IntegrationBadge, both web actions, all unit + E2E tests, README/CHANGELOG/docs/configuration.mdx/getting-started.mdx/introduction.mdx. 2. `--cli` flag now accepts space-separated multi-values (`--cli claude codex`) instead of comma-separated (`--cli claude,codex`). The parser consumes positional values after `--cli` only while they match a known CLI name — so `--cli claude block-sudo` correctly parses block-sudo as a positional policy name, not a malformed --cli value. Repetition still works (`--cli claude --cli codex`). Help text and EXAMPLES updated. 3. README's per-CLI shields replaced with actual logo images (simpleicons CDN: `cdn.simpleicons.org/claude` and `cdn.simpleicons.org/openai`) and a "+ more coming soon" line. 4. New `.codex/hooks.json` for this repo, mirroring the existing `.claude/settings.json`. All six Codex events under PascalCase keys with the `__failproofai_hook__` marker. Per Codex docs, Codex sets no env-var equivalent of `$CLAUDE_PROJECT_DIR`, so the command uses a relative `bun bin/failproofai.mjs` path (Codex spawns hooks with the project root as cwd in the common case). CLAUDE.md grew a section explaining this and the `--cli codex` install recipe for production users. Verified: 1066/1066 unit tests pass, 217/217 E2E tests pass, lint clean, build green. Re-confirmed against the live Codex hooks docs that our stdin field reads (session_id, transcript_path, cwd, hook_event_name, tool_name, tool_input, tool_response, prompt) and output protocol (`hookSpecificOutput.permissionDecision: "deny"` for PreToolUse, `hookSpecificOutput.decision.behavior: "deny"` for PermissionRequest, `hookSpecificOutput.additionalContext` for PostToolUse, exit 2 + stderr fallback) match exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Six fixes from the bot review on the original Codex-integration commits: 1. README "Supported agent CLIs" heading lifted from h3 to h2 to fix markdownlint MD001 (heading-level jump from h1). 2. CHANGELOG.md Codex feature bullet now ends with the required `(#220)` PR-number trailer per repo convention. 3. docs/configuration.mdx fallback-branches section now covers all four cases the installer actually handles: one CLI detected (auto-pick), both detected interactive (prompt), both detected non-interactive (install for both), neither detected (fall back to claude with a warning). 4. app/actions/install-hooks-web.ts removeHooksWebAction mirrors the install-side resolution: when the dashboard omits cli, it resolves via detectInstalledClis() instead of forwarding undefined into removeHooks() and silently falling back to ["claude"]. Prevents a web uninstall from leaving Codex hooks behind. 5. src/hooks/install-prompt.ts raw-mode CLI picker now accepts the (str, key) keypress signature and detects Ctrl+C/Ctrl+D via key.ctrl + key.name, restoring the terminal and exiting 130 on abort instead of silently resolving to "install for both". Mirrors the contract used by promptPolicySelection(). 6. src/hooks/resolve-permission-mode.ts no longer scans the full ~/.codex/sessions tree on every Codex hook. Adds a disk cache at ~/.failproofai/cache/codex-session-paths.json (sessionId → transcript path, verified-on-read) and tries today + yesterday's date directories before falling back to the full tree. Bounds the common-case latency to O(1) lookups for the second+ tool use of any active session. Verified: 1066/1066 unit, 217/217 E2E, lint clean (only the pre-existing <img> warning), tsc clean, build green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`cdn.simpleicons.org/openai` returns 404 — simpleicons doesn't ship an
OpenAI mark. Swap both Claude + OpenAI logos to iconify
(api.iconify.design/logos/{anthropic,openai}-icon.svg) which:
• Both URLs return HTTP 200 colored SVGs (verified via curl -I)
• Iconify covers both brands, so the visual treatment is consistent
• Plain SVG, no color-param suffix needed
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two CDN attempts didn't render reliably on GitHub:
• simpleicons.org returned 404 for /openai (no such slug)
• iconify.design returned valid SVGs but with em-based dimensions
(width="0.99em" height="1em") — GitHub's image proxy/camo can't
scale em units from an <img height="48"> attribute, so the icons
appeared blank/missing on the rendered README. Anthropic's iconify
SVG also used fill="#181818" (near-black), invisible on dark theme.
Fix: vendor the simpleicons SVGs into assets/logos/ and add explicit
brand-color fills:
• claude.svg with fill="#D97757" (Anthropic orange)
• openai.svg with fill="#10A37F" (OpenAI green)
Reference via relative paths (`assets/logos/claude.svg`) which GitHub
resolves correctly when rendering README.md, and which scale cleanly
because the SVGs use viewBox="0 0 24 24" with no inline pixel
dimensions — the <img width/height> attrs control render size.
Verified detectInstalledClis() works on a machine with both CLIs:
which claude → /home/nivedit/.local/bin/claude
which codex → /home/nivedit/.nvm/versions/node/v25.8.2/bin/codex
detectInstalledClis() → ["claude", "codex"]
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…en for Codex logo `#10A37F` is OpenAI's *ChatGPT* brand color, not Codex's. Per OpenAI's own guidelines the spiral mark renders black on light backgrounds and white on dark backgrounds, not green. Replace assets/logos/openai.svg with two theme variants: • openai-light.svg → fill="#000000" (black; renders on light theme) • openai-dark.svg → fill="#FFFFFF" (white; renders on dark theme) Reference both via <picture> in README so GitHub serves the right one based on `prefers-color-scheme`. Falls back to the light variant when the browser or renderer doesn't support media queries. Claude logo stays #D97757 (Anthropic's brand orange) — it's the canonical brand color and reads well on both light and dark themes, so no <picture> needed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bumps package.json from 0.0.9-beta.2 to 0.0.9 and rolls the ## Unreleased changelog section into ## 0.0.9 — 2026-04-28. 0.0.9 contents: Features: - OpenAI Codex hook integration via `failproofai policies --install --cli codex` (or --cli claude codex for both); supports all six Codex hook events, PermissionRequest wired through policy-evaluator, per-CLI telemetry tagging, interactive arrow-key CLI selector when both agents detected (#220, #222, #223) - Surface Slack community link in CLI banner and Reach Us menu (#225) Fixes: - Trailing blank line after enabled-policies summary in install output (#224) - Resolve hook bin via $CLAUDE_PROJECT_DIR (was relative path) (#219) - Mintlify validation of Arabic built-in-policies docs Docs: - Bump built-in policy count from 32 to 39 in README and translations (#207) Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Promotes the version from 0.0.9-beta.3 to 0.0.9 stable. The ## 0.0.9 — 2026-04-28 changelog section is already up to date with the contents below; no CHANGELOG churn required. 0.0.9 contents: Features: - OpenAI Codex hook integration via `failproofai policies --install --cli codex` (or --cli claude codex for both); supports all six Codex hook events, PermissionRequest wired through policy-evaluator, per-CLI telemetry tagging, interactive arrow-key CLI selector when both agents detected (#220, #222, #223) - Activity dashboard CLI filter alongside event-type, policy, and session-id filters; Codex sessions in the activity feed are now clickable, with the existing log viewer rendering Codex transcripts via lib/codex-sessions.ts (#226) - Surface Slack community link in CLI banner and Reach Us menu (#225) - Show OpenAI Codex projects on the /projects page alongside Claude Code projects, with CLI badges per row and per session; /project/[name] is Codex-aware and the session viewer renders the CLI badge beside the Session Log header (#232) Fixes: - Trailing blank line after enabled-policies summary in install output (#224) - Resolve hook bin via $CLAUDE_PROJECT_DIR (was relative path) (#219) - Mintlify validation of Arabic built-in-policies docs - Mintlify parse error in docs/de/dashboard.mdx (#229) - block-read-outside-cwd false-deny on globs and -v host:/path (#230) - Decouple hook chain from clsx/tailwind via lib/format-date.ts (#231) Docs: - Bump built-in policy count from 32 to 39 in README and translations (#207) Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat: add GitHub Copilot CLI integration (beta) Mirrors the Codex rollout (#220 + #226 + #232) for GitHub Copilot CLI: hook install/handler/path-protection, activity dashboard CLI filter + session viewer fallback, /projects listing + per-project sessions. Hooks install in Copilot's "VS Code compatible" PascalCase mode so the existing handler, snake_case payload parser, and hookSpecificOutput output shape all work unchanged. User scope writes ~/.copilot/hooks/failproofai.json; project scope writes <cwd>/.github/hooks/failproofai.json with version:1 and bash+powershell +timeoutSec entries (verified against Copilot CLI 1.0.39). Dashboard parser was rewritten against the real events.jsonl schema inspected from a live install — dotted-path record types (session.start, user.message, assistant.message, tool.execution_start/ _complete, …) rather than the camelCase hook payload format the docs hinted at. Project listing reads cwd from each session's workspace.yaml (always present, even pre-interaction). Adds lib/cli-registry.ts as the single source of truth for per-CLI metadata (label, badge classes, dashboard providers); future CLIs land with one registry entry instead of touching badge/filter/merge code. Marked beta because Copilot's events.jsonl schema is not publicly documented; parser is best-effort and falls open on unknown record types. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: green CI + uninstall prompt heading CI build failed with Turbopack "Module not found: Can't resolve 'fs/promises'" because lib/cli-registry.ts declared lazy `getProjects` / `loadSessionLog` providers via dynamic imports of codex-projects / copilot-projects. Even though no caller used them yet, Turbopack traced the dynamic-import expressions through the client-bundled CliBadge / project-list / hooks-client consumers and pulled Node-only modules into the client graph. Drop the providers from cli-registry; it's now a pure metadata module (label + badgeClasses). lib/projects.ts and the session viewer page already have their own targeted dynamic imports. Also: remove cross-module import in lib/copilot-projects.ts (was importing `getCopilotSessionStateRoot` from copilot-sessions); inline the trivial path computation so the two trees stay independent (mirrors how lib/codex-projects.ts and lib/codex-sessions.ts are arranged). Bug fix surfaced in conversation: `failproofai policies --uninstall` prompt heading said "Install Hooks" / "Choose where to install:" even though the action was uninstall. Add an `action: "install" | "uninstall"` parameter to resolveTargetClis + promptCliTargetSelection so the heading reads "Remove Hooks" / "Choose where to remove from:" during uninstall, and the non-interactive single-CLI log line says "removing hooks from <CLI>" instead of "installing hooks for <CLI>". Threaded through both bin/failproofai.mjs call sites. Tests: 1158 unit + 227 e2e pass; tsc clean; lint clean; bun run build clean (Turbopack no longer errors). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: address CodeRabbit review on PR #236 Critical / major: - lib/cli-registry.ts: isKnownCli now uses Object.prototype.hasOwnProperty.call instead of `in` so inherited prototype keys ("toString", "constructor", "hasOwnProperty") don't validate as known CLIs. - lib/copilot-sessions.ts: getCopilotSessionDir + findCopilotTranscript / findCopilotWorkspace reject path-traversal sessionIds (`../foo`, `..`, `/abs/path`) by `path.resolve`-ing the candidate and asserting it stays under getCopilotSessionStateRoot(). - lib/copilot-projects.ts: track `hasTranscript` per session-state dir and filter workspace-only sessions out of metasToSessionFiles — /project/[cwd] no longer renders clickable rows that lead to "Session log file not found." Workspace-only sessions still surface the project itself on /projects. - lib/copilot-projects.ts + lib/projects.ts: switch fs/promises, os, path imports to the `node:` specifier form (mirrors lib/codex- sessions.ts and is what Turbopack expects). - __tests__/e2e/hooks/copilot-integration.e2e.test.ts: pass FAILPROOFAI_BINARY_OVERRIDE in the --scope local rejection test so it doesn't depend on a global failproofai install in PATH. - __tests__/lib/projects.test.ts: add Copilot-aggregation assertions — Claude+Copilot merge, Claude+Codex+Copilot merge, Copilot-only surfacing, and graceful fall-through when getCopilotProjects() rejects. Minor: - CHANGELOG.md: shorten Copilot Features + Fixes entries to the repo's one-line + (#236) format. - docs/dashboard.mdx: rewrite Activity-tab Session viewer wording so the badge explanation lists all three CLIs and the click-through description includes Copilot CLI. Tests: 1165 unit + 227 e2e pass; tsc + bun run build + lint clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: cleanups from self-review on PR #236 Three minor, no-behavior-change tidies surfaced during /review: - lib/copilot-projects.ts: replace `open(path).stat().close()` with direct `stat(path)` from node:fs/promises in `statMtime`. Halves the FD pressure (was opening two handles per session-state dir for mtime alone) and removes the try/finally close dance. The codex-projects equivalent uses open() because it reuses the handle for readFirstLine; we don't, so the simpler form is cleaner. - lib/copilot-sessions.ts: rename `CopilotToolToolTelemetry` (typo) to `CopilotToolTelemetry`. - lib/copilot-sessions.ts: inline `getMs(date)` — the wrapper was just `date.getTime()` and added indirection at four call sites. 1165 unit + 227 e2e + tsc + bun run build all green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: use current GitHub Copilot mark with light/dark variants The previous copilot.svg used an older SimpleIcons path with a hard-coded fill="#000000", which rendered invisibly on GitHub's dark README theme. Replaced with the current canonical githubcopilot.svg path from simple-icons/simple-icons, split into copilot-light.svg and copilot-dark.svg, and switched the README block to <picture> with prefers-color-scheme — same pattern as the OpenAI logo. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: changelog entry for Copilot logo fix Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: address remaining CodeRabbit nits on PR #236 - project-list CLI filter onChange now validates via isKnownCli, mirroring the URL-read path on line 54-57. Defends against <option> values drifting out of sync with KNOWN_CLI_IDS. - resolve-permission-mode fallback comment broadened to cover the codex without-sessionId case in addition to copilot / unknown. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: address CodeRabbit follow-up review on PR #236 Four findings from the latest CodeRabbit pass: - lib/copilot-projects.ts: filter `hasTranscript` in getCopilotProjects and getCopilotSessionsByEncodedName so workspace-only sessions don't surface as /projects rows whose detail page would show an empty session list. Mirrors the filter already in metasToSessionFiles. - lib/copilot-sessions.ts: wrap readFile + parseCopilotLog in try/catch in getCopilotSessionLog. findCopilotTranscript only proves existence at lookup time; the file can be removed/rotated before the async readFile lands. Preserve the nullable contract instead of throwing into the session page. - lib/codex-projects.ts: switch fs/promises, os, path imports to the node:-prefixed form for Turbopack resolver consistency. Other per-CLI modules already use this form. - __tests__/e2e/hooks/copilot-integration.e2e.test.ts: build the JWT-shaped fixture string at runtime via base64url Buffer encodes instead of embedding a contiguous JWT literal in source. Avoids tripping secret scanners (and this repo's own sanitize-jwt detector) on test code. Each segment is 10+ base64url chars so it still matches JWT_RE in builtin-policies.ts. Tests: existing copilot-projects tests that previously asserted workspace-only sessions surface as /projects rows are updated to match the new behavior; new positive tests cover the filter and the read-race path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: silence one Turbopack NFT warning in next.config.ts Replace `readFileSync(join(__dirname, "package.json"))` with a static JSON import. Turbopack's Node File Tracer was flagging the runtime filesystem call as a "could touch the whole project" operation, producing one of two "Encountered unexpected file in NFT list" warnings on `bun run build`. Static `import pkg from "./package.json"` resolves at compile time so NFT no longer treats the config as dynamic. This drops the build warning count from 2 → 1; the remaining warning is a Turbopack quirk where `next.config.ts` shows up transitively in the NFT trace from server components and doesn't have a clean user-code fix. `bun run dev` is unaffected and starts clean. tsconfig already has `resolveJsonModule: true` so the static import type-checks without changes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: dogfood Copilot hooks via .github/hooks/failproofai.json Mirror the existing .claude/settings.json and .codex/hooks.json contributor configs by shipping a project-scope Copilot hooks file so devs working on this repo with the GitHub Copilot CLI get failproofai hooks active automatically — no `policies --install` step required. The file lives at .github/hooks/failproofai.json (Copilot's project-scope path; .copilot/ is the user-scope path) and uses Copilot's "VS Code compatible" form: `version: 1`, PascalCase event names, and `bash` + `powershell` + `timeoutSec` per entry. All six COPILOT_HOOK_EVENT_TYPES are wired (SessionStart, SessionEnd, UserPromptSubmit, PreToolUse, PostToolUse, Stop) to `bun bin/failproofai.mjs --hook <Event> --cli copilot`. Path is relative since Copilot — like Codex — spawns hooks with the project root as cwd and exposes no $COPILOT_PROJECT_DIR equivalent. CLAUDE.md gains a `### Copilot hooks` subsection documenting the path choice, the relative-vs-absolute caveat, and the recommended production-user install command. CHANGELOG's existing Unreleased Copilot Features bullet is extended to call out the dogfood config. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Mirrors the Codex (#220, #226, #232) and Copilot (#236) rollouts for Cursor Agent CLI. Hook install/uninstall, handler canonicalization, policy-evaluator output adapter, path-protection, activity dashboard filter + per-CLI badge, /projects merge, and session viewer fallback. Cursor's hook protocol differs from Claude/Codex/Copilot: - camelCase event keys (preToolUse, beforeSubmitPrompt, …) — handler canonicalizes via CURSOR_EVENT_MAP before policy lookup. - Flat array of hook entries per event (no Claude-style {hooks: [...]} matcher wrapper); writeHookEntries / removeHooksFromFile / hooksInstalledInSettings all walk the flat structure directly. - Stdout decision shape is {permission, user_message, agent_message, additional_context, followup_message} (not hookSpecificOutput). policy-evaluator.ts now branches on session.cli === "cursor" for the deny / instruct / allow-with-info paths and emits the Cursor shape. - No documented permission-mode equivalent; resolve-permission-mode.ts falls into the existing "default" branch (same as Copilot). - Detected via cursor-agent (preferred) or agent (legacy alias). Settings paths: user → ~/.cursor/hooks.json project → <cwd>/.cursor/hooks.json Path-protection (isAgentInternalPath + isAgentSettingsFile) extended to cover ~/.cursor/ and .cursor/hooks.json so the agent can't disable its own hooks. Frontend: lib/cli-registry.ts adds a "Cursor Agent" entry with an emerald badge; lib/projects.ts merges cursor projects; the project page (app/project/[name]/page.tsx) and session viewer (app/project/[name]/session/[sessionId]/page.tsx) extend the external-CLI fallback chain. The activity feed (hooks-client.tsx) recognizes /.cursor/ transcript paths. Adds lib/cursor-projects.ts + lib/cursor-sessions.ts as scaffold parsers — Cursor's transcript layout is undocumented, so the modules probe candidate subdirs (agent-sessions/, conversations/, sessions/) and metadata files (meta.json/session.json/workspace.{json,yaml}) and gracefully return [] when the directory layout doesn't match. The parser handles dotted-path record types (session.start, user.message, …) and degrades to system entries for unknown types so nothing is silently dropped — same shape as the Copilot parser. Drops the .cursor/hooks.json file at the project root (mirrors the existing .codex/hooks.json and .github/hooks/failproofai.json) so contributors developing failproofai with Cursor get hooks active automatically. Adjusts .gitignore to track that single file while leaving the rest of .cursor/ ignored. Tests: 60 unit suites / 1228 tests pass; 9 e2e suites / 235 tests pass (including the new __tests__/e2e/hooks/cursor-integration.e2e.test.ts which covers PreToolUse deny + agent-settings guard + beforeSubmitPrompt allow + activity tagging + install/uninstall flows). Bumps version to 0.0.10-beta.1. Also fixes a pre-existing parity gap: the project detail page (app/project/[name]/page.tsx) only listed Claude + Codex sessions, missing Copilot (and now Cursor); it now merges all four sources. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs: note GitHub Copilot CLI testing is ongoing Update the README beta callout to clarify that GitHub Copilot CLI support is still under active testing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [luv-245] feat: add Cursor Agent CLI integration (beta) Mirrors the Codex (#220, #226, #232) and Copilot (#236) rollouts for Cursor Agent CLI. Hook install/uninstall, handler canonicalization, policy-evaluator output adapter, path-protection, activity dashboard filter + per-CLI badge, /projects merge, and session viewer fallback. Cursor's hook protocol differs from Claude/Codex/Copilot: - camelCase event keys (preToolUse, beforeSubmitPrompt, …) — handler canonicalizes via CURSOR_EVENT_MAP before policy lookup. - Flat array of hook entries per event (no Claude-style {hooks: [...]} matcher wrapper); writeHookEntries / removeHooksFromFile / hooksInstalledInSettings all walk the flat structure directly. - Stdout decision shape is {permission, user_message, agent_message, additional_context, followup_message} (not hookSpecificOutput). policy-evaluator.ts now branches on session.cli === "cursor" for the deny / instruct / allow-with-info paths and emits the Cursor shape. - No documented permission-mode equivalent; resolve-permission-mode.ts falls into the existing "default" branch (same as Copilot). - Detected via cursor-agent (preferred) or agent (legacy alias). Settings paths: user → ~/.cursor/hooks.json project → <cwd>/.cursor/hooks.json Path-protection (isAgentInternalPath + isAgentSettingsFile) extended to cover ~/.cursor/ and .cursor/hooks.json so the agent can't disable its own hooks. Frontend: lib/cli-registry.ts adds a "Cursor Agent" entry with an emerald badge; lib/projects.ts merges cursor projects; the project page (app/project/[name]/page.tsx) and session viewer (app/project/[name]/session/[sessionId]/page.tsx) extend the external-CLI fallback chain. The activity feed (hooks-client.tsx) recognizes /.cursor/ transcript paths. Adds lib/cursor-projects.ts + lib/cursor-sessions.ts as scaffold parsers — Cursor's transcript layout is undocumented, so the modules probe candidate subdirs (agent-sessions/, conversations/, sessions/) and metadata files (meta.json/session.json/workspace.{json,yaml}) and gracefully return [] when the directory layout doesn't match. The parser handles dotted-path record types (session.start, user.message, …) and degrades to system entries for unknown types so nothing is silently dropped — same shape as the Copilot parser. Drops the .cursor/hooks.json file at the project root (mirrors the existing .codex/hooks.json and .github/hooks/failproofai.json) so contributors developing failproofai with Cursor get hooks active automatically. Adjusts .gitignore to track that single file while leaving the rest of .cursor/ ignored. Tests: 60 unit suites / 1228 tests pass; 9 e2e suites / 235 tests pass (including the new __tests__/e2e/hooks/cursor-integration.e2e.test.ts which covers PreToolUse deny + agent-settings guard + beforeSubmitPrompt allow + activity tagging + install/uninstall flows). Bumps version to 0.0.10-beta.1. Also fixes a pre-existing parity gap: the project detail page (app/project/[name]/page.tsx) only listed Claude + Codex sessions, missing Copilot (and now Cursor); it now merges all four sources. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [luv-245] fix: address CodeRabbit review feedback Five fixes from the bot review on PR #245: 1. .cursor/hooks.json: drop the `__failproofai_hook__` marker from the static dev config so the file matches Cursor's documented hook entry schema ({type, command, timeout}). The marker is still written by `cursor.buildHookEntry` for dynamically installed hooks; the legacy command-substring fallback in `isMarkedHook` identifies these static entries during uninstall. 2. CHANGELOG.md: fold the new Cursor entries into the existing Unreleased subsections (Features / Fixes / Docs) instead of creating a duplicate `### Features` heading. Resolves the MD024 markdownlint complaint. 3. lib/cursor-projects.ts: parse meta.json with `JSON.parse` first so escape sequences in JSON strings (Windows paths like `C:\\Users\\alice\\repo`) are decoded. Falls back to the YAML-ish regex only when the file isn't valid JSON. 4. lib/cursor-projects.ts: replace `findFirstExisting` (returns first existing path) with `findFirstUsableMeta` (returns first path whose parsed cwd is non-empty). Stops a stale `meta.json` from shadowing a valid `workspace.yaml` and dropping the session. 5. lib/cursor-sessions.ts: validate `data.content` / `data.text` with `typeof === "string"` before treating them as text in the user.message and assistant.message branches. Avoids surfacing non-string values via the `as string` casts. All 1228 unit + 235 e2e tests still pass. Lint clean (one pre-existing unrelated `<img>` warning). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Summary
Integrationadapter (src/hooks/integrations.ts). Codex's hook protocol is Claude-compatible by design — same stdin field names, same exit-code + JSON output, same{hooks: {EventName: [...]}}settings shape — so the runtime hot path needed only event-name canonicalization (snake_case → PascalCase).hookSpecificOutput.decision.behaviorshape per Codex docs), PostToolUse, UserPromptSubmit, Stop.block-sudonow also fires forPermissionRequestso Codex sandbox-escalation requests are guarded.~/.codex/hooks.json(user),<cwd>/.codex/hooks.json(project). Codex doesn't have alocalscope; install rejects--cli codex --scope localwith a friendly error.--cli claude codexflag onpolicies --install/--uninstalland--hook EVENT— space-separated multi-value (or repeatable--cli claude --cli codex). The parser only consumes known CLI names after--cli, so--cli claude block-sudocorrectly parses block-sudo as a positional policy name. Default behavior is interactive: detect installed CLIs (which claude/which codex) and prompt; auto-pick if only one is present; back-compat default ofclaudewhen no flag and no agent detected.src/hooks/resolve-permission-mode.tswalks~/.codex/sessions/<…>/<sessionId>.jsonlforturn_context.payload.approval_policyand mapsnever→full-auto,on-request→default.isAgentInternalPath/isAgentSettingsFilegeneralized to also cover~/.codex/and.codex/hooks.jsonso existing path-protection rules (block-read-outside-cwd, etc.) apply to Codex unchanged.Claude Code/OpenAI Codex).claude-code→claudeto match the actual CLI binary name (perclaude --help). User-facingdisplayNameis still "Claude Code".cdn.simpleicons.org/claudeandcdn.simpleicons.org/openai) plus a "+ more coming soon" line. CHANGELOG entry under## Unreleased > ### Features. Docs updated:configuration.mdx,introduction.mdx,getting-started.mdxall mention Codex +--cli..codex/hooks.jsonin this repo mirroring.claude/settings.json— all six Codex events under PascalCase keys with__failproofai_hook__marker. Codex doesn't define an env var equivalent to$CLAUDE_PROJECT_DIR, so the in-repo dev hooks use a relativebun bin/failproofai.mjspath; CLAUDE.md documents the constraint and the--cli codexinstall recipe for production users.Reference: closed multi-CLI mega-PR #185 — borrowed the
Integrationinterface shape, but trimmed to two impls (claude, codex) and addedPermissionRequestwhich #185 omitted. Other CLIs (Cursor, Copilot, Gemini, OpenCode, Pi) are easy to add later via the same adapter.Files: 18 modified + 5 new (
integrations.ts,resolve-permission-mode.ts,__tests__/hooks/integrations.test.ts,__tests__/e2e/hooks/codex-integration.e2e.test.ts,.codex/hooks.json).Test plan
bun run lint— clean (only the pre-existing<img>warning intool-input-output.tsx)bunx tsc --noEmit— cleanbun run test:run— 1066/1066 pass (24 new unit tests inintegrations.test.ts; existinghandler.test.tsupdated to assertcli/integrationfields are emitted in telemetry + activity entries)bun run test:e2e— 217/217 pass including 10 new Codex E2E tests covering: PreToolUse Bash deny, snake_casepre_tool_usecanonicalization, agent-settings-file path guard, PostToolUseadditionalContext, PermissionRequest deny shape, UserPromptSubmit allow, activity entry tagged withintegration: codex, settings file written to.codex/hooks.jsonwith PascalCase keys +version: 1,--scope localrejected with friendly error, uninstall idempotencebun run build— Next.js build succeeds, dist/index.js regenerated, prune-standalone runs clean🤖 Generated with Claude Code