failproofai 0.0.8 → 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 (131) 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]__0h3orxc._.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/{151bdxm9n-pry.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/{0mbc8hyeqe2c4.js → 0jce49ygr4fdv.js} +1 -1
  89. package/.next/standalone/.next/static/chunks/0mungg3~jpwe7.js +1 -0
  90. package/.next/standalone/.next/static/chunks/{175-vim0.ztb2.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/{0eowehbf5egcz.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 +1039 -281
  105. package/package.json +2 -2
  106. package/src/hooks/builtin-policies.ts +29 -6
  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]__0_rr1ty._.js +0 -3
  117. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dj-tbi._.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/096~b1zwv69ph.js +0 -1
  123. package/.next/standalone/.next/static/chunks/0bkir2pd22ski.js +0 -1
  124. package/.next/standalone/.next/static/chunks/0ksdlt_1hucdm.js +0 -1
  125. package/.next/standalone/.next/static/chunks/0lua3p__elu_..js +0 -6
  126. package/.next/standalone/.next/static/chunks/0mir9jdxn35~s.css +0 -1
  127. package/.next/standalone/.next/static/chunks/0s_18.dox44e9.js +0 -1
  128. package/.next/standalone/.next/static/chunks/0t3euwspxi_zg.js +0 -1
  129. /package/.next/standalone/.next/static/{RYld7TSCDXm2_WhJq20rD → oUO8u4z9JvtTzS_2RJoGo}/_buildManifest.js +0 -0
  130. /package/.next/standalone/.next/static/{RYld7TSCDXm2_WhJq20rD → oUO8u4z9JvtTzS_2RJoGo}/_clientMiddlewareManifest.js +0 -0
  131. /package/.next/standalone/.next/static/{RYld7TSCDXm2_WhJq20rD → 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 ?? "";
@@ -1206,9 +1268,11 @@ function registerBuiltinPolicies(enabledNames) {
1206
1268
  }
1207
1269
  }
1208
1270
  }
1209
- 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;
1210
1272
  var init_builtin_policies = __esm(() => {
1211
1273
  init_hook_logger();
1274
+ isClaudeInternalPath = isAgentInternalPath;
1275
+ isClaudeSettingsFile = isAgentSettingsFile;
1212
1276
  SHELL_OPERATORS = new Set(["&&", "||", "|", ";"]);
1213
1277
  SHELL_METACHAR_RE = /[;&<>`$()\\]/;
1214
1278
  JWT_RE = /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/;
@@ -1372,7 +1436,7 @@ var init_builtin_policies = __esm(() => {
1372
1436
  name: "block-sudo",
1373
1437
  description: "Block sudo commands",
1374
1438
  fn: blockSudo,
1375
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1439
+ match: { events: ["PreToolUse", "PermissionRequest"], toolNames: ["Bash"] },
1376
1440
  defaultEnabled: true,
1377
1441
  category: "Dangerous Commands",
1378
1442
  params: {
@@ -1782,7 +1846,8 @@ async function evaluatePolicies(eventType, payload, session, config) {
1782
1846
  payload,
1783
1847
  toolName,
1784
1848
  toolInput,
1785
- session
1849
+ session,
1850
+ cli: session?.cli
1786
1851
  };
1787
1852
  const instructEntries = [];
1788
1853
  const allowEntries = [];
@@ -1827,6 +1892,25 @@ async function evaluatePolicies(eventType, payload, session, config) {
1827
1892
  decision: "deny"
1828
1893
  };
1829
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
+ }
1830
1914
  if (eventType === "PostToolUse") {
1831
1915
  const response = {
1832
1916
  hookSpecificOutput: {
@@ -1911,7 +1995,7 @@ You MUST complete the above action(s) NOW. Do NOT ask the user for confirmation
1911
1995
  const combined = allowEntries.map((e) => e.reason).join(`
1912
1996
  `);
1913
1997
  const policyNames = allowEntries.map((e) => e.policyName);
1914
- const supportsHookSpecificOutput = eventType === "PreToolUse" || eventType === "PostToolUse" || eventType === "UserPromptSubmit";
1998
+ const supportsHookSpecificOutput = eventType === "PreToolUse" || eventType === "PostToolUse" || eventType === "UserPromptSubmit" || eventType === "PermissionRequest";
1915
1999
  const response = supportsHookSpecificOutput ? { hookSpecificOutput: { hookEventName: eventType, additionalContext: `Note from failproofai: ${combined}` } } : { reason: combined };
1916
2000
  const stderrMsg = allowEntries.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`).join(`
1917
2001
  `);
@@ -2313,7 +2397,7 @@ var init_hook_activity_store = __esm(() => {
2313
2397
  });
2314
2398
 
2315
2399
  // package.json
2316
- var version2 = "0.0.8";
2400
+ var version2 = "0.0.9-beta.0";
2317
2401
  var init_package = () => {};
2318
2402
 
2319
2403
  // src/posthog-key.ts
@@ -2344,6 +2428,118 @@ var init_hook_telemetry = __esm(() => {
2344
2428
  API_KEY = POSTHOG_API_KEY;
2345
2429
  });
2346
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
+
2347
2543
  // lib/telemetry-id.ts
2348
2544
  import fs from "node:fs";
2349
2545
  import path from "node:path";
@@ -2427,26 +2623,26 @@ var init_telemetry_id = __esm(() => {
2427
2623
 
2428
2624
  // src/auth/token-store.ts
2429
2625
  import {
2430
- readFileSync as readFileSync3,
2431
- writeFileSync as writeFileSync3,
2432
- existsSync as existsSync5,
2433
- mkdirSync as mkdirSync4,
2626
+ readFileSync as readFileSync4,
2627
+ writeFileSync as writeFileSync4,
2628
+ existsSync as existsSync6,
2629
+ mkdirSync as mkdirSync5,
2434
2630
  unlinkSync as unlinkSync2,
2435
2631
  renameSync as renameSync3,
2436
2632
  openSync,
2437
2633
  closeSync
2438
2634
  } from "node:fs";
2439
- import { join as join4 } from "node:path";
2440
- import { homedir as homedir6 } from "node:os";
2635
+ import { join as join5 } from "node:path";
2636
+ import { homedir as homedir7 } from "node:os";
2441
2637
  function ensureAuthDir() {
2442
- if (!existsSync5(AUTH_DIR))
2443
- mkdirSync4(AUTH_DIR, { recursive: true, mode: 448 });
2638
+ if (!existsSync6(AUTH_DIR))
2639
+ mkdirSync5(AUTH_DIR, { recursive: true, mode: 448 });
2444
2640
  }
2445
2641
  function readTokens() {
2446
- if (!existsSync5(AUTH_FILE))
2642
+ if (!existsSync6(AUTH_FILE))
2447
2643
  return null;
2448
2644
  try {
2449
- const raw = readFileSync3(AUTH_FILE, "utf8");
2645
+ const raw = readFileSync4(AUTH_FILE, "utf8");
2450
2646
  return JSON.parse(raw);
2451
2647
  } catch {
2452
2648
  return null;
@@ -2457,23 +2653,23 @@ function writeTokens(tokens) {
2457
2653
  const tmpPath = `${AUTH_FILE}.tmp`;
2458
2654
  const fd = openSync(tmpPath, "w", 384);
2459
2655
  try {
2460
- writeFileSync3(fd, JSON.stringify(tokens, null, 2));
2656
+ writeFileSync4(fd, JSON.stringify(tokens, null, 2));
2461
2657
  } finally {
2462
2658
  closeSync(fd);
2463
2659
  }
2464
2660
  renameSync3(tmpPath, AUTH_FILE);
2465
2661
  }
2466
2662
  function clearTokens() {
2467
- if (existsSync5(AUTH_FILE))
2663
+ if (existsSync6(AUTH_FILE))
2468
2664
  unlinkSync2(AUTH_FILE);
2469
2665
  }
2470
2666
  function isLoggedIn() {
2471
- return existsSync5(AUTH_FILE);
2667
+ return existsSync6(AUTH_FILE);
2472
2668
  }
2473
2669
  var AUTH_DIR, AUTH_FILE;
2474
2670
  var init_token_store = __esm(() => {
2475
- AUTH_DIR = join4(homedir6(), ".failproofai");
2476
- AUTH_FILE = join4(AUTH_DIR, "auth.json");
2671
+ AUTH_DIR = join5(homedir7(), ".failproofai");
2672
+ AUTH_FILE = join5(AUTH_DIR, "auth.json");
2477
2673
  });
2478
2674
 
2479
2675
  // src/relay/queue.ts
@@ -2488,17 +2684,17 @@ __export(exports_queue, {
2488
2684
  });
2489
2685
  import {
2490
2686
  appendFileSync as appendFileSync3,
2491
- mkdirSync as mkdirSync5,
2492
- existsSync as existsSync6,
2493
- readFileSync as readFileSync4,
2687
+ mkdirSync as mkdirSync6,
2688
+ existsSync as existsSync7,
2689
+ readFileSync as readFileSync5,
2494
2690
  statSync as statSync4,
2495
2691
  renameSync as renameSync4,
2496
2692
  unlinkSync as unlinkSync3,
2497
- readdirSync as readdirSync3,
2693
+ readdirSync as readdirSync4,
2498
2694
  chmodSync
2499
2695
  } from "node:fs";
2500
- import { join as join5 } from "node:path";
2501
- import { homedir as homedir7 } from "node:os";
2696
+ import { join as join6 } from "node:path";
2697
+ import { homedir as homedir8 } from "node:os";
2502
2698
  import { createHash, randomUUID } from "node:crypto";
2503
2699
  function hashCwd(cwd) {
2504
2700
  if (!cwd)
@@ -2528,8 +2724,8 @@ function sanitize(entry) {
2528
2724
  };
2529
2725
  }
2530
2726
  function ensureDir2() {
2531
- if (!existsSync6(QUEUE_DIR)) {
2532
- mkdirSync5(QUEUE_DIR, { recursive: true, mode: 448 });
2727
+ if (!existsSync7(QUEUE_DIR)) {
2728
+ mkdirSync6(QUEUE_DIR, { recursive: true, mode: 448 });
2533
2729
  }
2534
2730
  }
2535
2731
  function appendToServerQueue(entry) {
@@ -2537,7 +2733,7 @@ function appendToServerQueue(entry) {
2537
2733
  return;
2538
2734
  ensureDir2();
2539
2735
  try {
2540
- if (existsSync6(PENDING_FILE) && statSync4(PENDING_FILE).size > MAX_QUEUE_BYTES) {
2736
+ if (existsSync7(PENDING_FILE) && statSync4(PENDING_FILE).size > MAX_QUEUE_BYTES) {
2541
2737
  return;
2542
2738
  }
2543
2739
  } catch {}
@@ -2556,7 +2752,7 @@ function queueSizeBytes() {
2556
2752
  }
2557
2753
  }
2558
2754
  function claimPendingBatch() {
2559
- if (!existsSync6(PENDING_FILE))
2755
+ if (!existsSync7(PENDING_FILE))
2560
2756
  return null;
2561
2757
  try {
2562
2758
  const size = statSync4(PENDING_FILE).size;
@@ -2566,7 +2762,7 @@ function claimPendingBatch() {
2566
2762
  return null;
2567
2763
  }
2568
2764
  const seq = `${Date.now()}-${process.pid}`;
2569
- const processingFile = join5(QUEUE_DIR, `${PROCESSING_PREFIX}${seq}.jsonl`);
2765
+ const processingFile = join6(QUEUE_DIR, `${PROCESSING_PREFIX}${seq}.jsonl`);
2570
2766
  try {
2571
2767
  renameSync4(PENDING_FILE, processingFile);
2572
2768
  try {
@@ -2583,15 +2779,15 @@ function claimPendingBatch() {
2583
2779
  function findOrphanProcessingFiles() {
2584
2780
  ensureDir2();
2585
2781
  try {
2586
- 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();
2587
2783
  } catch {
2588
2784
  return [];
2589
2785
  }
2590
2786
  }
2591
2787
  function readProcessingFile(path2) {
2592
- if (!existsSync6(path2))
2788
+ if (!existsSync7(path2))
2593
2789
  return [];
2594
- const content = readFileSync4(path2, "utf8");
2790
+ const content = readFileSync5(path2, "utf8");
2595
2791
  const out = [];
2596
2792
  for (const line of content.split(`
2597
2793
  `)) {
@@ -2612,20 +2808,20 @@ function deleteProcessingFile(path2) {
2612
2808
  var QUEUE_DIR, PENDING_FILE, PROCESSING_PREFIX = "processing-", MAX_QUEUE_BYTES;
2613
2809
  var init_queue = __esm(() => {
2614
2810
  init_token_store();
2615
- QUEUE_DIR = join5(homedir7(), ".failproofai", "cache", "server-queue");
2616
- PENDING_FILE = join5(QUEUE_DIR, "pending.jsonl");
2811
+ QUEUE_DIR = join6(homedir8(), ".failproofai", "cache", "server-queue");
2812
+ PENDING_FILE = join6(QUEUE_DIR, "pending.jsonl");
2617
2813
  MAX_QUEUE_BYTES = 50 * 1024 * 1024;
2618
2814
  });
2619
2815
 
2620
2816
  // src/relay/pid.ts
2621
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync7, unlinkSync as unlinkSync4, mkdirSync as mkdirSync6 } from "node:fs";
2622
- import { join as join6, dirname as dirname3 } from "node:path";
2623
- 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";
2624
2820
  function readPid() {
2625
- if (!existsSync7(PID_FILE))
2821
+ if (!existsSync8(PID_FILE))
2626
2822
  return null;
2627
2823
  try {
2628
- const raw = readFileSync5(PID_FILE, "utf8").trim();
2824
+ const raw = readFileSync6(PID_FILE, "utf8").trim();
2629
2825
  const pid = parseInt(raw, 10);
2630
2826
  if (Number.isNaN(pid) || pid <= 0)
2631
2827
  return null;
@@ -2635,13 +2831,13 @@ function readPid() {
2635
2831
  }
2636
2832
  }
2637
2833
  function writePid(pid) {
2638
- const dir = dirname3(PID_FILE);
2639
- if (!existsSync7(dir))
2640
- mkdirSync6(dir, { recursive: true, mode: 448 });
2641
- 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));
2642
2838
  }
2643
2839
  function clearPid() {
2644
- if (existsSync7(PID_FILE))
2840
+ if (existsSync8(PID_FILE))
2645
2841
  unlinkSync4(PID_FILE);
2646
2842
  }
2647
2843
  function isProcessAlive(pid) {
@@ -2673,7 +2869,7 @@ function stopRelay() {
2673
2869
  }
2674
2870
  var PID_FILE;
2675
2871
  var init_pid = __esm(() => {
2676
- PID_FILE = join6(homedir8(), ".failproofai", "relay.pid");
2872
+ PID_FILE = join7(homedir9(), ".failproofai", "relay.pid");
2677
2873
  });
2678
2874
 
2679
2875
  // src/relay/daemon.ts
@@ -2685,9 +2881,9 @@ __export(exports_daemon, {
2685
2881
  ensureRelayRunning: () => ensureRelayRunning
2686
2882
  });
2687
2883
  import { spawn } from "node:child_process";
2688
- import { existsSync as existsSync8 } from "node:fs";
2689
- import { join as join7 } from "node:path";
2690
- 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";
2691
2887
  import { randomUUID as randomUUID2 } from "node:crypto";
2692
2888
  function ensureRelayRunning() {
2693
2889
  if (!isLoggedIn())
@@ -2904,7 +3100,7 @@ async function runDaemon() {
2904
3100
  }
2905
3101
  relay.close();
2906
3102
  } catch {}
2907
- if (existsSync8(QUEUE_DIR2)) {}
3103
+ if (existsSync9(QUEUE_DIR2)) {}
2908
3104
  await new Promise((r) => setTimeout(r, reconnectDelay));
2909
3105
  reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
2910
3106
  }
@@ -2954,7 +3150,7 @@ var init_daemon = __esm(() => {
2954
3150
  init_token_store();
2955
3151
  init_pid();
2956
3152
  init_queue();
2957
- QUEUE_DIR2 = join7(homedir9(), ".failproofai", "cache", "server-queue");
3153
+ QUEUE_DIR2 = join8(homedir10(), ".failproofai", "cache", "server-queue");
2958
3154
  });
2959
3155
 
2960
3156
  // src/hooks/handler.ts
@@ -2962,7 +3158,15 @@ var exports_handler = {};
2962
3158
  __export(exports_handler, {
2963
3159
  handleHookEvent: () => handleHookEvent
2964
3160
  });
2965
- 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") {
2966
3170
  const startTime = performance.now();
2967
3171
  const MAX_STDIN_BYTES = 1048576;
2968
3172
  let payload = "";
@@ -2997,12 +3201,15 @@ async function handleHookEvent(eventType) {
2997
3201
  hookLogWarn(`payload parse failed for ${eventType} (${payload.length} bytes)`);
2998
3202
  }
2999
3203
  }
3204
+ const canonicalEventType = canonicalizeEventType(eventType, cli);
3205
+ const sessionId = parsed.session_id;
3000
3206
  const session = {
3001
- sessionId: parsed.session_id,
3207
+ sessionId,
3002
3208
  transcriptPath: parsed.transcript_path,
3003
3209
  cwd: parsed.cwd,
3004
- permissionMode: parsed.permission_mode,
3005
- hookEventName: parsed.hook_event_name
3210
+ permissionMode: resolvePermissionMode(cli, parsed, sessionId),
3211
+ hookEventName: parsed.hook_event_name,
3212
+ cli
3006
3213
  };
3007
3214
  const config = readMergedHooksConfig(session.cwd);
3008
3215
  clearPolicies();
@@ -3030,6 +3237,7 @@ async function handleHookEvent(eventType) {
3030
3237
  hook_name: hookName,
3031
3238
  error_type: isTimeout ? "timeout" : "exception",
3032
3239
  event_type: eventType,
3240
+ cli,
3033
3241
  is_convention_policy: isConvention,
3034
3242
  convention_scope: conventionScope ?? null
3035
3243
  });
@@ -3040,6 +3248,7 @@ async function handleHookEvent(eventType) {
3040
3248
  }
3041
3249
  if (customHooksList.length > 0) {
3042
3250
  trackHookEvent(getInstanceId(), "custom_hooks_loaded", {
3251
+ cli,
3043
3252
  custom_hooks_count: customHooksList.length,
3044
3253
  custom_hook_names: customHooksList.map((h) => h.name),
3045
3254
  event_types_covered: [...new Set(customHooksList.flatMap((h) => h.match?.events ?? []))]
@@ -3047,15 +3256,16 @@ async function handleHookEvent(eventType) {
3047
3256
  }
3048
3257
  if (loadResult.conventionSources.length > 0) {
3049
3258
  trackHookEvent(getInstanceId(), "convention_policies_loaded", {
3050
- event_type: eventType,
3259
+ event_type: canonicalEventType,
3260
+ cli,
3051
3261
  project_file_count: loadResult.conventionSources.filter((s) => s.scope === "project").length,
3052
3262
  user_file_count: loadResult.conventionSources.filter((s) => s.scope === "user").length,
3053
3263
  convention_hook_count: conventionHookNames.size,
3054
3264
  convention_hook_names: [...conventionHookNames]
3055
3265
  });
3056
3266
  }
3057
- hookLogInfo(`event=${eventType} policies=${config.enabledPolicies.length} custom=${customHooksList.length} convention=${conventionHookNames.size}`);
3058
- 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);
3059
3269
  const durationMs = Math.round(performance.now() - startTime);
3060
3270
  hookLogInfo(`result=${result.decision} policy=${result.policyName ?? "none"} duration=${durationMs}ms`);
3061
3271
  if (result.stdout) {
@@ -3066,7 +3276,8 @@ async function handleHookEvent(eventType) {
3066
3276
  }
3067
3277
  const activityEntry = {
3068
3278
  timestamp: Date.now(),
3069
- eventType,
3279
+ eventType: canonicalEventType,
3280
+ integration: cli,
3070
3281
  toolName: parsed.tool_name ?? null,
3071
3282
  policyName: result.policyName,
3072
3283
  policyNames: result.policyNames,
@@ -3101,7 +3312,8 @@ async function handleHookEvent(eventType) {
3101
3312
  const paramKeysOverridden = hasCustomParams ? Object.keys(config.policyParams[result.policyName]) : [];
3102
3313
  const distinctId = getInstanceId();
3103
3314
  await trackHookEvent(distinctId, "hook_policy_triggered", {
3104
- event_type: eventType,
3315
+ event_type: canonicalEventType,
3316
+ cli,
3105
3317
  tool_name: parsed.tool_name ?? null,
3106
3318
  policy_name: result.policyName,
3107
3319
  decision: result.decision,
@@ -3116,12 +3328,14 @@ async function handleHookEvent(eventType) {
3116
3328
  return result.exitCode;
3117
3329
  }
3118
3330
  var init_handler = __esm(() => {
3331
+ init_types();
3119
3332
  init_hooks_config();
3120
3333
  init_builtin_policies();
3121
3334
  init_policy_evaluator();
3122
3335
  init_custom_hooks_loader();
3123
3336
  init_hook_activity_store();
3124
3337
  init_hook_telemetry();
3338
+ init_resolve_permission_mode();
3125
3339
  init_telemetry_id();
3126
3340
  init_hook_logger();
3127
3341
  });
@@ -3135,9 +3349,9 @@ __export(exports_daemon2, {
3135
3349
  ensureRelayRunning: () => ensureRelayRunning2
3136
3350
  });
3137
3351
  import { spawn as spawn2 } from "node:child_process";
3138
- import { existsSync as existsSync9 } from "node:fs";
3139
- import { join as join8 } from "node:path";
3140
- 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";
3141
3355
  import { randomUUID as randomUUID3 } from "node:crypto";
3142
3356
  function ensureRelayRunning2() {
3143
3357
  if (!isLoggedIn())
@@ -3354,7 +3568,7 @@ async function runDaemon2() {
3354
3568
  }
3355
3569
  relay.close();
3356
3570
  } catch {}
3357
- if (existsSync9(QUEUE_DIR3)) {}
3571
+ if (existsSync10(QUEUE_DIR3)) {}
3358
3572
  await new Promise((r) => setTimeout(r, reconnectDelay));
3359
3573
  reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS2);
3360
3574
  }
@@ -3404,41 +3618,279 @@ var init_daemon2 = __esm(() => {
3404
3618
  init_token_store();
3405
3619
  init_pid();
3406
3620
  init_queue();
3407
- QUEUE_DIR3 = join8(homedir10(), ".failproofai", "cache", "server-queue");
3621
+ QUEUE_DIR3 = join9(homedir11(), ".failproofai", "cache", "server-queue");
3408
3622
  });
3409
3623
 
3410
- // src/hooks/types.ts
3411
- var HOOK_SCOPES, HOOK_EVENT_TYPES, FAILPROOFAI_HOOK_MARKER = "__failproofai_hook__";
3412
- var init_types = __esm(() => {
3413
- HOOK_SCOPES = ["user", "project", "local"];
3414
- HOOK_EVENT_TYPES = [
3415
- "SessionStart",
3416
- "SessionEnd",
3417
- "UserPromptSubmit",
3418
- "PreToolUse",
3419
- "PermissionRequest",
3420
- "PermissionDenied",
3421
- "PostToolUse",
3422
- "PostToolUseFailure",
3423
- "Notification",
3424
- "SubagentStart",
3425
- "SubagentStop",
3426
- "TaskCreated",
3427
- "TaskCompleted",
3428
- "Stop",
3429
- "StopFailure",
3430
- "TeammateIdle",
3431
- "InstructionsLoaded",
3432
- "ConfigChange",
3433
- "CwdChanged",
3434
- "FileChanged",
3435
- "WorktreeCreate",
3436
- "WorktreeRemove",
3437
- "PreCompact",
3438
- "PostCompact",
3439
- "Elicitation",
3440
- "ElicitationResult"
3441
- ];
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
+ };
3442
3894
  });
3443
3895
 
3444
3896
  // src/hooks/install-prompt.ts
@@ -3629,7 +4081,7 @@ async function promptPolicySelection(preSelected, options = {}) {
3629
4081
  process.stdout.write(output);
3630
4082
  lastLineCount = lines.length;
3631
4083
  }
3632
- return new Promise((resolve5) => {
4084
+ return new Promise((resolve6) => {
3633
4085
  render();
3634
4086
  process.stdin.setRawMode(true);
3635
4087
  process.stdin.resume();
@@ -3671,7 +4123,7 @@ async function promptPolicySelection(preSelected, options = {}) {
3671
4123
  const selected = items.filter((i) => i.selected).map((i) => i.name);
3672
4124
  process.stdout.write(`
3673
4125
  `);
3674
- resolve5(selected);
4126
+ resolve6(selected);
3675
4127
  } else if (key.name === "backspace" || key.name === "delete") {
3676
4128
  if (search.length > 0) {
3677
4129
  search = search.slice(0, -1);
@@ -3695,6 +4147,7 @@ async function promptPolicySelection(preSelected, options = {}) {
3695
4147
  }
3696
4148
  var init_install_prompt = __esm(() => {
3697
4149
  init_builtin_policies();
4150
+ init_integrations();
3698
4151
  });
3699
4152
 
3700
4153
  // src/cli-error.ts
@@ -3719,20 +4172,12 @@ __export(exports_manager, {
3719
4172
  hooksInstalledInSettings: () => hooksInstalledInSettings,
3720
4173
  getSettingsPath: () => getSettingsPath
3721
4174
  });
3722
- import { execSync as execSync3 } from "node:child_process";
3723
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync10, mkdirSync as mkdirSync7 } from "node:fs";
3724
- import { resolve as resolve5, dirname as dirname4, basename as basename2 } from "node:path";
3725
- 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";
3726
4179
  function getSettingsPath(scope, cwd) {
3727
- const base = cwd ? resolve5(cwd) : process.cwd();
3728
- switch (scope) {
3729
- case "user":
3730
- return resolve5(homedir11(), ".claude", "settings.json");
3731
- case "project":
3732
- return resolve5(base, ".claude", "settings.json");
3733
- case "local":
3734
- return resolve5(base, ".claude", "settings.local.json");
3735
- }
4180
+ return claudeCode.getSettingsPath(scope, cwd);
3736
4181
  }
3737
4182
  function scopeLabel(scope) {
3738
4183
  switch (scope) {
@@ -3744,22 +4189,13 @@ function scopeLabel(scope) {
3744
4189
  return `{cwd}/.claude/settings.local.json`;
3745
4190
  }
3746
4191
  }
3747
- function readSettings(settingsPath) {
3748
- if (!existsSync10(settingsPath)) {
3749
- return {};
3750
- }
3751
- const raw = readFileSync6(settingsPath, "utf8");
3752
- return JSON.parse(raw);
3753
- }
3754
- function writeSettings(settingsPath, settings) {
3755
- mkdirSync7(dirname4(settingsPath), { recursive: true });
3756
- writeFileSync5(settingsPath, JSON.stringify(settings, null, 2) + `
3757
- `, "utf8");
3758
- }
3759
4192
  function resolveFailproofaiBinary() {
4193
+ const override = process.env.FAILPROOFAI_BINARY_OVERRIDE;
4194
+ if (override && override.trim())
4195
+ return override.trim();
3760
4196
  try {
3761
4197
  const cmd = process.platform === "win32" ? "where failproofai" : "which failproofai";
3762
- const result = execSync3(cmd, { encoding: "utf8" }).trim();
4198
+ const result = execSync4(cmd, { encoding: "utf8" }).trim();
3763
4199
  return result.split(`
3764
4200
  `)[0].trim();
3765
4201
  } catch {
@@ -3767,12 +4203,6 @@ function resolveFailproofaiBinary() {
3767
4203
  ` + "Install it globally first: npm install -g failproofai");
3768
4204
  }
3769
4205
  }
3770
- function isFailproofaiHook(hook) {
3771
- if (hook[FAILPROOFAI_HOOK_MARKER] === true)
3772
- return true;
3773
- const cmd = typeof hook.command === "string" ? hook.command : "";
3774
- return cmd.includes("failproofai") && cmd.includes("--hook");
3775
- }
3776
4206
  function validatePolicyNames(names) {
3777
4207
  const invalid = names.filter((n) => !VALID_POLICY_NAMES.has(n));
3778
4208
  if (invalid.length > 0) {
@@ -3792,58 +4222,9 @@ function deduplicateScopes(scopes, cwd) {
3792
4222
  });
3793
4223
  }
3794
4224
  function hooksInstalledInSettings(scope, cwd) {
3795
- const settingsPath = getSettingsPath(scope, cwd);
3796
- if (!existsSync10(settingsPath))
3797
- return false;
3798
- try {
3799
- const settings = readSettings(settingsPath);
3800
- if (!settings.hooks)
3801
- return false;
3802
- for (const matchers of Object.values(settings.hooks)) {
3803
- if (!Array.isArray(matchers))
3804
- continue;
3805
- for (const matcher of matchers) {
3806
- if (!matcher.hooks)
3807
- continue;
3808
- if (matcher.hooks.some((h) => isFailproofaiHook(h))) {
3809
- return true;
3810
- }
3811
- }
3812
- }
3813
- } catch {}
3814
- return false;
3815
- }
3816
- function removeHooksFromSettingsFile(settingsPath) {
3817
- const settings = readSettings(settingsPath);
3818
- if (!settings.hooks)
3819
- return 0;
3820
- let removed = 0;
3821
- for (const eventType of Object.keys(settings.hooks)) {
3822
- const matchers = settings.hooks[eventType];
3823
- if (!Array.isArray(matchers))
3824
- continue;
3825
- for (let i = matchers.length - 1;i >= 0; i--) {
3826
- const matcher = matchers[i];
3827
- if (!matcher.hooks)
3828
- continue;
3829
- const before = matcher.hooks.length;
3830
- matcher.hooks = matcher.hooks.filter((h) => !isFailproofaiHook(h));
3831
- removed += before - matcher.hooks.length;
3832
- if (matcher.hooks.length === 0) {
3833
- matchers.splice(i, 1);
3834
- }
3835
- }
3836
- if (matchers.length === 0) {
3837
- delete settings.hooks[eventType];
3838
- }
3839
- }
3840
- if (Object.keys(settings.hooks).length === 0) {
3841
- delete settings.hooks;
3842
- }
3843
- writeSettings(settingsPath, settings);
3844
- return removed;
4225
+ return claudeCode.hooksInstalledInSettings(scope, cwd);
3845
4226
  }
3846
- 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) {
3847
4228
  if (policyNames !== undefined && policyNames.length > 0) {
3848
4229
  const nonAllNames = policyNames.filter((n) => n !== "all");
3849
4230
  if (nonAllNames.length > 0)
@@ -3853,6 +4234,13 @@ async function installHooks(policyNames, scope = "user", cwd, includeBeta = fals
3853
4234
  ` + `Use either: --install all or --install block-sudo sanitize-jwt ...`);
3854
4235
  }
3855
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
+ }
3856
4244
  const binaryPath = resolveFailproofaiBinary();
3857
4245
  const previousConfig = readScopedHooksConfig(scope, cwd);
3858
4246
  const previousEnabled = new Set(previousConfig.enabledPolicies);
@@ -3873,7 +4261,7 @@ async function installHooks(policyNames, scope = "user", cwd, includeBeta = fals
3873
4261
  if (removeCustomHooks) {
3874
4262
  delete configToWrite.customPoliciesPath;
3875
4263
  } else if (customPoliciesPath) {
3876
- configToWrite.customPoliciesPath = resolve5(customPoliciesPath);
4264
+ configToWrite.customPoliciesPath = resolve6(customPoliciesPath);
3877
4265
  let validatedHooks = [];
3878
4266
  try {
3879
4267
  validatedHooks = await loadCustomHooks(configToWrite.customPoliciesPath, { strict: true });
@@ -3896,39 +4284,15 @@ Enabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}`)
3896
4284
  } else if (configToWrite.customPoliciesPath) {
3897
4285
  console.log(`Custom hooks path: ${configToWrite.customPoliciesPath}`);
3898
4286
  }
3899
- const settingsPath = getSettingsPath(scope, cwd);
3900
- const settings = readSettings(settingsPath);
3901
- if (!settings.hooks) {
3902
- settings.hooks = {};
3903
- }
3904
- for (const eventType of HOOK_EVENT_TYPES) {
3905
- const command = scope === "project" ? `npx -y failproofai --hook ${eventType}` : `"${binaryPath}" --hook ${eventType}`;
3906
- const hookEntry = {
3907
- type: "command",
3908
- command,
3909
- timeout: 60000,
3910
- [FAILPROOFAI_HOOK_MARKER]: true
3911
- };
3912
- if (!settings.hooks[eventType]) {
3913
- settings.hooks[eventType] = [];
3914
- }
3915
- const matchers = settings.hooks[eventType];
3916
- let found = false;
3917
- for (const matcher of matchers) {
3918
- if (!matcher.hooks)
3919
- continue;
3920
- const failproofaiIdx = matcher.hooks.findIndex((h) => isFailproofaiHook(h));
3921
- if (failproofaiIdx >= 0) {
3922
- matcher.hooks[failproofaiIdx] = hookEntry;
3923
- found = true;
3924
- break;
3925
- }
3926
- }
3927
- if (!found) {
3928
- matchers.push({ hooks: [hookEntry] });
3929
- }
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 });
3930
4295
  }
3931
- writeSettings(settingsPath, settings);
3932
4296
  try {
3933
4297
  const newSet = new Set(selectedPolicies);
3934
4298
  const policiesAdded = selectedPolicies.filter((p) => !previousEnabled.has(p));
@@ -3936,6 +4300,8 @@ Enabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}`)
3936
4300
  const distinctId = getInstanceId();
3937
4301
  await trackHookEvent(distinctId, "hooks_installed", {
3938
4302
  scope,
4303
+ cli: selectedClis,
4304
+ cli_count: selectedClis.length,
3939
4305
  policies: selectedPolicies,
3940
4306
  policy_count: selectedPolicies.length,
3941
4307
  policies_added: policiesAdded,
@@ -3951,8 +4317,11 @@ Enabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}`)
3951
4317
  command_format: scope === "project" ? "npx" : "absolute"
3952
4318
  });
3953
4319
  } catch {}
3954
- console.log(`Failproof AI hooks installed for all ${HOOK_EVENT_TYPES.length} event types (scope: ${scope}).`);
3955
- 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
+ }
3956
4325
  if (scope === "project") {
3957
4326
  console.log(`Command: npx -y failproofai`);
3958
4327
  console.log(`
@@ -3973,6 +4342,7 @@ This file can be committed to git — no machine-specific paths.`);
3973
4342
  }
3974
4343
  async function removeHooks(policyNames, scope = "user", cwd, opts) {
3975
4344
  const configScope = scope === "all" ? "user" : scope;
4345
+ const selectedClis = opts?.cli && opts.cli.length > 0 ? [...new Set(opts.cli)] : ["claude"];
3976
4346
  if (opts?.removeCustomHooks) {
3977
4347
  const config = readScopedHooksConfig(configScope, cwd);
3978
4348
  delete config.customPoliciesPath;
@@ -4001,6 +4371,7 @@ async function removeHooks(policyNames, scope = "user", cwd, opts) {
4001
4371
  const actuallyRemoved = policyNames.filter((p) => config.enabledPolicies.includes(p));
4002
4372
  await trackHookEvent(distinctId, "hooks_removed", {
4003
4373
  scope,
4374
+ cli: selectedClis,
4004
4375
  removal_mode: opts?.betaOnly ? "beta_policies" : "policies",
4005
4376
  beta_only: opts?.betaOnly ?? false,
4006
4377
  policies_removed: actuallyRemoved,
@@ -4017,42 +4388,49 @@ async function removeHooks(policyNames, scope = "user", cwd, opts) {
4017
4388
  return;
4018
4389
  }
4019
4390
  const configBeforeRemoval = readScopedHooksConfig(configScope, cwd);
4020
- const scopesToRemove = scope === "all" ? [...HOOK_SCOPES] : [scope];
4021
4391
  let totalRemoved = 0;
4022
- for (const s of scopesToRemove) {
4023
- const settingsPath = getSettingsPath(s, cwd);
4024
- if (!existsSync10(settingsPath)) {
4025
- if (scope !== "all") {
4026
- console.log("No settings file found. Nothing to remove.");
4027
- 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;
4028
4404
  }
4029
- continue;
4030
- }
4031
- const settings = readSettings(settingsPath);
4032
- if (!settings.hooks) {
4033
- if (scope !== "all") {
4405
+ const removed = integration.removeHooksFromFile(settingsPath);
4406
+ if (removed === 0 && scope !== "all" && selectedClis.length === 1) {
4034
4407
  console.log("No hooks found in settings. Nothing to remove.");
4035
- 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}`);
4036
4415
  }
4037
- continue;
4038
- }
4039
- const removed = removeHooksFromSettingsFile(settingsPath);
4040
- totalRemoved += removed;
4041
- if (scope !== "all") {
4042
- console.log(`Removed ${removed} failproofai hook(s) from settings.`);
4043
- console.log(`Settings: ${settingsPath}`);
4044
4416
  }
4045
4417
  }
4418
+ if (nothingToReport && totalRemoved === 0)
4419
+ return;
4046
4420
  if (scope === "all") {
4047
4421
  console.log(`Removed ${totalRemoved} failproofai hook(s) from all scopes.`);
4048
- for (const s of scopesToRemove) {
4049
- 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
+ }
4050
4427
  }
4051
4428
  }
4052
4429
  try {
4053
4430
  const distinctId = getInstanceId();
4054
4431
  await trackHookEvent(distinctId, "hooks_removed", {
4055
4432
  scope,
4433
+ cli: selectedClis,
4056
4434
  removal_mode: "hooks",
4057
4435
  policies_removed: configBeforeRemoval.enabledPolicies,
4058
4436
  removed_count: totalRemoved,
@@ -4196,7 +4574,7 @@ Failproof AI Hook Policies
4196
4574
  if (config.customPoliciesPath) {
4197
4575
  console.log(`
4198
4576
  ── Custom Policies (${config.customPoliciesPath}) ───────────────────────`);
4199
- if (!existsSync10(config.customPoliciesPath)) {
4577
+ if (!existsSync12(config.customPoliciesPath)) {
4200
4578
  console.log(` \x1B[31m✗ File not found: ${config.customPoliciesPath}\x1B[0m`);
4201
4579
  } else {
4202
4580
  const hooks = await loadCustomHooks(config.customPoliciesPath);
@@ -4211,10 +4589,10 @@ Failproof AI Hook Policies
4211
4589
  }
4212
4590
  console.log();
4213
4591
  }
4214
- const base = cwd ? resolve5(cwd) : process.cwd();
4592
+ const base = cwd ? resolve6(cwd) : process.cwd();
4215
4593
  const conventionDirs = [
4216
- { label: "Project", dir: resolve5(base, ".failproofai", "policies") },
4217
- { label: "User", dir: resolve5(homedir11(), ".failproofai", "policies") }
4594
+ { label: "Project", dir: resolve6(base, ".failproofai", "policies") },
4595
+ { label: "User", dir: resolve6(homedir13(), ".failproofai", "policies") }
4218
4596
  ];
4219
4597
  for (const { label, dir } of conventionDirs) {
4220
4598
  const files = discoverPolicyFiles(dir);
@@ -4244,6 +4622,7 @@ Failproof AI Hook Policies
4244
4622
  var VALID_POLICY_NAMES;
4245
4623
  var init_manager = __esm(() => {
4246
4624
  init_types();
4625
+ init_integrations();
4247
4626
  init_install_prompt();
4248
4627
  init_hooks_config();
4249
4628
  init_builtin_policies();
@@ -4254,6 +4633,316 @@ var init_manager = __esm(() => {
4254
4633
  VALID_POLICY_NAMES = new Set(BUILTIN_POLICIES.map((p) => p.name));
4255
4634
  });
4256
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
+
4257
4946
  // src/auth/login.ts
4258
4947
  var exports_login = {};
4259
4948
  __export(exports_login, {
@@ -4389,14 +5078,14 @@ __export(exports_pid, {
4389
5078
  isProcessAlive: () => isProcessAlive2,
4390
5079
  clearPid: () => clearPid2
4391
5080
  });
4392
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync11, unlinkSync as unlinkSync5, mkdirSync as mkdirSync8 } from "node:fs";
4393
- import { join as join9, dirname as dirname5 } from "node:path";
4394
- 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";
4395
5084
  function readPid2() {
4396
- if (!existsSync11(PID_FILE2))
5085
+ if (!existsSync13(PID_FILE2))
4397
5086
  return null;
4398
5087
  try {
4399
- const raw = readFileSync7(PID_FILE2, "utf8").trim();
5088
+ const raw = readFileSync8(PID_FILE2, "utf8").trim();
4400
5089
  const pid = parseInt(raw, 10);
4401
5090
  if (Number.isNaN(pid) || pid <= 0)
4402
5091
  return null;
@@ -4406,13 +5095,13 @@ function readPid2() {
4406
5095
  }
4407
5096
  }
4408
5097
  function writePid2(pid) {
4409
- const dir = dirname5(PID_FILE2);
4410
- if (!existsSync11(dir))
4411
- mkdirSync8(dir, { recursive: true, mode: 448 });
4412
- 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));
4413
5102
  }
4414
5103
  function clearPid2() {
4415
- if (existsSync11(PID_FILE2))
5104
+ if (existsSync13(PID_FILE2))
4416
5105
  unlinkSync5(PID_FILE2);
4417
5106
  }
4418
5107
  function isProcessAlive2(pid) {
@@ -4450,26 +5139,26 @@ function relayStatus() {
4450
5139
  }
4451
5140
  var PID_FILE2;
4452
5141
  var init_pid2 = __esm(() => {
4453
- PID_FILE2 = join9(homedir12(), ".failproofai", "relay.pid");
5142
+ PID_FILE2 = join10(homedir14(), ".failproofai", "relay.pid");
4454
5143
  });
4455
5144
 
4456
5145
  // lib/paths.ts
4457
- import { homedir as homedir13 } from "os";
4458
- import { join as join10 } from "path";
5146
+ import { homedir as homedir15 } from "os";
5147
+ import { join as join11 } from "path";
4459
5148
  function getDefaultClaudeProjectsPath() {
4460
- return join10(homedir13(), ".claude", "projects");
5149
+ return join11(homedir15(), ".claude", "projects");
4461
5150
  }
4462
5151
  var init_paths = () => {};
4463
5152
 
4464
5153
  // scripts/parse-script-args.ts
4465
- import { resolve as resolve6 } from "path";
5154
+ import { resolve as resolve7 } from "path";
4466
5155
  function parseStringFlag(flagName, errorLabel, inlineValue, args, index, options) {
4467
5156
  const raw = inlineValue ?? args[index + 1];
4468
5157
  if (raw === undefined || inlineValue === null && raw.startsWith("-")) {
4469
5158
  console.error(`Error: ${flagName} requires ${errorLabel}`);
4470
5159
  process.exit(1);
4471
5160
  }
4472
- const value = options?.resolve ? resolve6(raw) : raw;
5161
+ const value = options?.resolve ? resolve7(raw) : raw;
4473
5162
  return { value, spliceCount: inlineValue !== null ? 1 : 2 };
4474
5163
  }
4475
5164
  function parseScriptArgs(argv) {
@@ -4530,8 +5219,8 @@ __export(exports_launch, {
4530
5219
  launch: () => launch
4531
5220
  });
4532
5221
  import { spawn as spawn4 } from "child_process";
4533
- import { realpathSync, existsSync as existsSync12 } from "node:fs";
4534
- 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";
4535
5224
  import { fileURLToPath } from "node:url";
4536
5225
  function launch(mode) {
4537
5226
  const { claudeProjectsPath: parsedPath, loggingLevel, disableTelemetry, allowedDevOrigins, remainingArgs } = parseScriptArgs(process.argv.slice(2));
@@ -4562,9 +5251,9 @@ function launch(mode) {
4562
5251
  process.env.PORT = port;
4563
5252
  process.env.HOSTNAME = "0.0.0.0";
4564
5253
  cmd = "node";
4565
- const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT ?? resolve7(dirname6(realpathSync(fileURLToPath(import.meta.url))), "..");
4566
- const serverJsPath = resolve7(packageRoot, ".next/standalone/server.js");
4567
- 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)) {
4568
5257
  console.error(`
4569
5258
  Error: Cannot find server.js at:
4570
5259
  ${serverJsPath}
@@ -4623,17 +5312,17 @@ var init_cli_error2 = __esm(() => {
4623
5312
 
4624
5313
  // bin/failproofai.mjs
4625
5314
  import { realpathSync as realpathSync2 } from "fs";
4626
- import { dirname as dirname7, resolve as resolve8 } from "path";
5315
+ import { dirname as dirname8, resolve as resolve9 } from "path";
4627
5316
  import { fileURLToPath as fileURLToPath2 } from "url";
4628
5317
  // package.json
4629
- var version = "0.0.8";
5318
+ var version = "0.0.9-beta.0";
4630
5319
 
4631
5320
  // bin/failproofai.mjs
4632
5321
  if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
4633
- 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))), "..");
4634
5323
  }
4635
5324
  if (!process.env.FAILPROOFAI_DIST_PATH) {
4636
- 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");
4637
5326
  }
4638
5327
  var args = process.argv.slice(2);
4639
5328
  if (args[0] === "p")
@@ -4642,12 +5331,16 @@ var hookIdx = args.indexOf("--hook");
4642
5331
  if (hookIdx >= 0) {
4643
5332
  if (!args[hookIdx + 1]) {
4644
5333
  console.error("Error: Missing event type after --hook");
4645
- console.error("Usage: failproofai --hook <event> (e.g. PreToolUse, PostToolUse)");
5334
+ console.error("Usage: failproofai --hook <event> [--cli <claude|codex>]");
4646
5335
  process.exit(1);
4647
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";
4648
5341
  try {
4649
5342
  const { handleHookEvent: handleHookEvent2 } = await Promise.resolve().then(() => (init_handler(), exports_handler));
4650
- const exitCode = await handleHookEvent2(args[hookIdx + 1]);
5343
+ const exitCode = await handleHookEvent2(eventType, cli);
4651
5344
  process.exit(exitCode);
4652
5345
  } catch (err) {
4653
5346
  const msg = err instanceof Error ? err.message : String(err);
@@ -4684,14 +5377,19 @@ COMMANDS
4684
5377
  (no args) Launch the policy dashboard
4685
5378
 
4686
5379
  policies, p List all available policies and their status
4687
- policies --install, -i Enable policies in Claude Code settings
5380
+ policies --install, -i Enable policies in agent CLI settings
4688
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.
4689
5385
  --scope user|project|local Config scope to write to (default: user)
5386
+ (Codex supports user|project only)
4690
5387
  --beta Include beta policies
4691
5388
  --custom, -c <path> Path to a JS file of custom policies
4692
5389
 
4693
5390
  policies --uninstall, -u Disable policies or remove hooks
4694
5391
  [names...] Specific policy names to disable
5392
+ --cli claude|codex Agent CLI(s) to uninstall from
4695
5393
  --scope user|project|local|all Config scope to remove from (default: user)
4696
5394
  --beta Remove only beta policies
4697
5395
  --custom, -c Clear the customPoliciesPath from config
@@ -4716,9 +5414,12 @@ EXAMPLES
4716
5414
  failproofai policies
4717
5415
  failproofai policies --install
4718
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
4719
5419
  failproofai policies --install --custom ./my-policies.js
4720
5420
  failproofai policies -i -c ./my-policies.js
4721
5421
  failproofai policies --uninstall block-sudo
5422
+ failproofai policies --uninstall --cli codex
4722
5423
  failproofai policies --uninstall --custom
4723
5424
 
4724
5425
  LINKS
@@ -4752,13 +5453,19 @@ USAGE
4752
5453
 
4753
5454
  OPTIONS (install)
4754
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).
4755
5460
  --scope user|project|local Config scope to write to (default: user)
5461
+ (Codex supports user|project only)
4756
5462
  --beta Include beta policies
4757
5463
  --custom, -c <path> Path to a JS file of custom policies
4758
5464
  (skips interactive prompt; validates file first)
4759
5465
 
4760
5466
  OPTIONS (uninstall)
4761
5467
  [names...] Specific policy names to disable (omit to remove hooks)
5468
+ --cli claude|codex Agent CLI(s) to uninstall from
4762
5469
  --scope user|project|local|all Config scope to remove from (default: user)
4763
5470
  --beta Remove only beta policies
4764
5471
  --custom, -c Clear the customPoliciesPath from config
@@ -4767,9 +5474,12 @@ EXAMPLES
4767
5474
  failproofai policies
4768
5475
  failproofai policies --install
4769
5476
  failproofai policies --install block-sudo sanitize-api-keys
5477
+ failproofai policies --install --cli codex --scope project
5478
+ failproofai policies --install --cli claude codex
4770
5479
  failproofai policies --install --custom ./my-policies.js
4771
5480
  failproofai policies -i -c ./my-policies.js
4772
5481
  failproofai policies --uninstall block-sudo
5482
+ failproofai policies --uninstall --cli codex
4773
5483
  failproofai policies -u
4774
5484
  failproofai policies --uninstall --custom
4775
5485
  `.trimStart());
@@ -4777,6 +5487,7 @@ EXAMPLES
4777
5487
  }
4778
5488
  if (isInstall) {
4779
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));
4780
5491
  const scopeIdx = subArgs.indexOf("--scope");
4781
5492
  const scope = scopeIdx >= 0 ? subArgs[scopeIdx + 1] : "user";
4782
5493
  if (scopeIdx >= 0 && (!scope || scope.startsWith("-"))) {
@@ -4791,13 +5502,35 @@ EXAMPLES
4791
5502
  throw new CliError3(`Missing path after --custom/-c
4792
5503
  Usage: --custom <path> (e.g. --custom ./my-policies.js)`);
4793
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
+ }
4794
5525
  const includeBeta = subArgs.includes("--beta");
4795
5526
  const consumedIdxs = new Set;
4796
5527
  if (scopeIdx >= 0)
4797
5528
  consumedIdxs.add(scopeIdx + 1);
4798
5529
  if (customIdx >= 0)
4799
5530
  consumedIdxs.add(customIdx + 1);
4800
- 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"]);
4801
5534
  const unknownInstallFlag = subArgs.find((a) => a.startsWith("-") && !flags.has(a));
4802
5535
  if (unknownInstallFlag) {
4803
5536
  throw new CliError3(`Unknown flag: ${unknownInstallFlag}
@@ -4805,11 +5538,13 @@ Run \`failproofai policies --help\` for usage.`);
4805
5538
  }
4806
5539
  const explicitPolicyNames = subArgs.filter((a, idx) => !a.startsWith("-") && !consumedIdxs.has(idx));
4807
5540
  const policyNames = explicitPolicyNames.length > 0 ? explicitPolicyNames : customPoliciesPath !== undefined ? [] : undefined;
4808
- 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);
4809
5543
  process.exit(0);
4810
5544
  }
4811
5545
  if (isUninstall) {
4812
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));
4813
5548
  const scopeIdx = subArgs.indexOf("--scope");
4814
5549
  const scope = scopeIdx >= 0 ? subArgs[scopeIdx + 1] : "user";
4815
5550
  if (scopeIdx >= 0 && (!scope || scope.startsWith("-"))) {
@@ -4818,19 +5553,42 @@ Run \`failproofai policies --help\` for usage.`);
4818
5553
  if (scopeIdx >= 0 && !["user", "project", "local", "all"].includes(scope)) {
4819
5554
  throw new CliError3(`Invalid scope: ${scope}. Valid values: user, project, local, all`);
4820
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
+ }
4821
5576
  const betaOnly = subArgs.includes("--beta");
4822
5577
  const removeCustomHooks = subArgs.includes("--custom") || subArgs.includes("-c");
4823
5578
  const consumedIdxs = new Set;
4824
5579
  if (scopeIdx >= 0)
4825
5580
  consumedIdxs.add(scopeIdx + 1);
4826
- 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"]);
4827
5584
  const unknownUninstallFlag = subArgs.find((a) => a.startsWith("-") && !flags.has(a));
4828
5585
  if (unknownUninstallFlag) {
4829
5586
  throw new CliError3(`Unknown flag: ${unknownUninstallFlag}
4830
5587
  Run \`failproofai policies --help\` for usage.`);
4831
5588
  }
4832
5589
  const policyNames = subArgs.filter((a, idx) => !a.startsWith("-") && !consumedIdxs.has(idx));
4833
- 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 });
4834
5592
  process.exit(0);
4835
5593
  }
4836
5594
  const knownListFlags = new Set(["--install", "-i", "--uninstall", "-u", "--help", "-h", "--list"]);