failproofai 0.0.7 → 0.0.9-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/.next/standalone/.codex/hooks.json +77 -0
  2. package/.next/standalone/.next/BUILD_ID +1 -1
  3. package/.next/standalone/.next/build-manifest.json +3 -3
  4. package/.next/standalone/.next/prerender-manifest.json +3 -3
  5. package/.next/standalone/.next/required-server-files.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  14. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  15. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  20. package/.next/standalone/.next/server/app/_not-found.rsc +17 -17
  21. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +17 -17
  22. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  23. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +11 -11
  24. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  25. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  26. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  27. package/.next/standalone/.next/server/app/index.html +1 -1
  28. package/.next/standalone/.next/server/app/index.rsc +16 -16
  29. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  30. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +16 -16
  31. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  32. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +11 -11
  33. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  34. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  35. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  37. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  38. package/.next/standalone/.next/server/app/policies/page.js +1 -1
  39. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  42. package/.next/standalone/.next/server/app/project/[name]/page.js +1 -1
  43. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  46. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  51. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g72weg._.js +1 -1
  54. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_05pz9._._.js +1 -1
  55. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
  56. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0m72uj7._.js → [root-of-the-server]__03rd.z8._.js} +2 -2
  57. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +2 -2
  58. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09icjsf._.js +2 -2
  59. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0zn7uo6._.js → [root-of-the-server]__0ca1zru._.js} +2 -2
  60. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e74wa-._.js +3 -0
  61. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ea22pr._.js +3 -0
  62. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +2 -2
  63. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +2 -2
  64. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0okos0k._.js → [root-of-the-server]__0vu.o-3._.js} +3 -3
  65. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +7 -7
  66. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0of~riu._.js → [root-of-the-server]__0zqcovi._.js} +2 -2
  67. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +2 -2
  68. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +2 -2
  69. package/.next/standalone/.next/server/chunks/ssr/_07a1g.3._.js +3 -0
  70. package/.next/standalone/.next/server/chunks/ssr/_0uy6m~m._.js +3 -0
  71. package/.next/standalone/.next/server/chunks/ssr/_0zaq1hm._.js +3 -0
  72. package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
  73. package/.next/standalone/.next/server/chunks/ssr/_11rg2a_._.js +3 -0
  74. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +2 -2
  76. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0h9llsw._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_0mebn66._.js +1 -1
  78. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  79. package/.next/standalone/.next/server/pages/404.html +2 -2
  80. package/.next/standalone/.next/server/pages/500.html +1 -1
  81. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  82. package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
  83. package/.next/standalone/.next/static/chunks/0.rk1iwdt1d7c.css +1 -0
  84. package/.next/standalone/.next/static/chunks/00b5h4r1el.6f.js +1 -0
  85. package/.next/standalone/.next/static/chunks/{01l2mh88iy.ga.js → 03lsndql_yml5.js} +1 -1
  86. package/.next/standalone/.next/static/chunks/0amfi~vb_gfgo.js +1 -0
  87. package/.next/standalone/.next/static/chunks/0fw2h.g66c0h3.js +1 -0
  88. package/.next/standalone/.next/static/chunks/{0f_9854du76y2.js → 0jce49ygr4fdv.js} +1 -1
  89. package/.next/standalone/.next/static/chunks/{0388wpenm9-a4.js → 0mungg3~jpwe7.js} +1 -1
  90. package/.next/standalone/.next/static/chunks/{0x0o8~u4jsatb.js → 0uq_5p-p7myfe.js} +2 -2
  91. package/.next/standalone/.next/static/chunks/0v.xuf4ynzp~~.js +6 -0
  92. package/.next/standalone/.next/static/chunks/{0kkzzoo.s-t3p.js → 0vb8xxj_v2tz8.js} +1 -1
  93. package/.next/standalone/.next/static/chunks/{0vlk_pv4somht.js → 0vwqucikost_q.js} +1 -1
  94. package/.next/standalone/.next/static/chunks/0~mroziiwl1m5.js +1 -0
  95. package/.next/standalone/app/actions/install-hooks-web.ts +21 -5
  96. package/.next/standalone/app/policies/hooks-client.tsx +23 -0
  97. package/.next/standalone/assets/logos/claude.svg +1 -0
  98. package/.next/standalone/assets/logos/openai-dark.svg +1 -0
  99. package/.next/standalone/assets/logos/openai-light.svg +1 -0
  100. package/.next/standalone/package.json +2 -2
  101. package/.next/standalone/server.js +1 -1
  102. package/README.md +22 -3
  103. package/bin/failproofai.mjs +89 -9
  104. package/dist/cli.mjs +1040 -297
  105. package/package.json +2 -2
  106. package/src/hooks/builtin-policies.ts +39 -33
  107. package/src/hooks/handler.ts +39 -10
  108. package/src/hooks/hook-activity-store.ts +2 -0
  109. package/src/hooks/install-prompt.ts +69 -0
  110. package/src/hooks/integrations.ts +373 -0
  111. package/src/hooks/manager.ts +96 -171
  112. package/src/hooks/policy-evaluator.ts +28 -1
  113. package/src/hooks/policy-types.ts +3 -1
  114. package/src/hooks/resolve-permission-mode.ts +147 -0
  115. package/src/hooks/types.ts +30 -1
  116. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dj-tbi._.js +0 -3
  117. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0tjjyb9._.js +0 -3
  118. package/.next/standalone/.next/server/chunks/ssr/_0h21oar._.js +0 -3
  119. package/.next/standalone/.next/server/chunks/ssr/_0i~.gk_._.js +0 -3
  120. package/.next/standalone/.next/server/chunks/ssr/_0q3h.2s._.js +0 -3
  121. package/.next/standalone/.next/server/chunks/ssr/_0x..fj-._.js +0 -3
  122. package/.next/standalone/.next/static/chunks/0a0lh_a4f_xs-.js +0 -6
  123. package/.next/standalone/.next/static/chunks/0bkir2pd22ski.js +0 -1
  124. package/.next/standalone/.next/static/chunks/0j2o20pqkib~d.js +0 -1
  125. package/.next/standalone/.next/static/chunks/0ksdlt_1hucdm.js +0 -1
  126. package/.next/standalone/.next/static/chunks/0mir9jdxn35~s.css +0 -1
  127. package/.next/standalone/.next/static/chunks/12wu.28cbx4dl.js +0 -1
  128. /package/.next/standalone/.next/static/{9FNjQiktocMN-qDiGqDL5 → oUO8u4z9JvtTzS_2RJoGo}/_buildManifest.js +0 -0
  129. /package/.next/standalone/.next/static/{9FNjQiktocMN-qDiGqDL5 → oUO8u4z9JvtTzS_2RJoGo}/_clientMiddlewareManifest.js +0 -0
  130. /package/.next/standalone/.next/static/{9FNjQiktocMN-qDiGqDL5 → oUO8u4z9JvtTzS_2RJoGo}/_ssgManifest.js +0 -0
package/dist/cli.mjs CHANGED
@@ -15,6 +15,60 @@ var __export = (target, all) => {
15
15
  };
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
+ // src/hooks/types.ts
19
+ var HOOK_SCOPES, INTEGRATION_TYPES, CODEX_HOOK_SCOPES, CODEX_HOOK_EVENT_TYPES, CODEX_EVENT_MAP, HOOK_EVENT_TYPES, FAILPROOFAI_HOOK_MARKER = "__failproofai_hook__";
20
+ var init_types = __esm(() => {
21
+ HOOK_SCOPES = ["user", "project", "local"];
22
+ INTEGRATION_TYPES = ["claude", "codex"];
23
+ CODEX_HOOK_SCOPES = ["user", "project"];
24
+ CODEX_HOOK_EVENT_TYPES = [
25
+ "session_start",
26
+ "pre_tool_use",
27
+ "permission_request",
28
+ "post_tool_use",
29
+ "user_prompt_submit",
30
+ "stop"
31
+ ];
32
+ CODEX_EVENT_MAP = {
33
+ session_start: "SessionStart",
34
+ pre_tool_use: "PreToolUse",
35
+ permission_request: "PermissionRequest",
36
+ post_tool_use: "PostToolUse",
37
+ user_prompt_submit: "UserPromptSubmit",
38
+ stop: "Stop"
39
+ };
40
+ HOOK_EVENT_TYPES = [
41
+ "SessionStart",
42
+ "SessionEnd",
43
+ "UserPromptSubmit",
44
+ "PreToolUse",
45
+ "PermissionRequest",
46
+ "PermissionDenied",
47
+ "PostToolUse",
48
+ "PostToolUseFailure",
49
+ "Notification",
50
+ "SubagentStart",
51
+ "SubagentStop",
52
+ "TaskCreated",
53
+ "TaskCompleted",
54
+ "Stop",
55
+ "StopFailure",
56
+ "TeammateIdle",
57
+ "InstructionsLoaded",
58
+ "ConfigChange",
59
+ "CwdChanged",
60
+ "FileChanged",
61
+ "WorktreeCreate",
62
+ "WorktreeRemove",
63
+ "PreCompact",
64
+ "PostCompact",
65
+ "Elicitation",
66
+ "ElicitationResult",
67
+ "UserPromptExpansion",
68
+ "PostToolBatch"
69
+ ];
70
+ });
71
+
18
72
  // src/hooks/hook-logger.ts
19
73
  import {
20
74
  appendFileSync,
@@ -277,12 +331,20 @@ import { resolve as resolve2, join as join2 } from "node:path";
277
331
  import { readFile, writeFile } from "node:fs/promises";
278
332
  import { execSync, execFileSync } from "node:child_process";
279
333
  import { homedir as homedir3 } from "node:os";
280
- function isClaudeInternalPath(resolved2) {
281
- const claudeDir = join2(homedir3(), ".claude");
282
- return resolved2 === claudeDir || resolved2.startsWith(claudeDir + "/");
334
+ function isAgentInternalPath(resolved2) {
335
+ for (const dir of [".claude", ".codex"]) {
336
+ const root = join2(homedir3(), dir);
337
+ if (resolved2 === root || resolved2.startsWith(root + "/"))
338
+ return true;
339
+ }
340
+ return false;
283
341
  }
284
- function isClaudeSettingsFile(resolved2) {
285
- return /[\\/]\.claude[\\/]settings(?:\.[^/\\]+)?\.json$/.test(resolved2);
342
+ function isAgentSettingsFile(resolved2) {
343
+ if (/[\\/]\.claude[\\/]settings(?:\.[^/\\]+)?\.json$/.test(resolved2))
344
+ return true;
345
+ if (/[\\/]\.codex[\\/]hooks\.json$/.test(resolved2))
346
+ return true;
347
+ return false;
286
348
  }
287
349
  function getCommand(ctx) {
288
350
  return ctx.toolInput?.command ?? "";
@@ -1074,22 +1136,7 @@ function requirePrBeforeStop(ctx) {
1074
1136
  return allow(`PR #${pr.number} exists: ${pr.url}`);
1075
1137
  }
1076
1138
  if (pr.state === "MERGED") {
1077
- try {
1078
- execFileSync("git", ["fetch", "origin", `+refs/heads/${baseBranch}:refs/remotes/origin/${baseBranch}`], {
1079
- cwd,
1080
- encoding: "utf8",
1081
- stdio: ["pipe", "pipe", "pipe"],
1082
- timeout: 1e4
1083
- });
1084
- const freshAhead = execFileSync("git", ["log", `origin/${baseBranch}..HEAD`, "--oneline"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
1085
- if (!freshAhead) {
1086
- return allow(`PR #${pr.number} was merged; branch is up to date with ${baseBranch}.`);
1087
- }
1088
- const freshDiff = execFileSync("git", ["diff", "--stat", `origin/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
1089
- if (!freshDiff) {
1090
- return allow(`PR #${pr.number} was merged; no file changes vs ${baseBranch}.`);
1091
- }
1092
- } catch {}
1139
+ return allow(`PR #${pr.number} was merged: ${pr.url}. ` + `Switch off this branch (e.g. 'git checkout ${baseBranch} && git pull') before stopping again.`);
1093
1140
  }
1094
1141
  return deny(`Pull request for branch "${branch}" is ${pr.state.toLowerCase()}. Run now: gh pr create`);
1095
1142
  } catch {
@@ -1221,9 +1268,11 @@ function registerBuiltinPolicies(enabledNames) {
1221
1268
  }
1222
1269
  }
1223
1270
  }
1224
- var SHELL_OPERATORS, SHELL_METACHAR_RE, JWT_RE, API_KEY_PATTERNS, CONNECTION_STRING_RE, PRIVATE_KEY_RE, BEARER_TOKEN_RE, SQL_TOOL_RE, DESTRUCTIVE_SQL_RE, DELETE_NO_WHERE_RE, SQL_WHERE_RE, SCHEMA_ALTER_RE, PUBLISH_CMD_RE, ENV_PRINTENV_RE, ECHO_ENV_RE, EXPORT_RE, PS_ENV_VAR_RE, PS_CHILDITEM_ENV_RE, DOTNET_GETENV_RE, CMD_ECHO_ENV_RE, ENV_FILE_PATH_RE, ENV_CMD_RE, SUDO_RE, PS_ELEVATION_RE, RUNAS_RE, CURL_PIPE_SH_RE, PS_WEB_PIPE_RE, FORCE_PUSH_RE, SECRET_FILE_RE, SECRET_FILE_ID_RSA_RE, SECRET_FILE_CREDENTIALS_RE, GIT_COMMIT_MERGE_RE, FAILPROOFAI_CLI_RE, FAILPROOFAI_UNINSTALL_RE, GIT_AMEND_RE, GIT_STASH_DROP_RE, GIT_ADD_ALL_RE, NPM_GLOBAL_RE, YARN_GLOBAL_RE, PNPM_GLOBAL_RE, BUN_GLOBAL_RE, CARGO_INSTALL_RE, PIP_SYSTEM_RE, PKG_MANAGER_DETECTORS, NOHUP_RE, SCREEN_DETACH_RE, TMUX_DETACH_RE, DISOWN_RE, BACKGROUND_AMPERSAND_RE, KUBECTL_RE, TERRAFORM_RE, AWS_CLI_RE, GCLOUD_RE, AZ_CLI_RE, HELM_RE, GH_PIPELINE_RE, gitBranchCache, READ_LIKE_CMDS, TOOL_CALL_TRACKER_MAX_BYTES = 65536, SEGMENT_SPLIT_RE, BUILTIN_POLICIES;
1271
+ var isClaudeInternalPath, isClaudeSettingsFile, SHELL_OPERATORS, SHELL_METACHAR_RE, JWT_RE, API_KEY_PATTERNS, CONNECTION_STRING_RE, PRIVATE_KEY_RE, BEARER_TOKEN_RE, SQL_TOOL_RE, DESTRUCTIVE_SQL_RE, DELETE_NO_WHERE_RE, SQL_WHERE_RE, SCHEMA_ALTER_RE, PUBLISH_CMD_RE, ENV_PRINTENV_RE, ECHO_ENV_RE, EXPORT_RE, PS_ENV_VAR_RE, PS_CHILDITEM_ENV_RE, DOTNET_GETENV_RE, CMD_ECHO_ENV_RE, ENV_FILE_PATH_RE, ENV_CMD_RE, SUDO_RE, PS_ELEVATION_RE, RUNAS_RE, CURL_PIPE_SH_RE, PS_WEB_PIPE_RE, FORCE_PUSH_RE, SECRET_FILE_RE, SECRET_FILE_ID_RSA_RE, SECRET_FILE_CREDENTIALS_RE, GIT_COMMIT_MERGE_RE, FAILPROOFAI_CLI_RE, FAILPROOFAI_UNINSTALL_RE, GIT_AMEND_RE, GIT_STASH_DROP_RE, GIT_ADD_ALL_RE, NPM_GLOBAL_RE, YARN_GLOBAL_RE, PNPM_GLOBAL_RE, BUN_GLOBAL_RE, CARGO_INSTALL_RE, PIP_SYSTEM_RE, PKG_MANAGER_DETECTORS, NOHUP_RE, SCREEN_DETACH_RE, TMUX_DETACH_RE, DISOWN_RE, BACKGROUND_AMPERSAND_RE, KUBECTL_RE, TERRAFORM_RE, AWS_CLI_RE, GCLOUD_RE, AZ_CLI_RE, HELM_RE, GH_PIPELINE_RE, gitBranchCache, READ_LIKE_CMDS, TOOL_CALL_TRACKER_MAX_BYTES = 65536, SEGMENT_SPLIT_RE, BUILTIN_POLICIES;
1225
1272
  var init_builtin_policies = __esm(() => {
1226
1273
  init_hook_logger();
1274
+ isClaudeInternalPath = isAgentInternalPath;
1275
+ isClaudeSettingsFile = isAgentSettingsFile;
1227
1276
  SHELL_OPERATORS = new Set(["&&", "||", "|", ";"]);
1228
1277
  SHELL_METACHAR_RE = /[;&<>`$()\\]/;
1229
1278
  JWT_RE = /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/;
@@ -1387,7 +1436,7 @@ var init_builtin_policies = __esm(() => {
1387
1436
  name: "block-sudo",
1388
1437
  description: "Block sudo commands",
1389
1438
  fn: blockSudo,
1390
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1439
+ match: { events: ["PreToolUse", "PermissionRequest"], toolNames: ["Bash"] },
1391
1440
  defaultEnabled: true,
1392
1441
  category: "Dangerous Commands",
1393
1442
  params: {
@@ -1797,7 +1846,8 @@ async function evaluatePolicies(eventType, payload, session, config) {
1797
1846
  payload,
1798
1847
  toolName,
1799
1848
  toolInput,
1800
- session
1849
+ session,
1850
+ cli: session?.cli
1801
1851
  };
1802
1852
  const instructEntries = [];
1803
1853
  const allowEntries = [];
@@ -1842,6 +1892,25 @@ async function evaluatePolicies(eventType, payload, session, config) {
1842
1892
  decision: "deny"
1843
1893
  };
1844
1894
  }
1895
+ if (eventType === "PermissionRequest") {
1896
+ const response = {
1897
+ hookSpecificOutput: {
1898
+ hookEventName: eventType,
1899
+ decision: {
1900
+ behavior: "deny",
1901
+ message: `Blocked ${displayTool} by failproofai because: ${reason}, as per the policy configured by the user`
1902
+ }
1903
+ }
1904
+ };
1905
+ return {
1906
+ exitCode: 0,
1907
+ stdout: JSON.stringify(response),
1908
+ stderr: "",
1909
+ policyName: policy.name,
1910
+ reason,
1911
+ decision: "deny"
1912
+ };
1913
+ }
1845
1914
  if (eventType === "PostToolUse") {
1846
1915
  const response = {
1847
1916
  hookSpecificOutput: {
@@ -1926,7 +1995,7 @@ You MUST complete the above action(s) NOW. Do NOT ask the user for confirmation
1926
1995
  const combined = allowEntries.map((e) => e.reason).join(`
1927
1996
  `);
1928
1997
  const policyNames = allowEntries.map((e) => e.policyName);
1929
- const supportsHookSpecificOutput = eventType === "PreToolUse" || eventType === "PostToolUse" || eventType === "UserPromptSubmit";
1998
+ const supportsHookSpecificOutput = eventType === "PreToolUse" || eventType === "PostToolUse" || eventType === "UserPromptSubmit" || eventType === "PermissionRequest";
1930
1999
  const response = supportsHookSpecificOutput ? { hookSpecificOutput: { hookEventName: eventType, additionalContext: `Note from failproofai: ${combined}` } } : { reason: combined };
1931
2000
  const stderrMsg = allowEntries.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`).join(`
1932
2001
  `);
@@ -2328,7 +2397,7 @@ var init_hook_activity_store = __esm(() => {
2328
2397
  });
2329
2398
 
2330
2399
  // package.json
2331
- var version2 = "0.0.7";
2400
+ var version2 = "0.0.9-beta.0";
2332
2401
  var init_package = () => {};
2333
2402
 
2334
2403
  // src/posthog-key.ts
@@ -2359,6 +2428,118 @@ var init_hook_telemetry = __esm(() => {
2359
2428
  API_KEY = POSTHOG_API_KEY;
2360
2429
  });
2361
2430
 
2431
+ // src/hooks/resolve-permission-mode.ts
2432
+ import { readFileSync as readFileSync3, readdirSync as readdirSync3, existsSync as existsSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
2433
+ import { dirname as dirname3, join as join4 } from "node:path";
2434
+ import { homedir as homedir6 } from "node:os";
2435
+ function resolvePermissionMode(integration, parsed, sessionId) {
2436
+ if (integration === "claude") {
2437
+ return parsed.permission_mode ?? "default";
2438
+ }
2439
+ if (integration === "codex" && sessionId) {
2440
+ return resolveCodexMode(sessionId) ?? "default";
2441
+ }
2442
+ return "default";
2443
+ }
2444
+ function readCache() {
2445
+ try {
2446
+ if (!existsSync5(CACHE_PATH))
2447
+ return {};
2448
+ return JSON.parse(readFileSync3(CACHE_PATH, "utf-8"));
2449
+ } catch {
2450
+ return {};
2451
+ }
2452
+ }
2453
+ function writeCacheEntry(sessionId, path) {
2454
+ try {
2455
+ mkdirSync4(dirname3(CACHE_PATH), { recursive: true });
2456
+ const cache = readCache();
2457
+ cache[sessionId] = path;
2458
+ writeFileSync3(CACHE_PATH, JSON.stringify(cache), "utf-8");
2459
+ } catch {}
2460
+ }
2461
+ function dirSearch(dir, sessionId) {
2462
+ try {
2463
+ for (const f of readdirSync3(dir, { withFileTypes: true })) {
2464
+ if (f.isFile() && f.name.includes(sessionId) && f.name.endsWith(".jsonl")) {
2465
+ return join4(dir, f.name);
2466
+ }
2467
+ }
2468
+ } catch {}
2469
+ return null;
2470
+ }
2471
+ function findCodexTranscriptSync(sessionId) {
2472
+ const cache = readCache();
2473
+ const cached = cache[sessionId];
2474
+ if (cached && existsSync5(cached))
2475
+ return cached;
2476
+ const root = join4(homedir6(), ".codex", "sessions");
2477
+ const today = new Date;
2478
+ const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
2479
+ const datedDirs = [today, yesterday].map((d) => {
2480
+ const y = String(d.getUTCFullYear());
2481
+ const m = String(d.getUTCMonth() + 1).padStart(2, "0");
2482
+ const day = String(d.getUTCDate()).padStart(2, "0");
2483
+ return join4(root, y, m, day);
2484
+ });
2485
+ for (const dir of datedDirs) {
2486
+ const hit = dirSearch(dir, sessionId);
2487
+ if (hit) {
2488
+ writeCacheEntry(sessionId, hit);
2489
+ return hit;
2490
+ }
2491
+ }
2492
+ try {
2493
+ for (const y of readdirSync3(root, { withFileTypes: true })) {
2494
+ if (!y.isDirectory())
2495
+ continue;
2496
+ for (const m of readdirSync3(join4(root, y.name), { withFileTypes: true })) {
2497
+ if (!m.isDirectory())
2498
+ continue;
2499
+ for (const d of readdirSync3(join4(root, y.name, m.name), { withFileTypes: true })) {
2500
+ if (!d.isDirectory())
2501
+ continue;
2502
+ const hit = dirSearch(join4(root, y.name, m.name, d.name), sessionId);
2503
+ if (hit) {
2504
+ writeCacheEntry(sessionId, hit);
2505
+ return hit;
2506
+ }
2507
+ }
2508
+ }
2509
+ }
2510
+ } catch {}
2511
+ return null;
2512
+ }
2513
+ function resolveCodexMode(sessionId) {
2514
+ try {
2515
+ const path = findCodexTranscriptSync(sessionId);
2516
+ if (!path)
2517
+ return;
2518
+ for (const line of readFileSync3(path, "utf-8").split(`
2519
+ `)) {
2520
+ if (!line.includes("turn_context"))
2521
+ continue;
2522
+ try {
2523
+ const obj = JSON.parse(line);
2524
+ if (obj.type === "turn_context") {
2525
+ const policy = obj.payload?.approval_policy;
2526
+ if (policy === "never")
2527
+ return "full-auto";
2528
+ if (policy === "on-request")
2529
+ return "default";
2530
+ if (policy)
2531
+ return policy;
2532
+ }
2533
+ } catch {}
2534
+ }
2535
+ } catch {}
2536
+ return;
2537
+ }
2538
+ var CACHE_PATH;
2539
+ var init_resolve_permission_mode = __esm(() => {
2540
+ CACHE_PATH = join4(homedir6(), ".failproofai", "cache", "codex-session-paths.json");
2541
+ });
2542
+
2362
2543
  // lib/telemetry-id.ts
2363
2544
  import fs from "node:fs";
2364
2545
  import path from "node:path";
@@ -2442,26 +2623,26 @@ var init_telemetry_id = __esm(() => {
2442
2623
 
2443
2624
  // src/auth/token-store.ts
2444
2625
  import {
2445
- readFileSync as readFileSync3,
2446
- writeFileSync as writeFileSync3,
2447
- existsSync as existsSync5,
2448
- mkdirSync as mkdirSync4,
2626
+ readFileSync as readFileSync4,
2627
+ writeFileSync as writeFileSync4,
2628
+ existsSync as existsSync6,
2629
+ mkdirSync as mkdirSync5,
2449
2630
  unlinkSync as unlinkSync2,
2450
2631
  renameSync as renameSync3,
2451
2632
  openSync,
2452
2633
  closeSync
2453
2634
  } from "node:fs";
2454
- import { join as join4 } from "node:path";
2455
- import { homedir as homedir6 } from "node:os";
2635
+ import { join as join5 } from "node:path";
2636
+ import { homedir as homedir7 } from "node:os";
2456
2637
  function ensureAuthDir() {
2457
- if (!existsSync5(AUTH_DIR))
2458
- mkdirSync4(AUTH_DIR, { recursive: true, mode: 448 });
2638
+ if (!existsSync6(AUTH_DIR))
2639
+ mkdirSync5(AUTH_DIR, { recursive: true, mode: 448 });
2459
2640
  }
2460
2641
  function readTokens() {
2461
- if (!existsSync5(AUTH_FILE))
2642
+ if (!existsSync6(AUTH_FILE))
2462
2643
  return null;
2463
2644
  try {
2464
- const raw = readFileSync3(AUTH_FILE, "utf8");
2645
+ const raw = readFileSync4(AUTH_FILE, "utf8");
2465
2646
  return JSON.parse(raw);
2466
2647
  } catch {
2467
2648
  return null;
@@ -2472,23 +2653,23 @@ function writeTokens(tokens) {
2472
2653
  const tmpPath = `${AUTH_FILE}.tmp`;
2473
2654
  const fd = openSync(tmpPath, "w", 384);
2474
2655
  try {
2475
- writeFileSync3(fd, JSON.stringify(tokens, null, 2));
2656
+ writeFileSync4(fd, JSON.stringify(tokens, null, 2));
2476
2657
  } finally {
2477
2658
  closeSync(fd);
2478
2659
  }
2479
2660
  renameSync3(tmpPath, AUTH_FILE);
2480
2661
  }
2481
2662
  function clearTokens() {
2482
- if (existsSync5(AUTH_FILE))
2663
+ if (existsSync6(AUTH_FILE))
2483
2664
  unlinkSync2(AUTH_FILE);
2484
2665
  }
2485
2666
  function isLoggedIn() {
2486
- return existsSync5(AUTH_FILE);
2667
+ return existsSync6(AUTH_FILE);
2487
2668
  }
2488
2669
  var AUTH_DIR, AUTH_FILE;
2489
2670
  var init_token_store = __esm(() => {
2490
- AUTH_DIR = join4(homedir6(), ".failproofai");
2491
- AUTH_FILE = join4(AUTH_DIR, "auth.json");
2671
+ AUTH_DIR = join5(homedir7(), ".failproofai");
2672
+ AUTH_FILE = join5(AUTH_DIR, "auth.json");
2492
2673
  });
2493
2674
 
2494
2675
  // src/relay/queue.ts
@@ -2503,17 +2684,17 @@ __export(exports_queue, {
2503
2684
  });
2504
2685
  import {
2505
2686
  appendFileSync as appendFileSync3,
2506
- mkdirSync as mkdirSync5,
2507
- existsSync as existsSync6,
2508
- readFileSync as readFileSync4,
2687
+ mkdirSync as mkdirSync6,
2688
+ existsSync as existsSync7,
2689
+ readFileSync as readFileSync5,
2509
2690
  statSync as statSync4,
2510
2691
  renameSync as renameSync4,
2511
2692
  unlinkSync as unlinkSync3,
2512
- readdirSync as readdirSync3,
2693
+ readdirSync as readdirSync4,
2513
2694
  chmodSync
2514
2695
  } from "node:fs";
2515
- import { join as join5 } from "node:path";
2516
- import { homedir as homedir7 } from "node:os";
2696
+ import { join as join6 } from "node:path";
2697
+ import { homedir as homedir8 } from "node:os";
2517
2698
  import { createHash, randomUUID } from "node:crypto";
2518
2699
  function hashCwd(cwd) {
2519
2700
  if (!cwd)
@@ -2543,8 +2724,8 @@ function sanitize(entry) {
2543
2724
  };
2544
2725
  }
2545
2726
  function ensureDir2() {
2546
- if (!existsSync6(QUEUE_DIR)) {
2547
- mkdirSync5(QUEUE_DIR, { recursive: true, mode: 448 });
2727
+ if (!existsSync7(QUEUE_DIR)) {
2728
+ mkdirSync6(QUEUE_DIR, { recursive: true, mode: 448 });
2548
2729
  }
2549
2730
  }
2550
2731
  function appendToServerQueue(entry) {
@@ -2552,7 +2733,7 @@ function appendToServerQueue(entry) {
2552
2733
  return;
2553
2734
  ensureDir2();
2554
2735
  try {
2555
- if (existsSync6(PENDING_FILE) && statSync4(PENDING_FILE).size > MAX_QUEUE_BYTES) {
2736
+ if (existsSync7(PENDING_FILE) && statSync4(PENDING_FILE).size > MAX_QUEUE_BYTES) {
2556
2737
  return;
2557
2738
  }
2558
2739
  } catch {}
@@ -2571,7 +2752,7 @@ function queueSizeBytes() {
2571
2752
  }
2572
2753
  }
2573
2754
  function claimPendingBatch() {
2574
- if (!existsSync6(PENDING_FILE))
2755
+ if (!existsSync7(PENDING_FILE))
2575
2756
  return null;
2576
2757
  try {
2577
2758
  const size = statSync4(PENDING_FILE).size;
@@ -2581,7 +2762,7 @@ function claimPendingBatch() {
2581
2762
  return null;
2582
2763
  }
2583
2764
  const seq = `${Date.now()}-${process.pid}`;
2584
- const processingFile = join5(QUEUE_DIR, `${PROCESSING_PREFIX}${seq}.jsonl`);
2765
+ const processingFile = join6(QUEUE_DIR, `${PROCESSING_PREFIX}${seq}.jsonl`);
2585
2766
  try {
2586
2767
  renameSync4(PENDING_FILE, processingFile);
2587
2768
  try {
@@ -2598,15 +2779,15 @@ function claimPendingBatch() {
2598
2779
  function findOrphanProcessingFiles() {
2599
2780
  ensureDir2();
2600
2781
  try {
2601
- return readdirSync3(QUEUE_DIR).filter((n) => n.startsWith(PROCESSING_PREFIX) && n.endsWith(".jsonl")).map((n) => join5(QUEUE_DIR, n)).sort();
2782
+ return readdirSync4(QUEUE_DIR).filter((n) => n.startsWith(PROCESSING_PREFIX) && n.endsWith(".jsonl")).map((n) => join6(QUEUE_DIR, n)).sort();
2602
2783
  } catch {
2603
2784
  return [];
2604
2785
  }
2605
2786
  }
2606
2787
  function readProcessingFile(path2) {
2607
- if (!existsSync6(path2))
2788
+ if (!existsSync7(path2))
2608
2789
  return [];
2609
- const content = readFileSync4(path2, "utf8");
2790
+ const content = readFileSync5(path2, "utf8");
2610
2791
  const out = [];
2611
2792
  for (const line of content.split(`
2612
2793
  `)) {
@@ -2627,20 +2808,20 @@ function deleteProcessingFile(path2) {
2627
2808
  var QUEUE_DIR, PENDING_FILE, PROCESSING_PREFIX = "processing-", MAX_QUEUE_BYTES;
2628
2809
  var init_queue = __esm(() => {
2629
2810
  init_token_store();
2630
- QUEUE_DIR = join5(homedir7(), ".failproofai", "cache", "server-queue");
2631
- PENDING_FILE = join5(QUEUE_DIR, "pending.jsonl");
2811
+ QUEUE_DIR = join6(homedir8(), ".failproofai", "cache", "server-queue");
2812
+ PENDING_FILE = join6(QUEUE_DIR, "pending.jsonl");
2632
2813
  MAX_QUEUE_BYTES = 50 * 1024 * 1024;
2633
2814
  });
2634
2815
 
2635
2816
  // src/relay/pid.ts
2636
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync7, unlinkSync as unlinkSync4, mkdirSync as mkdirSync6 } from "node:fs";
2637
- import { join as join6, dirname as dirname3 } from "node:path";
2638
- import { homedir as homedir8 } from "node:os";
2817
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync8, unlinkSync as unlinkSync4, mkdirSync as mkdirSync7 } from "node:fs";
2818
+ import { join as join7, dirname as dirname4 } from "node:path";
2819
+ import { homedir as homedir9 } from "node:os";
2639
2820
  function readPid() {
2640
- if (!existsSync7(PID_FILE))
2821
+ if (!existsSync8(PID_FILE))
2641
2822
  return null;
2642
2823
  try {
2643
- const raw = readFileSync5(PID_FILE, "utf8").trim();
2824
+ const raw = readFileSync6(PID_FILE, "utf8").trim();
2644
2825
  const pid = parseInt(raw, 10);
2645
2826
  if (Number.isNaN(pid) || pid <= 0)
2646
2827
  return null;
@@ -2650,13 +2831,13 @@ function readPid() {
2650
2831
  }
2651
2832
  }
2652
2833
  function writePid(pid) {
2653
- const dir = dirname3(PID_FILE);
2654
- if (!existsSync7(dir))
2655
- mkdirSync6(dir, { recursive: true, mode: 448 });
2656
- writeFileSync4(PID_FILE, String(pid));
2834
+ const dir = dirname4(PID_FILE);
2835
+ if (!existsSync8(dir))
2836
+ mkdirSync7(dir, { recursive: true, mode: 448 });
2837
+ writeFileSync5(PID_FILE, String(pid));
2657
2838
  }
2658
2839
  function clearPid() {
2659
- if (existsSync7(PID_FILE))
2840
+ if (existsSync8(PID_FILE))
2660
2841
  unlinkSync4(PID_FILE);
2661
2842
  }
2662
2843
  function isProcessAlive(pid) {
@@ -2688,7 +2869,7 @@ function stopRelay() {
2688
2869
  }
2689
2870
  var PID_FILE;
2690
2871
  var init_pid = __esm(() => {
2691
- PID_FILE = join6(homedir8(), ".failproofai", "relay.pid");
2872
+ PID_FILE = join7(homedir9(), ".failproofai", "relay.pid");
2692
2873
  });
2693
2874
 
2694
2875
  // src/relay/daemon.ts
@@ -2700,9 +2881,9 @@ __export(exports_daemon, {
2700
2881
  ensureRelayRunning: () => ensureRelayRunning
2701
2882
  });
2702
2883
  import { spawn } from "node:child_process";
2703
- import { existsSync as existsSync8 } from "node:fs";
2704
- import { join as join7 } from "node:path";
2705
- import { homedir as homedir9 } from "node:os";
2884
+ import { existsSync as existsSync9 } from "node:fs";
2885
+ import { join as join8 } from "node:path";
2886
+ import { homedir as homedir10 } from "node:os";
2706
2887
  import { randomUUID as randomUUID2 } from "node:crypto";
2707
2888
  function ensureRelayRunning() {
2708
2889
  if (!isLoggedIn())
@@ -2919,7 +3100,7 @@ async function runDaemon() {
2919
3100
  }
2920
3101
  relay.close();
2921
3102
  } catch {}
2922
- if (existsSync8(QUEUE_DIR2)) {}
3103
+ if (existsSync9(QUEUE_DIR2)) {}
2923
3104
  await new Promise((r) => setTimeout(r, reconnectDelay));
2924
3105
  reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
2925
3106
  }
@@ -2969,7 +3150,7 @@ var init_daemon = __esm(() => {
2969
3150
  init_token_store();
2970
3151
  init_pid();
2971
3152
  init_queue();
2972
- QUEUE_DIR2 = join7(homedir9(), ".failproofai", "cache", "server-queue");
3153
+ QUEUE_DIR2 = join8(homedir10(), ".failproofai", "cache", "server-queue");
2973
3154
  });
2974
3155
 
2975
3156
  // src/hooks/handler.ts
@@ -2977,7 +3158,15 @@ var exports_handler = {};
2977
3158
  __export(exports_handler, {
2978
3159
  handleHookEvent: () => handleHookEvent
2979
3160
  });
2980
- async function handleHookEvent(eventType) {
3161
+ function canonicalizeEventType(raw, cli) {
3162
+ if (cli === "codex") {
3163
+ const mapped = CODEX_EVENT_MAP[raw];
3164
+ if (mapped)
3165
+ return mapped;
3166
+ }
3167
+ return raw;
3168
+ }
3169
+ async function handleHookEvent(eventType, cli = "claude") {
2981
3170
  const startTime = performance.now();
2982
3171
  const MAX_STDIN_BYTES = 1048576;
2983
3172
  let payload = "";
@@ -3012,12 +3201,15 @@ async function handleHookEvent(eventType) {
3012
3201
  hookLogWarn(`payload parse failed for ${eventType} (${payload.length} bytes)`);
3013
3202
  }
3014
3203
  }
3204
+ const canonicalEventType = canonicalizeEventType(eventType, cli);
3205
+ const sessionId = parsed.session_id;
3015
3206
  const session = {
3016
- sessionId: parsed.session_id,
3207
+ sessionId,
3017
3208
  transcriptPath: parsed.transcript_path,
3018
3209
  cwd: parsed.cwd,
3019
- permissionMode: parsed.permission_mode,
3020
- hookEventName: parsed.hook_event_name
3210
+ permissionMode: resolvePermissionMode(cli, parsed, sessionId),
3211
+ hookEventName: parsed.hook_event_name,
3212
+ cli
3021
3213
  };
3022
3214
  const config = readMergedHooksConfig(session.cwd);
3023
3215
  clearPolicies();
@@ -3045,6 +3237,7 @@ async function handleHookEvent(eventType) {
3045
3237
  hook_name: hookName,
3046
3238
  error_type: isTimeout ? "timeout" : "exception",
3047
3239
  event_type: eventType,
3240
+ cli,
3048
3241
  is_convention_policy: isConvention,
3049
3242
  convention_scope: conventionScope ?? null
3050
3243
  });
@@ -3055,6 +3248,7 @@ async function handleHookEvent(eventType) {
3055
3248
  }
3056
3249
  if (customHooksList.length > 0) {
3057
3250
  trackHookEvent(getInstanceId(), "custom_hooks_loaded", {
3251
+ cli,
3058
3252
  custom_hooks_count: customHooksList.length,
3059
3253
  custom_hook_names: customHooksList.map((h) => h.name),
3060
3254
  event_types_covered: [...new Set(customHooksList.flatMap((h) => h.match?.events ?? []))]
@@ -3062,15 +3256,16 @@ async function handleHookEvent(eventType) {
3062
3256
  }
3063
3257
  if (loadResult.conventionSources.length > 0) {
3064
3258
  trackHookEvent(getInstanceId(), "convention_policies_loaded", {
3065
- event_type: eventType,
3259
+ event_type: canonicalEventType,
3260
+ cli,
3066
3261
  project_file_count: loadResult.conventionSources.filter((s) => s.scope === "project").length,
3067
3262
  user_file_count: loadResult.conventionSources.filter((s) => s.scope === "user").length,
3068
3263
  convention_hook_count: conventionHookNames.size,
3069
3264
  convention_hook_names: [...conventionHookNames]
3070
3265
  });
3071
3266
  }
3072
- hookLogInfo(`event=${eventType} policies=${config.enabledPolicies.length} custom=${customHooksList.length} convention=${conventionHookNames.size}`);
3073
- const result = await evaluatePolicies(eventType, parsed, session, config);
3267
+ hookLogInfo(`event=${canonicalEventType} cli=${cli} policies=${config.enabledPolicies.length} custom=${customHooksList.length} convention=${conventionHookNames.size}`);
3268
+ const result = await evaluatePolicies(canonicalEventType, parsed, session, config);
3074
3269
  const durationMs = Math.round(performance.now() - startTime);
3075
3270
  hookLogInfo(`result=${result.decision} policy=${result.policyName ?? "none"} duration=${durationMs}ms`);
3076
3271
  if (result.stdout) {
@@ -3081,7 +3276,8 @@ async function handleHookEvent(eventType) {
3081
3276
  }
3082
3277
  const activityEntry = {
3083
3278
  timestamp: Date.now(),
3084
- eventType,
3279
+ eventType: canonicalEventType,
3280
+ integration: cli,
3085
3281
  toolName: parsed.tool_name ?? null,
3086
3282
  policyName: result.policyName,
3087
3283
  policyNames: result.policyNames,
@@ -3116,7 +3312,8 @@ async function handleHookEvent(eventType) {
3116
3312
  const paramKeysOverridden = hasCustomParams ? Object.keys(config.policyParams[result.policyName]) : [];
3117
3313
  const distinctId = getInstanceId();
3118
3314
  await trackHookEvent(distinctId, "hook_policy_triggered", {
3119
- event_type: eventType,
3315
+ event_type: canonicalEventType,
3316
+ cli,
3120
3317
  tool_name: parsed.tool_name ?? null,
3121
3318
  policy_name: result.policyName,
3122
3319
  decision: result.decision,
@@ -3131,12 +3328,14 @@ async function handleHookEvent(eventType) {
3131
3328
  return result.exitCode;
3132
3329
  }
3133
3330
  var init_handler = __esm(() => {
3331
+ init_types();
3134
3332
  init_hooks_config();
3135
3333
  init_builtin_policies();
3136
3334
  init_policy_evaluator();
3137
3335
  init_custom_hooks_loader();
3138
3336
  init_hook_activity_store();
3139
3337
  init_hook_telemetry();
3338
+ init_resolve_permission_mode();
3140
3339
  init_telemetry_id();
3141
3340
  init_hook_logger();
3142
3341
  });
@@ -3150,9 +3349,9 @@ __export(exports_daemon2, {
3150
3349
  ensureRelayRunning: () => ensureRelayRunning2
3151
3350
  });
3152
3351
  import { spawn as spawn2 } from "node:child_process";
3153
- import { existsSync as existsSync9 } from "node:fs";
3154
- import { join as join8 } from "node:path";
3155
- import { homedir as homedir10 } from "node:os";
3352
+ import { existsSync as existsSync10 } from "node:fs";
3353
+ import { join as join9 } from "node:path";
3354
+ import { homedir as homedir11 } from "node:os";
3156
3355
  import { randomUUID as randomUUID3 } from "node:crypto";
3157
3356
  function ensureRelayRunning2() {
3158
3357
  if (!isLoggedIn())
@@ -3369,7 +3568,7 @@ async function runDaemon2() {
3369
3568
  }
3370
3569
  relay.close();
3371
3570
  } catch {}
3372
- if (existsSync9(QUEUE_DIR3)) {}
3571
+ if (existsSync10(QUEUE_DIR3)) {}
3373
3572
  await new Promise((r) => setTimeout(r, reconnectDelay));
3374
3573
  reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS2);
3375
3574
  }
@@ -3419,41 +3618,279 @@ var init_daemon2 = __esm(() => {
3419
3618
  init_token_store();
3420
3619
  init_pid();
3421
3620
  init_queue();
3422
- QUEUE_DIR3 = join8(homedir10(), ".failproofai", "cache", "server-queue");
3621
+ QUEUE_DIR3 = join9(homedir11(), ".failproofai", "cache", "server-queue");
3423
3622
  });
3424
3623
 
3425
- // src/hooks/types.ts
3426
- var HOOK_SCOPES, HOOK_EVENT_TYPES, FAILPROOFAI_HOOK_MARKER = "__failproofai_hook__";
3427
- var init_types = __esm(() => {
3428
- HOOK_SCOPES = ["user", "project", "local"];
3429
- HOOK_EVENT_TYPES = [
3430
- "SessionStart",
3431
- "SessionEnd",
3432
- "UserPromptSubmit",
3433
- "PreToolUse",
3434
- "PermissionRequest",
3435
- "PermissionDenied",
3436
- "PostToolUse",
3437
- "PostToolUseFailure",
3438
- "Notification",
3439
- "SubagentStart",
3440
- "SubagentStop",
3441
- "TaskCreated",
3442
- "TaskCompleted",
3443
- "Stop",
3444
- "StopFailure",
3445
- "TeammateIdle",
3446
- "InstructionsLoaded",
3447
- "ConfigChange",
3448
- "CwdChanged",
3449
- "FileChanged",
3450
- "WorktreeCreate",
3451
- "WorktreeRemove",
3452
- "PreCompact",
3453
- "PostCompact",
3454
- "Elicitation",
3455
- "ElicitationResult"
3456
- ];
3624
+ // src/hooks/integrations.ts
3625
+ import { execSync as execSync3 } from "node:child_process";
3626
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync11, mkdirSync as mkdirSync8 } from "node:fs";
3627
+ import { resolve as resolve5, dirname as dirname5 } from "node:path";
3628
+ import { homedir as homedir12 } from "node:os";
3629
+ function readJsonFile(path2) {
3630
+ if (!existsSync11(path2))
3631
+ return {};
3632
+ const raw = readFileSync7(path2, "utf8");
3633
+ return JSON.parse(raw);
3634
+ }
3635
+ function writeJsonFile(path2, data) {
3636
+ mkdirSync8(dirname5(path2), { recursive: true });
3637
+ writeFileSync6(path2, JSON.stringify(data, null, 2) + `
3638
+ `, "utf8");
3639
+ }
3640
+ function isMarkedHook(hook) {
3641
+ if (hook[FAILPROOFAI_HOOK_MARKER] === true)
3642
+ return true;
3643
+ const cmd = typeof hook.command === "string" ? hook.command : "";
3644
+ return cmd.includes("failproofai") && cmd.includes("--hook");
3645
+ }
3646
+ function binaryExists(name) {
3647
+ try {
3648
+ const cmd = process.platform === "win32" ? `where ${name}` : `which ${name}`;
3649
+ execSync3(cmd, { encoding: "utf8", stdio: "pipe" });
3650
+ return true;
3651
+ } catch {
3652
+ return false;
3653
+ }
3654
+ }
3655
+ function getIntegration(id) {
3656
+ const integration = INTEGRATIONS[id];
3657
+ if (!integration) {
3658
+ throw new Error(`Unknown integration: ${id}. Valid: ${INTEGRATION_TYPES.join(", ")}`);
3659
+ }
3660
+ return integration;
3661
+ }
3662
+ function detectInstalledClis() {
3663
+ return INTEGRATION_TYPES.filter((id) => INTEGRATIONS[id].detectInstalled());
3664
+ }
3665
+ var claudeCode, codex, INTEGRATIONS;
3666
+ var init_integrations = __esm(() => {
3667
+ init_types();
3668
+ claudeCode = {
3669
+ id: "claude",
3670
+ displayName: "Claude Code",
3671
+ scopes: HOOK_SCOPES,
3672
+ eventTypes: HOOK_EVENT_TYPES,
3673
+ getSettingsPath(scope, cwd) {
3674
+ const base = cwd ? resolve5(cwd) : process.cwd();
3675
+ switch (scope) {
3676
+ case "user":
3677
+ return resolve5(homedir12(), ".claude", "settings.json");
3678
+ case "project":
3679
+ return resolve5(base, ".claude", "settings.json");
3680
+ case "local":
3681
+ return resolve5(base, ".claude", "settings.local.json");
3682
+ }
3683
+ },
3684
+ readSettings(settingsPath) {
3685
+ return readJsonFile(settingsPath);
3686
+ },
3687
+ writeSettings(settingsPath, settings) {
3688
+ writeJsonFile(settingsPath, settings);
3689
+ },
3690
+ buildHookEntry(binaryPath, eventType, scope) {
3691
+ const command = scope === "project" ? `npx -y failproofai --hook ${eventType}` : `"${binaryPath}" --hook ${eventType}`;
3692
+ return {
3693
+ type: "command",
3694
+ command,
3695
+ timeout: 60000,
3696
+ [FAILPROOFAI_HOOK_MARKER]: true
3697
+ };
3698
+ },
3699
+ isFailproofaiHook: isMarkedHook,
3700
+ writeHookEntries(settings, binaryPath, scope) {
3701
+ const s = settings;
3702
+ if (!s.hooks)
3703
+ s.hooks = {};
3704
+ for (const eventType of HOOK_EVENT_TYPES) {
3705
+ const hookEntry = this.buildHookEntry(binaryPath, eventType, scope);
3706
+ if (!s.hooks[eventType])
3707
+ s.hooks[eventType] = [];
3708
+ const matchers = s.hooks[eventType];
3709
+ let found = false;
3710
+ for (const matcher of matchers) {
3711
+ if (!matcher.hooks)
3712
+ continue;
3713
+ const idx = matcher.hooks.findIndex((h) => isMarkedHook(h));
3714
+ if (idx >= 0) {
3715
+ matcher.hooks[idx] = hookEntry;
3716
+ found = true;
3717
+ break;
3718
+ }
3719
+ }
3720
+ if (!found)
3721
+ matchers.push({ hooks: [hookEntry] });
3722
+ }
3723
+ },
3724
+ removeHooksFromFile(settingsPath) {
3725
+ const settings = this.readSettings(settingsPath);
3726
+ if (!settings.hooks)
3727
+ return 0;
3728
+ let removed = 0;
3729
+ for (const eventType of Object.keys(settings.hooks)) {
3730
+ const matchers = settings.hooks[eventType];
3731
+ if (!Array.isArray(matchers))
3732
+ continue;
3733
+ for (let i = matchers.length - 1;i >= 0; i--) {
3734
+ const matcher = matchers[i];
3735
+ if (!matcher.hooks)
3736
+ continue;
3737
+ const before = matcher.hooks.length;
3738
+ matcher.hooks = matcher.hooks.filter((h) => !isMarkedHook(h));
3739
+ removed += before - matcher.hooks.length;
3740
+ if (matcher.hooks.length === 0)
3741
+ matchers.splice(i, 1);
3742
+ }
3743
+ if (matchers.length === 0)
3744
+ delete settings.hooks[eventType];
3745
+ }
3746
+ if (Object.keys(settings.hooks).length === 0)
3747
+ delete settings.hooks;
3748
+ this.writeSettings(settingsPath, settings);
3749
+ return removed;
3750
+ },
3751
+ hooksInstalledInSettings(scope, cwd) {
3752
+ const settingsPath = this.getSettingsPath(scope, cwd);
3753
+ if (!existsSync11(settingsPath))
3754
+ return false;
3755
+ try {
3756
+ const settings = this.readSettings(settingsPath);
3757
+ if (!settings.hooks)
3758
+ return false;
3759
+ for (const matchers of Object.values(settings.hooks)) {
3760
+ if (!Array.isArray(matchers))
3761
+ continue;
3762
+ for (const matcher of matchers) {
3763
+ if (!matcher.hooks)
3764
+ continue;
3765
+ if (matcher.hooks.some((h) => isMarkedHook(h)))
3766
+ return true;
3767
+ }
3768
+ }
3769
+ } catch {}
3770
+ return false;
3771
+ },
3772
+ detectInstalled() {
3773
+ return binaryExists("claude") || binaryExists("claude-code");
3774
+ }
3775
+ };
3776
+ codex = {
3777
+ id: "codex",
3778
+ displayName: "OpenAI Codex",
3779
+ scopes: CODEX_HOOK_SCOPES,
3780
+ eventTypes: CODEX_HOOK_EVENT_TYPES,
3781
+ getSettingsPath(scope, cwd) {
3782
+ const base = cwd ? resolve5(cwd) : process.cwd();
3783
+ switch (scope) {
3784
+ case "user":
3785
+ return resolve5(homedir12(), ".codex", "hooks.json");
3786
+ case "project":
3787
+ return resolve5(base, ".codex", "hooks.json");
3788
+ case "local":
3789
+ return resolve5(base, ".codex", "hooks.json");
3790
+ }
3791
+ },
3792
+ readSettings(settingsPath) {
3793
+ const raw = readJsonFile(settingsPath);
3794
+ if (raw.version === undefined)
3795
+ raw.version = 1;
3796
+ return raw;
3797
+ },
3798
+ writeSettings(settingsPath, settings) {
3799
+ writeJsonFile(settingsPath, settings);
3800
+ },
3801
+ buildHookEntry(binaryPath, eventType, scope) {
3802
+ const command = scope === "project" ? `npx -y failproofai --hook ${eventType} --cli codex` : `"${binaryPath}" --hook ${eventType} --cli codex`;
3803
+ return {
3804
+ type: "command",
3805
+ command,
3806
+ timeout: 60000,
3807
+ [FAILPROOFAI_HOOK_MARKER]: true
3808
+ };
3809
+ },
3810
+ isFailproofaiHook: isMarkedHook,
3811
+ writeHookEntries(settings, binaryPath, scope) {
3812
+ const s = settings;
3813
+ if (s.version === undefined)
3814
+ s.version = 1;
3815
+ if (!s.hooks)
3816
+ s.hooks = {};
3817
+ for (const eventType of CODEX_HOOK_EVENT_TYPES) {
3818
+ const pascalKey = CODEX_EVENT_MAP[eventType];
3819
+ const hookEntry = this.buildHookEntry(binaryPath, eventType, scope);
3820
+ if (!s.hooks[pascalKey])
3821
+ s.hooks[pascalKey] = [];
3822
+ const matchers = s.hooks[pascalKey];
3823
+ let found = false;
3824
+ for (const matcher of matchers) {
3825
+ if (!matcher.hooks)
3826
+ continue;
3827
+ const idx = matcher.hooks.findIndex((h) => isMarkedHook(h));
3828
+ if (idx >= 0) {
3829
+ matcher.hooks[idx] = hookEntry;
3830
+ found = true;
3831
+ break;
3832
+ }
3833
+ }
3834
+ if (!found)
3835
+ matchers.push({ hooks: [hookEntry] });
3836
+ }
3837
+ },
3838
+ removeHooksFromFile(settingsPath) {
3839
+ const settings = this.readSettings(settingsPath);
3840
+ if (!settings.hooks)
3841
+ return 0;
3842
+ let removed = 0;
3843
+ for (const eventType of Object.keys(settings.hooks)) {
3844
+ const matchers = settings.hooks[eventType];
3845
+ if (!Array.isArray(matchers))
3846
+ continue;
3847
+ for (let i = matchers.length - 1;i >= 0; i--) {
3848
+ const matcher = matchers[i];
3849
+ if (!matcher.hooks)
3850
+ continue;
3851
+ const before = matcher.hooks.length;
3852
+ matcher.hooks = matcher.hooks.filter((h) => !isMarkedHook(h));
3853
+ removed += before - matcher.hooks.length;
3854
+ if (matcher.hooks.length === 0)
3855
+ matchers.splice(i, 1);
3856
+ }
3857
+ if (matchers.length === 0)
3858
+ delete settings.hooks[eventType];
3859
+ }
3860
+ if (Object.keys(settings.hooks).length === 0)
3861
+ delete settings.hooks;
3862
+ this.writeSettings(settingsPath, settings);
3863
+ return removed;
3864
+ },
3865
+ hooksInstalledInSettings(scope, cwd) {
3866
+ const settingsPath = this.getSettingsPath(scope, cwd);
3867
+ if (!existsSync11(settingsPath))
3868
+ return false;
3869
+ try {
3870
+ const settings = this.readSettings(settingsPath);
3871
+ if (!settings.hooks)
3872
+ return false;
3873
+ for (const matchers of Object.values(settings.hooks)) {
3874
+ if (!Array.isArray(matchers))
3875
+ continue;
3876
+ for (const matcher of matchers) {
3877
+ if (!matcher.hooks)
3878
+ continue;
3879
+ if (matcher.hooks.some((h) => isMarkedHook(h)))
3880
+ return true;
3881
+ }
3882
+ }
3883
+ } catch {}
3884
+ return false;
3885
+ },
3886
+ detectInstalled() {
3887
+ return binaryExists("codex");
3888
+ }
3889
+ };
3890
+ INTEGRATIONS = {
3891
+ claude: claudeCode,
3892
+ codex
3893
+ };
3457
3894
  });
3458
3895
 
3459
3896
  // src/hooks/install-prompt.ts
@@ -3644,7 +4081,7 @@ async function promptPolicySelection(preSelected, options = {}) {
3644
4081
  process.stdout.write(output);
3645
4082
  lastLineCount = lines.length;
3646
4083
  }
3647
- return new Promise((resolve5) => {
4084
+ return new Promise((resolve6) => {
3648
4085
  render();
3649
4086
  process.stdin.setRawMode(true);
3650
4087
  process.stdin.resume();
@@ -3686,7 +4123,7 @@ async function promptPolicySelection(preSelected, options = {}) {
3686
4123
  const selected = items.filter((i) => i.selected).map((i) => i.name);
3687
4124
  process.stdout.write(`
3688
4125
  `);
3689
- resolve5(selected);
4126
+ resolve6(selected);
3690
4127
  } else if (key.name === "backspace" || key.name === "delete") {
3691
4128
  if (search.length > 0) {
3692
4129
  search = search.slice(0, -1);
@@ -3710,6 +4147,7 @@ async function promptPolicySelection(preSelected, options = {}) {
3710
4147
  }
3711
4148
  var init_install_prompt = __esm(() => {
3712
4149
  init_builtin_policies();
4150
+ init_integrations();
3713
4151
  });
3714
4152
 
3715
4153
  // src/cli-error.ts
@@ -3734,20 +4172,12 @@ __export(exports_manager, {
3734
4172
  hooksInstalledInSettings: () => hooksInstalledInSettings,
3735
4173
  getSettingsPath: () => getSettingsPath
3736
4174
  });
3737
- import { execSync as execSync3 } from "node:child_process";
3738
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync10, mkdirSync as mkdirSync7 } from "node:fs";
3739
- import { resolve as resolve5, dirname as dirname4, basename as basename2 } from "node:path";
3740
- import { homedir as homedir11, platform, arch, release, hostname } from "node:os";
4175
+ import { execSync as execSync4 } from "node:child_process";
4176
+ import { existsSync as existsSync12 } from "node:fs";
4177
+ import { resolve as resolve6, basename as basename2 } from "node:path";
4178
+ import { homedir as homedir13, platform, arch, release, hostname } from "node:os";
3741
4179
  function getSettingsPath(scope, cwd) {
3742
- const base = cwd ? resolve5(cwd) : process.cwd();
3743
- switch (scope) {
3744
- case "user":
3745
- return resolve5(homedir11(), ".claude", "settings.json");
3746
- case "project":
3747
- return resolve5(base, ".claude", "settings.json");
3748
- case "local":
3749
- return resolve5(base, ".claude", "settings.local.json");
3750
- }
4180
+ return claudeCode.getSettingsPath(scope, cwd);
3751
4181
  }
3752
4182
  function scopeLabel(scope) {
3753
4183
  switch (scope) {
@@ -3759,22 +4189,13 @@ function scopeLabel(scope) {
3759
4189
  return `{cwd}/.claude/settings.local.json`;
3760
4190
  }
3761
4191
  }
3762
- function readSettings(settingsPath) {
3763
- if (!existsSync10(settingsPath)) {
3764
- return {};
3765
- }
3766
- const raw = readFileSync6(settingsPath, "utf8");
3767
- return JSON.parse(raw);
3768
- }
3769
- function writeSettings(settingsPath, settings) {
3770
- mkdirSync7(dirname4(settingsPath), { recursive: true });
3771
- writeFileSync5(settingsPath, JSON.stringify(settings, null, 2) + `
3772
- `, "utf8");
3773
- }
3774
4192
  function resolveFailproofaiBinary() {
4193
+ const override = process.env.FAILPROOFAI_BINARY_OVERRIDE;
4194
+ if (override && override.trim())
4195
+ return override.trim();
3775
4196
  try {
3776
4197
  const cmd = process.platform === "win32" ? "where failproofai" : "which failproofai";
3777
- const result = execSync3(cmd, { encoding: "utf8" }).trim();
4198
+ const result = execSync4(cmd, { encoding: "utf8" }).trim();
3778
4199
  return result.split(`
3779
4200
  `)[0].trim();
3780
4201
  } catch {
@@ -3782,12 +4203,6 @@ function resolveFailproofaiBinary() {
3782
4203
  ` + "Install it globally first: npm install -g failproofai");
3783
4204
  }
3784
4205
  }
3785
- function isFailproofaiHook(hook) {
3786
- if (hook[FAILPROOFAI_HOOK_MARKER] === true)
3787
- return true;
3788
- const cmd = typeof hook.command === "string" ? hook.command : "";
3789
- return cmd.includes("failproofai") && cmd.includes("--hook");
3790
- }
3791
4206
  function validatePolicyNames(names) {
3792
4207
  const invalid = names.filter((n) => !VALID_POLICY_NAMES.has(n));
3793
4208
  if (invalid.length > 0) {
@@ -3807,58 +4222,9 @@ function deduplicateScopes(scopes, cwd) {
3807
4222
  });
3808
4223
  }
3809
4224
  function hooksInstalledInSettings(scope, cwd) {
3810
- const settingsPath = getSettingsPath(scope, cwd);
3811
- if (!existsSync10(settingsPath))
3812
- return false;
3813
- try {
3814
- const settings = readSettings(settingsPath);
3815
- if (!settings.hooks)
3816
- return false;
3817
- for (const matchers of Object.values(settings.hooks)) {
3818
- if (!Array.isArray(matchers))
3819
- continue;
3820
- for (const matcher of matchers) {
3821
- if (!matcher.hooks)
3822
- continue;
3823
- if (matcher.hooks.some((h) => isFailproofaiHook(h))) {
3824
- return true;
3825
- }
3826
- }
3827
- }
3828
- } catch {}
3829
- return false;
3830
- }
3831
- function removeHooksFromSettingsFile(settingsPath) {
3832
- const settings = readSettings(settingsPath);
3833
- if (!settings.hooks)
3834
- return 0;
3835
- let removed = 0;
3836
- for (const eventType of Object.keys(settings.hooks)) {
3837
- const matchers = settings.hooks[eventType];
3838
- if (!Array.isArray(matchers))
3839
- continue;
3840
- for (let i = matchers.length - 1;i >= 0; i--) {
3841
- const matcher = matchers[i];
3842
- if (!matcher.hooks)
3843
- continue;
3844
- const before = matcher.hooks.length;
3845
- matcher.hooks = matcher.hooks.filter((h) => !isFailproofaiHook(h));
3846
- removed += before - matcher.hooks.length;
3847
- if (matcher.hooks.length === 0) {
3848
- matchers.splice(i, 1);
3849
- }
3850
- }
3851
- if (matchers.length === 0) {
3852
- delete settings.hooks[eventType];
3853
- }
3854
- }
3855
- if (Object.keys(settings.hooks).length === 0) {
3856
- delete settings.hooks;
3857
- }
3858
- writeSettings(settingsPath, settings);
3859
- return removed;
4225
+ return claudeCode.hooksInstalledInSettings(scope, cwd);
3860
4226
  }
3861
- async function installHooks(policyNames, scope = "user", cwd, includeBeta = false, source, customPoliciesPath, removeCustomHooks = false) {
4227
+ async function installHooks(policyNames, scope = "user", cwd, includeBeta = false, source, customPoliciesPath, removeCustomHooks = false, cli) {
3862
4228
  if (policyNames !== undefined && policyNames.length > 0) {
3863
4229
  const nonAllNames = policyNames.filter((n) => n !== "all");
3864
4230
  if (nonAllNames.length > 0)
@@ -3868,6 +4234,13 @@ async function installHooks(policyNames, scope = "user", cwd, includeBeta = fals
3868
4234
  ` + `Use either: --install all or --install block-sudo sanitize-jwt ...`);
3869
4235
  }
3870
4236
  }
4237
+ const selectedClis = cli && cli.length > 0 ? [...new Set(cli)] : ["claude"];
4238
+ for (const cliId of selectedClis) {
4239
+ const integration = getIntegration(cliId);
4240
+ if (!integration.scopes.includes(scope)) {
4241
+ throw new CliError(`Scope "${scope}" is not supported by ${integration.displayName}. ` + `Valid scopes: ${integration.scopes.join(", ")}`);
4242
+ }
4243
+ }
3871
4244
  const binaryPath = resolveFailproofaiBinary();
3872
4245
  const previousConfig = readScopedHooksConfig(scope, cwd);
3873
4246
  const previousEnabled = new Set(previousConfig.enabledPolicies);
@@ -3888,7 +4261,7 @@ async function installHooks(policyNames, scope = "user", cwd, includeBeta = fals
3888
4261
  if (removeCustomHooks) {
3889
4262
  delete configToWrite.customPoliciesPath;
3890
4263
  } else if (customPoliciesPath) {
3891
- configToWrite.customPoliciesPath = resolve5(customPoliciesPath);
4264
+ configToWrite.customPoliciesPath = resolve6(customPoliciesPath);
3892
4265
  let validatedHooks = [];
3893
4266
  try {
3894
4267
  validatedHooks = await loadCustomHooks(configToWrite.customPoliciesPath, { strict: true });
@@ -3911,39 +4284,15 @@ Enabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}`)
3911
4284
  } else if (configToWrite.customPoliciesPath) {
3912
4285
  console.log(`Custom hooks path: ${configToWrite.customPoliciesPath}`);
3913
4286
  }
3914
- const settingsPath = getSettingsPath(scope, cwd);
3915
- const settings = readSettings(settingsPath);
3916
- if (!settings.hooks) {
3917
- settings.hooks = {};
3918
- }
3919
- for (const eventType of HOOK_EVENT_TYPES) {
3920
- const command = scope === "project" ? `npx -y failproofai --hook ${eventType}` : `"${binaryPath}" --hook ${eventType}`;
3921
- const hookEntry = {
3922
- type: "command",
3923
- command,
3924
- timeout: 60000,
3925
- [FAILPROOFAI_HOOK_MARKER]: true
3926
- };
3927
- if (!settings.hooks[eventType]) {
3928
- settings.hooks[eventType] = [];
3929
- }
3930
- const matchers = settings.hooks[eventType];
3931
- let found = false;
3932
- for (const matcher of matchers) {
3933
- if (!matcher.hooks)
3934
- continue;
3935
- const failproofaiIdx = matcher.hooks.findIndex((h) => isFailproofaiHook(h));
3936
- if (failproofaiIdx >= 0) {
3937
- matcher.hooks[failproofaiIdx] = hookEntry;
3938
- found = true;
3939
- break;
3940
- }
3941
- }
3942
- if (!found) {
3943
- matchers.push({ hooks: [hookEntry] });
3944
- }
4287
+ const writtenSettingsPaths = [];
4288
+ for (const cliId of selectedClis) {
4289
+ const integration = getIntegration(cliId);
4290
+ const settingsPath = integration.getSettingsPath(scope, cwd);
4291
+ const settings = integration.readSettings(settingsPath);
4292
+ integration.writeHookEntries(settings, binaryPath, scope);
4293
+ integration.writeSettings(settingsPath, settings);
4294
+ writtenSettingsPaths.push({ cli: cliId, path: settingsPath });
3945
4295
  }
3946
- writeSettings(settingsPath, settings);
3947
4296
  try {
3948
4297
  const newSet = new Set(selectedPolicies);
3949
4298
  const policiesAdded = selectedPolicies.filter((p) => !previousEnabled.has(p));
@@ -3951,6 +4300,8 @@ Enabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}`)
3951
4300
  const distinctId = getInstanceId();
3952
4301
  await trackHookEvent(distinctId, "hooks_installed", {
3953
4302
  scope,
4303
+ cli: selectedClis,
4304
+ cli_count: selectedClis.length,
3954
4305
  policies: selectedPolicies,
3955
4306
  policy_count: selectedPolicies.length,
3956
4307
  policies_added: policiesAdded,
@@ -3966,8 +4317,11 @@ Enabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}`)
3966
4317
  command_format: scope === "project" ? "npx" : "absolute"
3967
4318
  });
3968
4319
  } catch {}
3969
- console.log(`Failproof AI hooks installed for all ${HOOK_EVENT_TYPES.length} event types (scope: ${scope}).`);
3970
- console.log(`Settings: ${settingsPath}`);
4320
+ for (const { cli: cliId, path: path2 } of writtenSettingsPaths) {
4321
+ const integration = getIntegration(cliId);
4322
+ console.log(`Failproof AI hooks installed for ${integration.displayName} ` + `(${integration.eventTypes.length} event types, scope: ${scope}).`);
4323
+ console.log(`Settings: ${path2}`);
4324
+ }
3971
4325
  if (scope === "project") {
3972
4326
  console.log(`Command: npx -y failproofai`);
3973
4327
  console.log(`
@@ -3988,6 +4342,7 @@ This file can be committed to git — no machine-specific paths.`);
3988
4342
  }
3989
4343
  async function removeHooks(policyNames, scope = "user", cwd, opts) {
3990
4344
  const configScope = scope === "all" ? "user" : scope;
4345
+ const selectedClis = opts?.cli && opts.cli.length > 0 ? [...new Set(opts.cli)] : ["claude"];
3991
4346
  if (opts?.removeCustomHooks) {
3992
4347
  const config = readScopedHooksConfig(configScope, cwd);
3993
4348
  delete config.customPoliciesPath;
@@ -4016,6 +4371,7 @@ async function removeHooks(policyNames, scope = "user", cwd, opts) {
4016
4371
  const actuallyRemoved = policyNames.filter((p) => config.enabledPolicies.includes(p));
4017
4372
  await trackHookEvent(distinctId, "hooks_removed", {
4018
4373
  scope,
4374
+ cli: selectedClis,
4019
4375
  removal_mode: opts?.betaOnly ? "beta_policies" : "policies",
4020
4376
  beta_only: opts?.betaOnly ?? false,
4021
4377
  policies_removed: actuallyRemoved,
@@ -4032,42 +4388,49 @@ async function removeHooks(policyNames, scope = "user", cwd, opts) {
4032
4388
  return;
4033
4389
  }
4034
4390
  const configBeforeRemoval = readScopedHooksConfig(configScope, cwd);
4035
- const scopesToRemove = scope === "all" ? [...HOOK_SCOPES] : [scope];
4036
4391
  let totalRemoved = 0;
4037
- for (const s of scopesToRemove) {
4038
- const settingsPath = getSettingsPath(s, cwd);
4039
- if (!existsSync10(settingsPath)) {
4040
- if (scope !== "all") {
4041
- console.log("No settings file found. Nothing to remove.");
4042
- return;
4392
+ let nothingToReport = false;
4393
+ for (const cliId of selectedClis) {
4394
+ const integration = getIntegration(cliId);
4395
+ const scopesToRemove = scope === "all" ? [...integration.scopes] : integration.scopes.includes(scope) ? [scope] : [];
4396
+ for (const s of scopesToRemove) {
4397
+ const settingsPath = integration.getSettingsPath(s, cwd);
4398
+ if (!existsSync12(settingsPath)) {
4399
+ if (scope !== "all" && selectedClis.length === 1) {
4400
+ console.log("No settings file found. Nothing to remove.");
4401
+ nothingToReport = true;
4402
+ }
4403
+ continue;
4043
4404
  }
4044
- continue;
4045
- }
4046
- const settings = readSettings(settingsPath);
4047
- if (!settings.hooks) {
4048
- if (scope !== "all") {
4405
+ const removed = integration.removeHooksFromFile(settingsPath);
4406
+ if (removed === 0 && scope !== "all" && selectedClis.length === 1) {
4049
4407
  console.log("No hooks found in settings. Nothing to remove.");
4050
- return;
4408
+ nothingToReport = true;
4409
+ continue;
4410
+ }
4411
+ totalRemoved += removed;
4412
+ if (scope !== "all") {
4413
+ console.log(`Removed ${removed} failproofai hook(s) from ${integration.displayName} settings.`);
4414
+ console.log(`Settings: ${settingsPath}`);
4051
4415
  }
4052
- continue;
4053
- }
4054
- const removed = removeHooksFromSettingsFile(settingsPath);
4055
- totalRemoved += removed;
4056
- if (scope !== "all") {
4057
- console.log(`Removed ${removed} failproofai hook(s) from settings.`);
4058
- console.log(`Settings: ${settingsPath}`);
4059
4416
  }
4060
4417
  }
4418
+ if (nothingToReport && totalRemoved === 0)
4419
+ return;
4061
4420
  if (scope === "all") {
4062
4421
  console.log(`Removed ${totalRemoved} failproofai hook(s) from all scopes.`);
4063
- for (const s of scopesToRemove) {
4064
- console.log(` ${s}: ${getSettingsPath(s, cwd)}`);
4422
+ for (const cliId of selectedClis) {
4423
+ const integration = getIntegration(cliId);
4424
+ for (const s of integration.scopes) {
4425
+ console.log(` ${integration.displayName} / ${s}: ${integration.getSettingsPath(s, cwd)}`);
4426
+ }
4065
4427
  }
4066
4428
  }
4067
4429
  try {
4068
4430
  const distinctId = getInstanceId();
4069
4431
  await trackHookEvent(distinctId, "hooks_removed", {
4070
4432
  scope,
4433
+ cli: selectedClis,
4071
4434
  removal_mode: "hooks",
4072
4435
  policies_removed: configBeforeRemoval.enabledPolicies,
4073
4436
  removed_count: totalRemoved,
@@ -4211,7 +4574,7 @@ Failproof AI Hook Policies
4211
4574
  if (config.customPoliciesPath) {
4212
4575
  console.log(`
4213
4576
  ── Custom Policies (${config.customPoliciesPath}) ───────────────────────`);
4214
- if (!existsSync10(config.customPoliciesPath)) {
4577
+ if (!existsSync12(config.customPoliciesPath)) {
4215
4578
  console.log(` \x1B[31m✗ File not found: ${config.customPoliciesPath}\x1B[0m`);
4216
4579
  } else {
4217
4580
  const hooks = await loadCustomHooks(config.customPoliciesPath);
@@ -4226,10 +4589,10 @@ Failproof AI Hook Policies
4226
4589
  }
4227
4590
  console.log();
4228
4591
  }
4229
- const base = cwd ? resolve5(cwd) : process.cwd();
4592
+ const base = cwd ? resolve6(cwd) : process.cwd();
4230
4593
  const conventionDirs = [
4231
- { label: "Project", dir: resolve5(base, ".failproofai", "policies") },
4232
- { label: "User", dir: resolve5(homedir11(), ".failproofai", "policies") }
4594
+ { label: "Project", dir: resolve6(base, ".failproofai", "policies") },
4595
+ { label: "User", dir: resolve6(homedir13(), ".failproofai", "policies") }
4233
4596
  ];
4234
4597
  for (const { label, dir } of conventionDirs) {
4235
4598
  const files = discoverPolicyFiles(dir);
@@ -4259,6 +4622,7 @@ Failproof AI Hook Policies
4259
4622
  var VALID_POLICY_NAMES;
4260
4623
  var init_manager = __esm(() => {
4261
4624
  init_types();
4625
+ init_integrations();
4262
4626
  init_install_prompt();
4263
4627
  init_hooks_config();
4264
4628
  init_builtin_policies();
@@ -4269,6 +4633,316 @@ var init_manager = __esm(() => {
4269
4633
  VALID_POLICY_NAMES = new Set(BUILTIN_POLICIES.map((p) => p.name));
4270
4634
  });
4271
4635
 
4636
+ // src/hooks/install-prompt.ts
4637
+ var exports_install_prompt = {};
4638
+ __export(exports_install_prompt, {
4639
+ resolveTargetClis: () => resolveTargetClis,
4640
+ promptPolicySelection: () => promptPolicySelection2
4641
+ });
4642
+ import * as readline2 from "node:readline";
4643
+ async function resolveTargetClis(explicit) {
4644
+ if (explicit && explicit.length > 0)
4645
+ return [...new Set(explicit)];
4646
+ const detected = detectInstalledClis();
4647
+ if (detected.length === 0) {
4648
+ console.log("\x1B[33mWarning: no agent CLI binary found in PATH (claude, codex). " + "Defaulting to Claude Code; hooks will activate when an agent is installed.\x1B[0m");
4649
+ return ["claude"];
4650
+ }
4651
+ if (detected.length === 1) {
4652
+ const integration = getIntegration(detected[0]);
4653
+ console.log(`Detected ${integration.displayName}; installing hooks for it.`);
4654
+ return detected;
4655
+ }
4656
+ if (!process.stdin.isTTY)
4657
+ return detected;
4658
+ const labels = detected.map((id) => getIntegration(id).displayName).join(" + ");
4659
+ process.stdout.write(`Detected ${labels}. Install for [B]oth (default), [C]laude Code only, or co[D]ex only? `);
4660
+ return new Promise((resolve7) => {
4661
+ readline2.emitKeypressEvents(process.stdin);
4662
+ const wasRaw = process.stdin.isRaw;
4663
+ if (process.stdin.setRawMode)
4664
+ process.stdin.setRawMode(true);
4665
+ const restore = () => {
4666
+ if (process.stdin.setRawMode)
4667
+ process.stdin.setRawMode(wasRaw ?? false);
4668
+ process.stdin.removeListener("keypress", onKey);
4669
+ };
4670
+ const onKey = (str, key) => {
4671
+ if (key && key.ctrl && (key.name === "c" || key.name === "d")) {
4672
+ restore();
4673
+ process.stdout.write(`
4674
+ `);
4675
+ process.exit(130);
4676
+ }
4677
+ const ch = (str || "").toLowerCase();
4678
+ restore();
4679
+ process.stdout.write(`
4680
+ `);
4681
+ if (ch === "c")
4682
+ resolve7(["claude"]);
4683
+ else if (ch === "d")
4684
+ resolve7(["codex"]);
4685
+ else
4686
+ resolve7(detected);
4687
+ };
4688
+ process.stdin.on("keypress", onKey);
4689
+ });
4690
+ }
4691
+ async function promptPolicySelection2(preSelected, options = {}) {
4692
+ const { includeBeta = false } = options;
4693
+ if (!process.stdin.isTTY) {
4694
+ const available = BUILTIN_POLICIES.filter((p) => includeBeta || !p.beta);
4695
+ if (preSelected)
4696
+ return preSelected.filter((name) => available.some((p) => p.name === name));
4697
+ return available.filter((p) => p.defaultEnabled).map((p) => p.name);
4698
+ }
4699
+ const preSelectedSet = preSelected ? new Set(preSelected) : null;
4700
+ const items = BUILTIN_POLICIES.filter((p) => includeBeta || !p.beta).map((p) => ({
4701
+ name: p.name,
4702
+ description: p.description,
4703
+ category: p.category,
4704
+ selected: preSelectedSet ? preSelectedSet.has(p.name) : p.defaultEnabled,
4705
+ beta: !!p.beta
4706
+ }));
4707
+ const total = items.length;
4708
+ const WINDOW_SIZE = 8;
4709
+ let cursor = 0;
4710
+ let search = "";
4711
+ let lastLineCount = 0;
4712
+ let cursorHidden = false;
4713
+ function hideCursor() {
4714
+ if (!cursorHidden) {
4715
+ process.stdout.write("\x1B[?25l");
4716
+ cursorHidden = true;
4717
+ }
4718
+ }
4719
+ function showCursor() {
4720
+ if (cursorHidden) {
4721
+ process.stdout.write("\x1B[?25h");
4722
+ cursorHidden = false;
4723
+ }
4724
+ }
4725
+ function truncateLine(line, width) {
4726
+ let visual = 0;
4727
+ let result = "";
4728
+ let i = 0;
4729
+ while (i < line.length) {
4730
+ if (line[i] === "\x1B" && line[i + 1] === "[") {
4731
+ let j = i + 2;
4732
+ while (j < line.length && !/[A-Za-z]/.test(line[j]))
4733
+ j++;
4734
+ j++;
4735
+ result += line.slice(i, j);
4736
+ i = j;
4737
+ } else {
4738
+ if (visual >= width)
4739
+ break;
4740
+ result += line[i];
4741
+ visual++;
4742
+ i++;
4743
+ }
4744
+ }
4745
+ return result;
4746
+ }
4747
+ function getFiltered() {
4748
+ if (!search)
4749
+ return items;
4750
+ const q = search.toLowerCase();
4751
+ return items.filter((i) => i.name.toLowerCase().includes(q) || i.description.toLowerCase().includes(q));
4752
+ }
4753
+ function buildDisplayRows(filtered) {
4754
+ const categoryOrder = [];
4755
+ const categoryEnabledCount = new Map;
4756
+ const categoryTotalCount = new Map;
4757
+ for (const p of items) {
4758
+ if (!categoryEnabledCount.has(p.category)) {
4759
+ categoryOrder.push(p.category);
4760
+ categoryEnabledCount.set(p.category, 0);
4761
+ categoryTotalCount.set(p.category, 0);
4762
+ }
4763
+ categoryTotalCount.set(p.category, categoryTotalCount.get(p.category) + 1);
4764
+ if (p.selected)
4765
+ categoryEnabledCount.set(p.category, categoryEnabledCount.get(p.category) + 1);
4766
+ }
4767
+ const filteredByCategory = new Map;
4768
+ for (const item of filtered) {
4769
+ const bucket = filteredByCategory.get(item.category) ?? [];
4770
+ bucket.push(item);
4771
+ filteredByCategory.set(item.category, bucket);
4772
+ }
4773
+ const rows = [];
4774
+ let idx = 0;
4775
+ for (const cat of categoryOrder) {
4776
+ const catFiltered = filteredByCategory.get(cat);
4777
+ if (!catFiltered || catFiltered.length === 0)
4778
+ continue;
4779
+ rows.push({ kind: "header", category: cat, enabledCount: categoryEnabledCount.get(cat), totalCount: categoryTotalCount.get(cat) });
4780
+ for (const item of catFiltered) {
4781
+ rows.push({ kind: "item", item, filteredIndex: idx++ });
4782
+ }
4783
+ }
4784
+ return rows;
4785
+ }
4786
+ function render() {
4787
+ const cols = process.stdout.columns || 120;
4788
+ hideCursor();
4789
+ const filtered = getFiltered();
4790
+ const shown = filtered.length;
4791
+ if (shown > 0 && cursor >= shown)
4792
+ cursor = shown - 1;
4793
+ const lines = [];
4794
+ lines.push(" Failproof AI — Policy Manager");
4795
+ lines.push("");
4796
+ const innerWidth = Math.max(20, cols - 6);
4797
+ const topBorder = " ┌" + "─".repeat(innerWidth + 2) + "┐";
4798
+ const botBorder = " └" + "─".repeat(innerWidth + 2) + "┘";
4799
+ const cursorChar = "\x1B[7m \x1B[0m";
4800
+ const countPart = search ? ` \x1B[2m(${shown}/${total})\x1B[0m` : ` \x1B[2m(${total} policies)\x1B[0m`;
4801
+ const searchContent = `\x1B[1mSearch:\x1B[0m ${search}${cursorChar}${countPart}`;
4802
+ lines.push(topBorder);
4803
+ lines.push(` │ ${searchContent}`);
4804
+ lines.push(botBorder);
4805
+ lines.push("");
4806
+ if (shown === 0) {
4807
+ lines.push(" \x1B[2mNo policies match “" + search + "”\x1B[0m");
4808
+ for (let i = 0;i < WINDOW_SIZE + 1; i++)
4809
+ lines.push("");
4810
+ } else {
4811
+ const displayRows = buildDisplayRows(filtered);
4812
+ let cursorDisplayRow = 0;
4813
+ for (let i = 0;i < displayRows.length; i++) {
4814
+ const row = displayRows[i];
4815
+ if (row.kind === "item" && row.filteredIndex === cursor) {
4816
+ cursorDisplayRow = i;
4817
+ break;
4818
+ }
4819
+ }
4820
+ let windowStart = cursorDisplayRow - Math.floor(WINDOW_SIZE / 2);
4821
+ windowStart = Math.max(0, windowStart);
4822
+ windowStart = Math.min(windowStart, Math.max(0, displayRows.length - WINDOW_SIZE));
4823
+ const windowEnd = Math.min(displayRows.length, windowStart + WINDOW_SIZE);
4824
+ const aboveItems = displayRows.slice(0, windowStart).filter((r) => r.kind === "item").length;
4825
+ if (aboveItems > 0) {
4826
+ lines.push(` \x1B[2m ↑ ${aboveItems} more above\x1B[0m`);
4827
+ } else {
4828
+ lines.push("");
4829
+ }
4830
+ for (let i = windowStart;i < windowEnd; i++) {
4831
+ const row = displayRows[i];
4832
+ if (row.kind === "header") {
4833
+ const label = ` ${row.category.toUpperCase()} (${row.enabledCount}/${row.totalCount}) `;
4834
+ const prefix = "── ";
4835
+ const prefixLen = 3 + label.length;
4836
+ const dashLen = Math.max(2, cols - 2 - prefixLen);
4837
+ lines.push(` \x1B[2m${prefix}${label}${"─".repeat(dashLen)}\x1B[0m`);
4838
+ } else {
4839
+ const item = row.item;
4840
+ const isActive = row.filteredIndex === cursor;
4841
+ const pointer = isActive ? "\x1B[36m❯\x1B[0m" : " ";
4842
+ const check = item.selected ? "\x1B[32m[✓]\x1B[0m" : "[ ]";
4843
+ const namePart = isActive ? `\x1B[1;36m${item.name}\x1B[0m` : item.name;
4844
+ const betaPart = item.beta ? " \x1B[35m[beta]\x1B[0m" : "";
4845
+ const pad = " ".repeat(Math.max(1, 28 - item.name.length));
4846
+ const desc = `\x1B[2m${item.description}\x1B[0m`;
4847
+ lines.push(` ${pointer} ${check} ${namePart}${betaPart}${pad}${desc}`);
4848
+ }
4849
+ }
4850
+ for (let i = windowEnd - windowStart;i < WINDOW_SIZE; i++) {
4851
+ lines.push("");
4852
+ }
4853
+ const belowItems = displayRows.slice(windowEnd).filter((r) => r.kind === "item").length;
4854
+ if (belowItems > 0) {
4855
+ lines.push(` \x1B[2m ↓ ${belowItems} more below\x1B[0m`);
4856
+ } else {
4857
+ lines.push("");
4858
+ }
4859
+ }
4860
+ lines.push("");
4861
+ lines.push(" \x1B[2m" + "─".repeat(cols - 2) + "\x1B[0m");
4862
+ lines.push(" [↑↓] Move [Space] Toggle [Ctrl+A] All [Ctrl+S] Save [Esc] Clear [^C] Quit");
4863
+ lines.push("");
4864
+ lines.push(" \x1B[2mTip: `policies` for a flat list · `policies --install <name…>` to skip prompt\x1B[0m");
4865
+ if (!includeBeta) {
4866
+ lines.push(" \x1B[2mTip: `policies --install --beta` to include beta policies\x1B[0m");
4867
+ }
4868
+ if (lastLineCount > 0) {
4869
+ process.stdout.write(`\x1B[${lastLineCount}A\x1B[J`);
4870
+ }
4871
+ const output = lines.map((l) => l === "" ? l : truncateLine(l, cols)).join(`
4872
+ `) + `
4873
+ `;
4874
+ process.stdout.write(output);
4875
+ lastLineCount = lines.length;
4876
+ }
4877
+ return new Promise((resolve7) => {
4878
+ render();
4879
+ process.stdin.setRawMode(true);
4880
+ process.stdin.resume();
4881
+ readline2.emitKeypressEvents(process.stdin);
4882
+ function keypressHandler(_str, key) {
4883
+ if (!key)
4884
+ return;
4885
+ if (key.ctrl && key.name === "c") {
4886
+ cleanup();
4887
+ process.exit(0);
4888
+ }
4889
+ const filtered = getFiltered();
4890
+ if (key.name === "up") {
4891
+ if (filtered.length > 0) {
4892
+ cursor = cursor > 0 ? cursor - 1 : filtered.length - 1;
4893
+ }
4894
+ render();
4895
+ } else if (key.name === "down") {
4896
+ if (filtered.length > 0) {
4897
+ cursor = cursor < filtered.length - 1 ? cursor + 1 : 0;
4898
+ }
4899
+ render();
4900
+ } else if (key.name === "return" || key.name === "space") {
4901
+ const item = filtered[cursor];
4902
+ if (item)
4903
+ item.selected = !item.selected;
4904
+ render();
4905
+ } else if (key.name === "escape") {
4906
+ search = "";
4907
+ cursor = 0;
4908
+ render();
4909
+ } else if (key.ctrl && key.name === "a") {
4910
+ const allSelected = filtered.length > 0 && filtered.every((i) => i.selected);
4911
+ for (const item of filtered)
4912
+ item.selected = !allSelected;
4913
+ render();
4914
+ } else if (key.ctrl && key.name === "s") {
4915
+ cleanup();
4916
+ const selected = items.filter((i) => i.selected).map((i) => i.name);
4917
+ process.stdout.write(`
4918
+ `);
4919
+ resolve7(selected);
4920
+ } else if (key.name === "backspace" || key.name === "delete") {
4921
+ if (search.length > 0) {
4922
+ search = search.slice(0, -1);
4923
+ cursor = 0;
4924
+ render();
4925
+ }
4926
+ } else if (_str && _str.length === 1 && !key.ctrl && !key.meta) {
4927
+ search += _str;
4928
+ cursor = 0;
4929
+ render();
4930
+ }
4931
+ }
4932
+ function cleanup() {
4933
+ showCursor();
4934
+ process.stdin.removeListener("keypress", keypressHandler);
4935
+ process.stdin.setRawMode(false);
4936
+ process.stdin.pause();
4937
+ }
4938
+ process.stdin.on("keypress", keypressHandler);
4939
+ });
4940
+ }
4941
+ var init_install_prompt2 = __esm(() => {
4942
+ init_builtin_policies();
4943
+ init_integrations();
4944
+ });
4945
+
4272
4946
  // src/auth/login.ts
4273
4947
  var exports_login = {};
4274
4948
  __export(exports_login, {
@@ -4404,14 +5078,14 @@ __export(exports_pid, {
4404
5078
  isProcessAlive: () => isProcessAlive2,
4405
5079
  clearPid: () => clearPid2
4406
5080
  });
4407
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync11, unlinkSync as unlinkSync5, mkdirSync as mkdirSync8 } from "node:fs";
4408
- import { join as join9, dirname as dirname5 } from "node:path";
4409
- import { homedir as homedir12 } from "node:os";
5081
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync7, existsSync as existsSync13, unlinkSync as unlinkSync5, mkdirSync as mkdirSync9 } from "node:fs";
5082
+ import { join as join10, dirname as dirname6 } from "node:path";
5083
+ import { homedir as homedir14 } from "node:os";
4410
5084
  function readPid2() {
4411
- if (!existsSync11(PID_FILE2))
5085
+ if (!existsSync13(PID_FILE2))
4412
5086
  return null;
4413
5087
  try {
4414
- const raw = readFileSync7(PID_FILE2, "utf8").trim();
5088
+ const raw = readFileSync8(PID_FILE2, "utf8").trim();
4415
5089
  const pid = parseInt(raw, 10);
4416
5090
  if (Number.isNaN(pid) || pid <= 0)
4417
5091
  return null;
@@ -4421,13 +5095,13 @@ function readPid2() {
4421
5095
  }
4422
5096
  }
4423
5097
  function writePid2(pid) {
4424
- const dir = dirname5(PID_FILE2);
4425
- if (!existsSync11(dir))
4426
- mkdirSync8(dir, { recursive: true, mode: 448 });
4427
- writeFileSync6(PID_FILE2, String(pid));
5098
+ const dir = dirname6(PID_FILE2);
5099
+ if (!existsSync13(dir))
5100
+ mkdirSync9(dir, { recursive: true, mode: 448 });
5101
+ writeFileSync7(PID_FILE2, String(pid));
4428
5102
  }
4429
5103
  function clearPid2() {
4430
- if (existsSync11(PID_FILE2))
5104
+ if (existsSync13(PID_FILE2))
4431
5105
  unlinkSync5(PID_FILE2);
4432
5106
  }
4433
5107
  function isProcessAlive2(pid) {
@@ -4465,26 +5139,26 @@ function relayStatus() {
4465
5139
  }
4466
5140
  var PID_FILE2;
4467
5141
  var init_pid2 = __esm(() => {
4468
- PID_FILE2 = join9(homedir12(), ".failproofai", "relay.pid");
5142
+ PID_FILE2 = join10(homedir14(), ".failproofai", "relay.pid");
4469
5143
  });
4470
5144
 
4471
5145
  // lib/paths.ts
4472
- import { homedir as homedir13 } from "os";
4473
- import { join as join10 } from "path";
5146
+ import { homedir as homedir15 } from "os";
5147
+ import { join as join11 } from "path";
4474
5148
  function getDefaultClaudeProjectsPath() {
4475
- return join10(homedir13(), ".claude", "projects");
5149
+ return join11(homedir15(), ".claude", "projects");
4476
5150
  }
4477
5151
  var init_paths = () => {};
4478
5152
 
4479
5153
  // scripts/parse-script-args.ts
4480
- import { resolve as resolve6 } from "path";
5154
+ import { resolve as resolve7 } from "path";
4481
5155
  function parseStringFlag(flagName, errorLabel, inlineValue, args, index, options) {
4482
5156
  const raw = inlineValue ?? args[index + 1];
4483
5157
  if (raw === undefined || inlineValue === null && raw.startsWith("-")) {
4484
5158
  console.error(`Error: ${flagName} requires ${errorLabel}`);
4485
5159
  process.exit(1);
4486
5160
  }
4487
- const value = options?.resolve ? resolve6(raw) : raw;
5161
+ const value = options?.resolve ? resolve7(raw) : raw;
4488
5162
  return { value, spliceCount: inlineValue !== null ? 1 : 2 };
4489
5163
  }
4490
5164
  function parseScriptArgs(argv) {
@@ -4545,8 +5219,8 @@ __export(exports_launch, {
4545
5219
  launch: () => launch
4546
5220
  });
4547
5221
  import { spawn as spawn4 } from "child_process";
4548
- import { realpathSync, existsSync as existsSync12 } from "node:fs";
4549
- import { resolve as resolve7, dirname as dirname6 } from "node:path";
5222
+ import { realpathSync, existsSync as existsSync14 } from "node:fs";
5223
+ import { resolve as resolve8, dirname as dirname7 } from "node:path";
4550
5224
  import { fileURLToPath } from "node:url";
4551
5225
  function launch(mode) {
4552
5226
  const { claudeProjectsPath: parsedPath, loggingLevel, disableTelemetry, allowedDevOrigins, remainingArgs } = parseScriptArgs(process.argv.slice(2));
@@ -4577,9 +5251,9 @@ function launch(mode) {
4577
5251
  process.env.PORT = port;
4578
5252
  process.env.HOSTNAME = "0.0.0.0";
4579
5253
  cmd = "node";
4580
- const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT ?? resolve7(dirname6(realpathSync(fileURLToPath(import.meta.url))), "..");
4581
- const serverJsPath = resolve7(packageRoot, ".next/standalone/server.js");
4582
- if (!existsSync12(serverJsPath)) {
5254
+ const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT ?? resolve8(dirname7(realpathSync(fileURLToPath(import.meta.url))), "..");
5255
+ const serverJsPath = resolve8(packageRoot, ".next/standalone/server.js");
5256
+ if (!existsSync14(serverJsPath)) {
4583
5257
  console.error(`
4584
5258
  Error: Cannot find server.js at:
4585
5259
  ${serverJsPath}
@@ -4638,17 +5312,17 @@ var init_cli_error2 = __esm(() => {
4638
5312
 
4639
5313
  // bin/failproofai.mjs
4640
5314
  import { realpathSync as realpathSync2 } from "fs";
4641
- import { dirname as dirname7, resolve as resolve8 } from "path";
5315
+ import { dirname as dirname8, resolve as resolve9 } from "path";
4642
5316
  import { fileURLToPath as fileURLToPath2 } from "url";
4643
5317
  // package.json
4644
- var version = "0.0.7";
5318
+ var version = "0.0.9-beta.0";
4645
5319
 
4646
5320
  // bin/failproofai.mjs
4647
5321
  if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
4648
- process.env.FAILPROOFAI_PACKAGE_ROOT = resolve8(dirname7(realpathSync2(fileURLToPath2(import.meta.url))), "..");
5322
+ process.env.FAILPROOFAI_PACKAGE_ROOT = resolve9(dirname8(realpathSync2(fileURLToPath2(import.meta.url))), "..");
4649
5323
  }
4650
5324
  if (!process.env.FAILPROOFAI_DIST_PATH) {
4651
- process.env.FAILPROOFAI_DIST_PATH = resolve8(dirname7(realpathSync2(fileURLToPath2(import.meta.url))), "..", "dist");
5325
+ process.env.FAILPROOFAI_DIST_PATH = resolve9(dirname8(realpathSync2(fileURLToPath2(import.meta.url))), "..", "dist");
4652
5326
  }
4653
5327
  var args = process.argv.slice(2);
4654
5328
  if (args[0] === "p")
@@ -4657,12 +5331,16 @@ var hookIdx = args.indexOf("--hook");
4657
5331
  if (hookIdx >= 0) {
4658
5332
  if (!args[hookIdx + 1]) {
4659
5333
  console.error("Error: Missing event type after --hook");
4660
- console.error("Usage: failproofai --hook <event> (e.g. PreToolUse, PostToolUse)");
5334
+ console.error("Usage: failproofai --hook <event> [--cli <claude|codex>]");
4661
5335
  process.exit(1);
4662
5336
  }
5337
+ const eventType = args[hookIdx + 1];
5338
+ const cliIdx = args.indexOf("--cli");
5339
+ const cliArg = cliIdx >= 0 ? args[cliIdx + 1] : undefined;
5340
+ const cli = cliArg && (cliArg === "claude" || cliArg === "codex") ? cliArg : "claude";
4663
5341
  try {
4664
5342
  const { handleHookEvent: handleHookEvent2 } = await Promise.resolve().then(() => (init_handler(), exports_handler));
4665
- const exitCode = await handleHookEvent2(args[hookIdx + 1]);
5343
+ const exitCode = await handleHookEvent2(eventType, cli);
4666
5344
  process.exit(exitCode);
4667
5345
  } catch (err) {
4668
5346
  const msg = err instanceof Error ? err.message : String(err);
@@ -4699,14 +5377,19 @@ COMMANDS
4699
5377
  (no args) Launch the policy dashboard
4700
5378
 
4701
5379
  policies, p List all available policies and their status
4702
- policies --install, -i Enable policies in Claude Code settings
5380
+ policies --install, -i Enable policies in agent CLI settings
4703
5381
  [names...] Specific policy names to enable
5382
+ --cli claude|codex Agent CLI(s) to install for; space-separated
5383
+ (e.g. --cli claude codex) or repeated.
5384
+ Default: detect installed CLIs and prompt.
4704
5385
  --scope user|project|local Config scope to write to (default: user)
5386
+ (Codex supports user|project only)
4705
5387
  --beta Include beta policies
4706
5388
  --custom, -c <path> Path to a JS file of custom policies
4707
5389
 
4708
5390
  policies --uninstall, -u Disable policies or remove hooks
4709
5391
  [names...] Specific policy names to disable
5392
+ --cli claude|codex Agent CLI(s) to uninstall from
4710
5393
  --scope user|project|local|all Config scope to remove from (default: user)
4711
5394
  --beta Remove only beta policies
4712
5395
  --custom, -c Clear the customPoliciesPath from config
@@ -4731,9 +5414,12 @@ EXAMPLES
4731
5414
  failproofai policies
4732
5415
  failproofai policies --install
4733
5416
  failproofai policies --install block-sudo sanitize-api-keys --scope project
5417
+ failproofai policies --install --cli codex --scope project
5418
+ failproofai policies --install --cli claude codex
4734
5419
  failproofai policies --install --custom ./my-policies.js
4735
5420
  failproofai policies -i -c ./my-policies.js
4736
5421
  failproofai policies --uninstall block-sudo
5422
+ failproofai policies --uninstall --cli codex
4737
5423
  failproofai policies --uninstall --custom
4738
5424
 
4739
5425
  LINKS
@@ -4767,13 +5453,19 @@ USAGE
4767
5453
 
4768
5454
  OPTIONS (install)
4769
5455
  [names...] Specific policy names to enable (omit for interactive)
5456
+ --cli claude|codex Agent CLI(s) to install for; space-separated
5457
+ (e.g. --cli claude codex) or repeated. Omit to
5458
+ detect installed CLIs and prompt (or auto-pick
5459
+ if only one is found).
4770
5460
  --scope user|project|local Config scope to write to (default: user)
5461
+ (Codex supports user|project only)
4771
5462
  --beta Include beta policies
4772
5463
  --custom, -c <path> Path to a JS file of custom policies
4773
5464
  (skips interactive prompt; validates file first)
4774
5465
 
4775
5466
  OPTIONS (uninstall)
4776
5467
  [names...] Specific policy names to disable (omit to remove hooks)
5468
+ --cli claude|codex Agent CLI(s) to uninstall from
4777
5469
  --scope user|project|local|all Config scope to remove from (default: user)
4778
5470
  --beta Remove only beta policies
4779
5471
  --custom, -c Clear the customPoliciesPath from config
@@ -4782,9 +5474,12 @@ EXAMPLES
4782
5474
  failproofai policies
4783
5475
  failproofai policies --install
4784
5476
  failproofai policies --install block-sudo sanitize-api-keys
5477
+ failproofai policies --install --cli codex --scope project
5478
+ failproofai policies --install --cli claude codex
4785
5479
  failproofai policies --install --custom ./my-policies.js
4786
5480
  failproofai policies -i -c ./my-policies.js
4787
5481
  failproofai policies --uninstall block-sudo
5482
+ failproofai policies --uninstall --cli codex
4788
5483
  failproofai policies -u
4789
5484
  failproofai policies --uninstall --custom
4790
5485
  `.trimStart());
@@ -4792,6 +5487,7 @@ EXAMPLES
4792
5487
  }
4793
5488
  if (isInstall) {
4794
5489
  const { installHooks: installHooks2 } = await Promise.resolve().then(() => (init_manager(), exports_manager));
5490
+ const { resolveTargetClis: resolveTargetClis2 } = await Promise.resolve().then(() => (init_install_prompt2(), exports_install_prompt));
4795
5491
  const scopeIdx = subArgs.indexOf("--scope");
4796
5492
  const scope = scopeIdx >= 0 ? subArgs[scopeIdx + 1] : "user";
4797
5493
  if (scopeIdx >= 0 && (!scope || scope.startsWith("-"))) {
@@ -4806,13 +5502,35 @@ EXAMPLES
4806
5502
  throw new CliError3(`Missing path after --custom/-c
4807
5503
  Usage: --custom <path> (e.g. --custom ./my-policies.js)`);
4808
5504
  }
5505
+ const VALID_CLIS = new Set(["claude", "codex"]);
5506
+ const cliFlagValues = [];
5507
+ const cliConsumedIdxs = new Set;
5508
+ const cliFlagIdxs = subArgs.map((a, i) => a === "--cli" ? i : -1).filter((i) => i >= 0);
5509
+ for (const idx of cliFlagIdxs) {
5510
+ let consumed = 0;
5511
+ for (let j = idx + 1;j < subArgs.length; j++) {
5512
+ const v = subArgs[j];
5513
+ if (v.startsWith("-"))
5514
+ break;
5515
+ if (!VALID_CLIS.has(v))
5516
+ break;
5517
+ cliFlagValues.push(v);
5518
+ cliConsumedIdxs.add(j);
5519
+ consumed++;
5520
+ }
5521
+ if (consumed === 0) {
5522
+ throw new CliError3("Missing value(s) for --cli. Usage: --cli claude codex (or any subset)");
5523
+ }
5524
+ }
4809
5525
  const includeBeta = subArgs.includes("--beta");
4810
5526
  const consumedIdxs = new Set;
4811
5527
  if (scopeIdx >= 0)
4812
5528
  consumedIdxs.add(scopeIdx + 1);
4813
5529
  if (customIdx >= 0)
4814
5530
  consumedIdxs.add(customIdx + 1);
4815
- const flags = new Set(["--install", "-i", "--scope", "--beta", "--custom", "-c"]);
5531
+ for (const i of cliConsumedIdxs)
5532
+ consumedIdxs.add(i);
5533
+ const flags = new Set(["--install", "-i", "--scope", "--beta", "--custom", "-c", "--cli"]);
4816
5534
  const unknownInstallFlag = subArgs.find((a) => a.startsWith("-") && !flags.has(a));
4817
5535
  if (unknownInstallFlag) {
4818
5536
  throw new CliError3(`Unknown flag: ${unknownInstallFlag}
@@ -4820,11 +5538,13 @@ Run \`failproofai policies --help\` for usage.`);
4820
5538
  }
4821
5539
  const explicitPolicyNames = subArgs.filter((a, idx) => !a.startsWith("-") && !consumedIdxs.has(idx));
4822
5540
  const policyNames = explicitPolicyNames.length > 0 ? explicitPolicyNames : customPoliciesPath !== undefined ? [] : undefined;
4823
- await installHooks2(policyNames, scope, undefined, includeBeta, undefined, customPoliciesPath);
5541
+ const cli = await resolveTargetClis2(cliFlagValues.length > 0 ? cliFlagValues : undefined);
5542
+ await installHooks2(policyNames, scope, undefined, includeBeta, undefined, customPoliciesPath, false, cli);
4824
5543
  process.exit(0);
4825
5544
  }
4826
5545
  if (isUninstall) {
4827
5546
  const { removeHooks: removeHooks2 } = await Promise.resolve().then(() => (init_manager(), exports_manager));
5547
+ const { resolveTargetClis: resolveTargetClis2 } = await Promise.resolve().then(() => (init_install_prompt2(), exports_install_prompt));
4828
5548
  const scopeIdx = subArgs.indexOf("--scope");
4829
5549
  const scope = scopeIdx >= 0 ? subArgs[scopeIdx + 1] : "user";
4830
5550
  if (scopeIdx >= 0 && (!scope || scope.startsWith("-"))) {
@@ -4833,19 +5553,42 @@ Run \`failproofai policies --help\` for usage.`);
4833
5553
  if (scopeIdx >= 0 && !["user", "project", "local", "all"].includes(scope)) {
4834
5554
  throw new CliError3(`Invalid scope: ${scope}. Valid values: user, project, local, all`);
4835
5555
  }
5556
+ const VALID_CLIS = new Set(["claude", "codex"]);
5557
+ const cliFlagValues = [];
5558
+ const cliConsumedIdxs = new Set;
5559
+ const cliFlagIdxs = subArgs.map((a, i) => a === "--cli" ? i : -1).filter((i) => i >= 0);
5560
+ for (const idx of cliFlagIdxs) {
5561
+ let consumed = 0;
5562
+ for (let j = idx + 1;j < subArgs.length; j++) {
5563
+ const v = subArgs[j];
5564
+ if (v.startsWith("-"))
5565
+ break;
5566
+ if (!VALID_CLIS.has(v))
5567
+ break;
5568
+ cliFlagValues.push(v);
5569
+ cliConsumedIdxs.add(j);
5570
+ consumed++;
5571
+ }
5572
+ if (consumed === 0) {
5573
+ throw new CliError3("Missing value(s) for --cli. Usage: --cli claude codex (or any subset)");
5574
+ }
5575
+ }
4836
5576
  const betaOnly = subArgs.includes("--beta");
4837
5577
  const removeCustomHooks = subArgs.includes("--custom") || subArgs.includes("-c");
4838
5578
  const consumedIdxs = new Set;
4839
5579
  if (scopeIdx >= 0)
4840
5580
  consumedIdxs.add(scopeIdx + 1);
4841
- const flags = new Set(["--uninstall", "-u", "--scope", "--beta", "--custom", "-c"]);
5581
+ for (const i of cliConsumedIdxs)
5582
+ consumedIdxs.add(i);
5583
+ const flags = new Set(["--uninstall", "-u", "--scope", "--beta", "--custom", "-c", "--cli"]);
4842
5584
  const unknownUninstallFlag = subArgs.find((a) => a.startsWith("-") && !flags.has(a));
4843
5585
  if (unknownUninstallFlag) {
4844
5586
  throw new CliError3(`Unknown flag: ${unknownUninstallFlag}
4845
5587
  Run \`failproofai policies --help\` for usage.`);
4846
5588
  }
4847
5589
  const policyNames = subArgs.filter((a, idx) => !a.startsWith("-") && !consumedIdxs.has(idx));
4848
- await removeHooks2(policyNames.length > 0 ? policyNames : undefined, scope, undefined, { betaOnly, removeCustomHooks });
5590
+ const cli = await resolveTargetClis2(cliFlagValues.length > 0 ? cliFlagValues : undefined);
5591
+ await removeHooks2(policyNames.length > 0 ? policyNames : undefined, scope, undefined, { betaOnly, removeCustomHooks, cli });
4849
5592
  process.exit(0);
4850
5593
  }
4851
5594
  const knownListFlags = new Set(["--install", "-i", "--uninstall", "-u", "--help", "-h", "--list"]);