failproofai 0.0.2-beta.6 → 0.0.2-beta.7

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 (159) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +5 -5
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
  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/build-manifest.json +2 -2
  17. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  21. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  22. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  23. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  24. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  25. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  26. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  27. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  28. package/.next/standalone/.next/server/app/index.html +1 -1
  29. package/.next/standalone/.next/server/app/index.rsc +15 -15
  30. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  31. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  32. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  33. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  34. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  35. package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
  36. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  37. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  39. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
  40. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  41. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  43. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  44. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  50. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  52. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  53. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  54. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/chunks/[root-of-the-server]__02nt~6d._.js +1 -1
  57. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_05pz9._._.js +1 -1
  58. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
  59. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0u_n1xe._.js → [root-of-the-server]__05zi2mt._.js} +2 -2
  60. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +2 -2
  61. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09icjsf._.js +2 -2
  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]__0epc5zr._.js → [root-of-the-server]__0kkt_9z._.js} +2 -2
  65. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0okos0k._.js +2 -2
  66. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +8 -9
  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/_10lm7or._.js +2 -2
  70. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0a_7sdg.js +2 -2
  73. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0ef3uwk.js +2 -2
  74. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0j79~gv.js +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0pbja1x.js +2 -2
  76. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0r6o0i2.js +2 -2
  77. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_11y81~_.js +2 -2
  78. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_12or2kf.js +2 -2
  79. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_0mebn66._.js +1 -1
  80. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  81. package/.next/standalone/.next/server/pages/404.html +2 -2
  82. package/.next/standalone/.next/server/pages/500.html +1 -1
  83. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  84. package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
  85. package/.next/standalone/.next/static/chunks/{0tood0~87-mm8.js → 02u4v.k5amfah.js} +1 -1
  86. package/.next/standalone/.next/static/chunks/{031pa5~qfzt~_.js → 09e7drilkf1sn.js} +1 -1
  87. package/.next/standalone/.next/static/chunks/{0jqg886bw85_6.js → 0bkizbynk9via.js} +1 -1
  88. package/.next/standalone/.next/static/chunks/{17p200_z1ivz4.js → 0e76l4~hq_sei.js} +1 -1
  89. package/.next/standalone/.next/static/chunks/{0wkzaq-8sxss7.js → 0ltx5i0xv85_s.js} +1 -1
  90. package/.next/standalone/.next/static/chunks/{0efsuf1p-k4qe.js → 0q7atesxo-36k.js} +1 -1
  91. package/.next/standalone/.next/static/chunks/{0rvepm.~uvks4.js → 0suauczjqzn07.js} +1 -1
  92. package/.next/standalone/.next/static/chunks/{0kbfx4p.g9wnr.js → 0w.rtg9.m8dk-.js} +2 -2
  93. package/.next/standalone/.next/static/chunks/{0_tx_~f8pi3d7.js → 13jdpvk~s2da8.js} +1 -1
  94. package/.next/standalone/.next/static/chunks/{turbopack-0uc5y~g6h.n7-.js → turbopack-0r26pc8h0y_-e.js} +1 -1
  95. package/.next/standalone/CHANGELOG.md +74 -0
  96. package/.next/standalone/CLAUDE.md +14 -0
  97. package/.next/standalone/README.md +20 -3
  98. package/.next/standalone/bin/failproofai.mjs +5 -0
  99. package/.next/standalone/bun.lock +31 -63
  100. package/.next/standalone/dist/cli.mjs +242 -61
  101. package/.next/standalone/docs/built-in-policies.mdx +2 -2
  102. package/.next/standalone/docs/configuration.mdx +46 -0
  103. package/.next/standalone/docs/custom-policies.mdx +63 -5
  104. package/.next/standalone/docs/docs.json +3 -3
  105. package/.next/standalone/examples/convention-policies/security-policies.mjs +40 -0
  106. package/.next/standalone/examples/convention-policies/workflow-policies.mjs +41 -0
  107. package/.next/standalone/node_modules/@next/env/package.json +1 -1
  108. package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
  109. package/.next/standalone/node_modules/next/dist/compiled/jsonwebtoken/index.js +2 -2
  110. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +1 -1
  111. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +1 -1
  112. package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +1 -1
  113. package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
  114. package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
  115. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +7 -2
  116. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
  117. package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
  118. package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
  119. package/.next/standalone/node_modules/next/dist/server/render.js +20 -19
  120. package/.next/standalone/node_modules/next/dist/shared/lib/errors/canary-only-config-error.js +1 -1
  121. package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
  122. package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
  123. package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
  124. package/.next/standalone/node_modules/next/package.json +15 -15
  125. package/.next/standalone/node_modules/react/cjs/react.development.js +1 -1
  126. package/.next/standalone/node_modules/react/cjs/react.production.js +1 -1
  127. package/.next/standalone/node_modules/react/package.json +1 -1
  128. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.browser.production.js +1 -1
  129. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.node.production.js +1 -1
  130. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.browser.production.js +3 -3
  131. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.edge.production.js +3 -3
  132. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.node.production.js +3 -3
  133. package/.next/standalone/node_modules/react-dom/cjs/react-dom.production.js +1 -1
  134. package/.next/standalone/node_modules/react-dom/package.json +2 -2
  135. package/.next/standalone/package.json +1 -1
  136. package/.next/standalone/server.js +1 -1
  137. package/.next/standalone/src/hooks/builtin-policies.ts +70 -18
  138. package/.next/standalone/src/hooks/custom-hooks-loader.ts +158 -21
  139. package/.next/standalone/src/hooks/handler.ts +26 -6
  140. package/.next/standalone/src/hooks/hooks-config.ts +47 -2
  141. package/.next/standalone/src/hooks/llm-client.ts +2 -2
  142. package/.next/standalone/src/hooks/loader-utils.ts +4 -4
  143. package/.next/standalone/src/hooks/manager.ts +57 -14
  144. package/.next/standalone/src/hooks/policy-evaluator.ts +16 -2
  145. package/README.md +20 -3
  146. package/bin/failproofai.mjs +5 -0
  147. package/dist/cli.mjs +242 -61
  148. package/package.json +1 -1
  149. package/src/hooks/builtin-policies.ts +70 -18
  150. package/src/hooks/custom-hooks-loader.ts +158 -21
  151. package/src/hooks/handler.ts +26 -6
  152. package/src/hooks/hooks-config.ts +47 -2
  153. package/src/hooks/llm-client.ts +2 -2
  154. package/src/hooks/loader-utils.ts +4 -4
  155. package/src/hooks/manager.ts +57 -14
  156. package/src/hooks/policy-evaluator.ts +16 -2
  157. /package/.next/standalone/.next/static/{gDMch26rYN-bU-9f6ftKR → Opbai6exOQP2W488FWmr6}/_buildManifest.js +0 -0
  158. /package/.next/standalone/.next/static/{gDMch26rYN-bU-9f6ftKR → Opbai6exOQP2W488FWmr6}/_clientMiddlewareManifest.js +0 -0
  159. /package/.next/standalone/.next/static/{gDMch26rYN-bU-9f6ftKR → Opbai6exOQP2W488FWmr6}/_ssgManifest.js +0 -0
@@ -149,11 +149,19 @@ function readMergedHooksConfig(cwd) {
149
149
  ...llm !== undefined ? { llm } : {}
150
150
  };
151
151
  }
152
- function getConfigPath() {
153
- return resolve(homedir2(), ".failproofai", "policies-config.json");
152
+ function getConfigPathForScope(scope, cwd) {
153
+ const base = cwd ? resolve(cwd) : process.cwd();
154
+ switch (scope) {
155
+ case "user":
156
+ return resolve(homedir2(), ".failproofai", "policies-config.json");
157
+ case "project":
158
+ return resolve(base, ".failproofai", "policies-config.json");
159
+ case "local":
160
+ return resolve(base, ".failproofai", "policies-config.local.json");
161
+ }
154
162
  }
155
- function readHooksConfig() {
156
- const configPath = getConfigPath();
163
+ function readScopedHooksConfig(scope, cwd) {
164
+ const configPath = getConfigPathForScope(scope, cwd);
157
165
  if (!existsSync2(configPath)) {
158
166
  return { enabledPolicies: [] };
159
167
  }
@@ -165,8 +173,8 @@ function readHooksConfig() {
165
173
  return { enabledPolicies: [] };
166
174
  }
167
175
  }
168
- function writeHooksConfig(config) {
169
- const configPath = getConfigPath();
176
+ function writeScopedHooksConfig(config, scope, cwd) {
177
+ const configPath = getConfigPathForScope(scope, cwd);
170
178
  const dir = dirname(configPath);
171
179
  if (!existsSync2(dir)) {
172
180
  mkdirSync2(dir, { recursive: true });
@@ -282,6 +290,37 @@ function getCurrentBranch(cwd) {
282
290
  return null;
283
291
  }
284
292
  }
293
+ function getHeadSha(cwd) {
294
+ try {
295
+ const sha = execSync("git rev-parse HEAD", {
296
+ cwd,
297
+ encoding: "utf8",
298
+ timeout: 3000
299
+ }).trim();
300
+ return sha || null;
301
+ } catch {
302
+ return null;
303
+ }
304
+ }
305
+ function getThirdPartyCheckRuns(cwd, sha) {
306
+ try {
307
+ const json = execFileSync("gh", [
308
+ "api",
309
+ `repos/{owner}/{repo}/commits/${sha}/check-runs`,
310
+ "--jq",
311
+ '.check_runs | map(select(.app.slug != "github-actions")) | map({name: .name, status: .status, conclusion: (.conclusion // "")})'
312
+ ], {
313
+ cwd,
314
+ encoding: "utf8",
315
+ timeout: 15000
316
+ }).trim();
317
+ if (!json || json === "[]")
318
+ return [];
319
+ return JSON.parse(json);
320
+ } catch {
321
+ return [];
322
+ }
323
+ }
285
324
  function matchesAllowedPattern(cmd, pattern) {
286
325
  const cmdTokens = parseArgvTokens(cmd);
287
326
  const patTokens = parseArgvTokens(pattern);
@@ -914,22 +953,27 @@ function requireCiGreenBeforeStop(ctx) {
914
953
  const branch = getCurrentBranch(cwd);
915
954
  if (!branch || branch === "HEAD")
916
955
  return allow("Detached HEAD, skipping CI check.");
917
- const runsJson = execFileSync("gh", ["run", "list", "--branch", branch, "--limit", "5", "--json", "status,conclusion,name"], {
918
- cwd,
919
- encoding: "utf8",
920
- timeout: 15000
921
- }).trim();
922
- if (!runsJson || runsJson === "[]")
923
- return allow(`No CI runs found for branch "${branch}".`);
924
- const runs = JSON.parse(runsJson);
925
- if (runs.length === 0)
956
+ let workflowRuns = [];
957
+ try {
958
+ const runsJson = execFileSync("gh", ["run", "list", "--branch", branch, "--limit", "5", "--json", "status,conclusion,name"], { cwd, encoding: "utf8", timeout: 15000 }).trim();
959
+ if (runsJson && runsJson !== "[]") {
960
+ workflowRuns = JSON.parse(runsJson);
961
+ }
962
+ } catch {}
963
+ let thirdPartyChecks = [];
964
+ const sha = getHeadSha(cwd);
965
+ if (sha) {
966
+ thirdPartyChecks = getThirdPartyCheckRuns(cwd, sha);
967
+ }
968
+ const allChecks = [...workflowRuns, ...thirdPartyChecks];
969
+ if (allChecks.length === 0)
926
970
  return allow(`No CI runs found for branch "${branch}".`);
927
- const failing = runs.filter((r) => r.status === "completed" && r.conclusion !== "success" && r.conclusion !== "skipped");
971
+ const failing = allChecks.filter((r) => r.status === "completed" && r.conclusion !== "success" && r.conclusion !== "skipped");
928
972
  if (failing.length > 0) {
929
973
  const names = failing.map((r) => `"${r.name}"`).join(", ");
930
974
  return deny(`CI checks are failing on branch "${branch}": ${names}. Fix the failing checks before stopping.`);
931
975
  }
932
- const pending = runs.filter((r) => r.status === "in_progress" || r.status === "queued" || r.status === "waiting");
976
+ const pending = allChecks.filter((r) => r.status === "in_progress" || r.status === "queued" || r.status === "waiting");
933
977
  if (pending.length > 0) {
934
978
  const names = pending.map((r) => `"${r.name}"`).join(", ");
935
979
  return deny(`CI checks are still running on branch "${branch}": ${names}. Wait for all checks to complete and verify they pass.`);
@@ -1334,6 +1378,15 @@ var init_builtin_policies = __esm(() => {
1334
1378
  });
1335
1379
 
1336
1380
  // src/hooks/policy-evaluator.ts
1381
+ function appendHint(baseReason, hint) {
1382
+ const base = baseReason.trim();
1383
+ const normalizedHint = typeof hint === "string" ? hint.trim() : "";
1384
+ if (!normalizedHint)
1385
+ return base;
1386
+ if (!base)
1387
+ return normalizedHint;
1388
+ return `${base}. ${normalizedHint}`;
1389
+ }
1337
1390
  async function evaluatePolicies(eventType, payload, session, config) {
1338
1391
  const toolName = payload.tool_name;
1339
1392
  const toolInput = payload.tool_input;
@@ -1373,7 +1426,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
1373
1426
  continue;
1374
1427
  }
1375
1428
  if (result.decision === "deny") {
1376
- const reason = result.reason ?? `Blocked by policy: ${policy.name}`;
1429
+ const reason = appendHint(result.reason ?? `Blocked by policy: ${policy.name}`, config?.policyParams?.[policy.name]?.hint);
1377
1430
  hookLogInfo(`deny by "${policy.name}": ${reason}`);
1378
1431
  const displayTool = ctx.toolName ?? "unknown tool";
1379
1432
  if (eventType === "PreToolUse") {
@@ -1420,7 +1473,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
1420
1473
  }
1421
1474
  if (result.decision === "instruct" && !instructPolicyName) {
1422
1475
  instructPolicyName = policy.name;
1423
- instructReason = result.reason ?? `Instruction from policy: ${policy.name}`;
1476
+ instructReason = appendHint(result.reason ?? `Instruction from policy: ${policy.name}`, config?.policyParams?.[policy.name]?.hint);
1424
1477
  hookLogInfo(`instruct by "${policy.name}": ${instructReason}`);
1425
1478
  }
1426
1479
  if (result.decision === "allow" && result.reason) {
@@ -1533,10 +1586,9 @@ async function createEsmShim(distIndex, distUrl) {
1533
1586
  const shimPath = distIndex + ".__failproofai_esm_shim__.mjs";
1534
1587
  const shimCode = [
1535
1588
  `import _cjs from '${distUrl}';`,
1536
- `export const createApp = _cjs.createApp;`,
1537
- `export const getQueueCondition = _cjs.getQueueCondition;`,
1538
- `export const clearQueueCondition = _cjs.clearQueueCondition;`,
1539
1589
  `export const customPolicies = _cjs.customPolicies;`,
1590
+ `export const getCustomHooks = _cjs.getCustomHooks;`,
1591
+ `export const clearCustomHooks = _cjs.clearCustomHooks;`,
1540
1592
  `export const allow = _cjs.allow;`,
1541
1593
  `export const deny = _cjs.deny;`,
1542
1594
  `export const instruct = _cjs.instruct;`,
@@ -1616,20 +1668,21 @@ var init_loader_utils = __esm(() => {
1616
1668
  });
1617
1669
 
1618
1670
  // src/hooks/custom-hooks-loader.ts
1619
- import { resolve as resolve4, isAbsolute } from "node:path";
1620
- import { existsSync as existsSync3 } from "node:fs";
1671
+ import { resolve as resolve4, isAbsolute, basename } from "node:path";
1672
+ import { existsSync as existsSync3, readdirSync } from "node:fs";
1621
1673
  import { pathToFileURL as pathToFileURL2 } from "node:url";
1622
- async function loadCustomHooks(customPoliciesPath, opts) {
1623
- if (!customPoliciesPath)
1674
+ import { homedir as homedir4 } from "node:os";
1675
+ function discoverPolicyFiles(dir) {
1676
+ if (!existsSync3(dir))
1624
1677
  return [];
1625
- const absPath = isAbsolute(customPoliciesPath) ? customPoliciesPath : resolve4(process.cwd(), customPoliciesPath);
1626
- if (!existsSync3(absPath)) {
1627
- if (opts?.strict)
1628
- throw new Error(`Custom hooks file not found: ${absPath}`);
1629
- hookLogWarn(`customPoliciesPath not found: ${absPath}`);
1678
+ try {
1679
+ const entries = readdirSync(dir, { withFileTypes: true });
1680
+ return entries.filter((e) => e.isFile() && CONVENTION_FILE_RE.test(e.name)).sort((a, b) => a.name.localeCompare(b.name)).map((e) => resolve4(dir, e.name));
1681
+ } catch {
1630
1682
  return [];
1631
1683
  }
1632
- clearCustomHooks();
1684
+ }
1685
+ async function loadSingleFile(absPath, opts) {
1633
1686
  const g = globalThis;
1634
1687
  g[LOADING_KEY] = true;
1635
1688
  let tmpFiles = [];
@@ -1645,18 +1698,87 @@ async function loadCustomHooks(customPoliciesPath, opts) {
1645
1698
  if (opts?.strict)
1646
1699
  throw new Error(`Failed to load custom hooks from ${absPath}: ${msg}`);
1647
1700
  hookLogError(`failed to load custom hooks from ${absPath}: ${msg}`);
1648
- return [];
1649
1701
  } finally {
1650
1702
  g[LOADING_KEY] = false;
1651
1703
  await cleanupTmpFiles(tmpFiles);
1652
1704
  }
1705
+ }
1706
+ async function loadCustomHooks(customPoliciesPath, opts) {
1707
+ if (!customPoliciesPath)
1708
+ return [];
1709
+ const absPath = isAbsolute(customPoliciesPath) ? customPoliciesPath : resolve4(opts?.sessionCwd ?? process.cwd(), customPoliciesPath);
1710
+ if (!existsSync3(absPath)) {
1711
+ if (opts?.strict)
1712
+ throw new Error(`Custom hooks file not found: ${absPath}`);
1713
+ hookLogWarn(`customPoliciesPath not found: ${absPath}`);
1714
+ return [];
1715
+ }
1716
+ clearCustomHooks();
1717
+ await loadSingleFile(absPath, opts);
1653
1718
  return getCustomHooks();
1654
1719
  }
1655
- var LOADING_KEY = "__FAILPROOFAI_LOADING_HOOKS__";
1720
+ async function loadAllCustomHooks(customPoliciesPath, opts) {
1721
+ clearCustomHooks();
1722
+ const conventionSources = [];
1723
+ if (customPoliciesPath) {
1724
+ const absPath = isAbsolute(customPoliciesPath) ? customPoliciesPath : resolve4(opts?.sessionCwd ?? process.cwd(), customPoliciesPath);
1725
+ if (existsSync3(absPath)) {
1726
+ await loadSingleFile(absPath);
1727
+ } else {
1728
+ hookLogWarn(`customPoliciesPath not found: ${absPath}`);
1729
+ }
1730
+ }
1731
+ const hooksBeforeConvention = getCustomHooks().length;
1732
+ const projectDir = resolve4(opts?.sessionCwd ?? process.cwd(), ".failproofai", "policies");
1733
+ const projectFiles = discoverPolicyFiles(projectDir);
1734
+ for (const file of projectFiles) {
1735
+ const hooksBefore = getCustomHooks().length;
1736
+ await loadSingleFile(file);
1737
+ const newHooks = getCustomHooks().slice(hooksBefore);
1738
+ if (newHooks.length > 0) {
1739
+ conventionSources.push({
1740
+ scope: "project",
1741
+ file: basename(file),
1742
+ hookNames: newHooks.map((h) => h.name)
1743
+ });
1744
+ }
1745
+ }
1746
+ const userDir = resolve4(homedir4(), ".failproofai", "policies");
1747
+ const userFiles = discoverPolicyFiles(userDir);
1748
+ for (const file of userFiles) {
1749
+ const hooksBefore = getCustomHooks().length;
1750
+ await loadSingleFile(file);
1751
+ const newHooks = getCustomHooks().slice(hooksBefore);
1752
+ if (newHooks.length > 0) {
1753
+ conventionSources.push({
1754
+ scope: "user",
1755
+ file: basename(file),
1756
+ hookNames: newHooks.map((h) => h.name)
1757
+ });
1758
+ }
1759
+ }
1760
+ const allHooks = getCustomHooks();
1761
+ const conventionCount = allHooks.length - hooksBeforeConvention;
1762
+ if (projectFiles.length > 0 || userFiles.length > 0) {
1763
+ hookLogInfo(`convention policies: ${projectFiles.length} project file(s), ${userFiles.length} user file(s), ${conventionCount} hook(s)`);
1764
+ }
1765
+ const conventionHookRefs = new Set;
1766
+ for (const hook of allHooks.slice(hooksBeforeConvention)) {
1767
+ conventionHookRefs.add(hook);
1768
+ }
1769
+ for (const hook of allHooks) {
1770
+ if (conventionHookRefs.has(hook)) {
1771
+ hook.__conventionSource = true;
1772
+ }
1773
+ }
1774
+ return { hooks: allHooks, conventionSources };
1775
+ }
1776
+ var LOADING_KEY = "__FAILPROOFAI_LOADING_HOOKS__", CONVENTION_FILE_RE;
1656
1777
  var init_custom_hooks_loader = __esm(() => {
1657
1778
  init_hook_logger();
1658
1779
  init_custom_hooks_registry();
1659
1780
  init_loader_utils();
1781
+ CONVENTION_FILE_RE = /policies\.(js|mjs|ts)$/;
1660
1782
  });
1661
1783
 
1662
1784
  // src/hooks/hook-activity-store.ts
@@ -1665,14 +1787,14 @@ import {
1665
1787
  writeFileSync as writeFileSync2,
1666
1788
  appendFileSync as appendFileSync2,
1667
1789
  renameSync as renameSync2,
1668
- readdirSync,
1790
+ readdirSync as readdirSync2,
1669
1791
  mkdirSync as mkdirSync3,
1670
1792
  existsSync as existsSync4,
1671
1793
  statSync as statSync2,
1672
1794
  unlinkSync
1673
1795
  } from "node:fs";
1674
1796
  import { join as join3 } from "node:path";
1675
- import { homedir as homedir4 } from "node:os";
1797
+ import { homedir as homedir5 } from "node:os";
1676
1798
  function ensureDir() {
1677
1799
  if (!existsSync4(storeDir)) {
1678
1800
  mkdirSync3(storeDir, { recursive: true });
@@ -1777,12 +1899,12 @@ function updateStats(entry) {
1777
1899
  }
1778
1900
  var PAGE_SIZE = 25, DEFAULT_STORE_DIR, CURRENT_FILE = "current.jsonl", COUNT_FILE = "current.count", STATS_FILE = "stats.json", LOCK_FILE = "current.lock", LOCK_STALE_MS = 2000, storeDir, rotateSeq = 0;
1779
1901
  var init_hook_activity_store = __esm(() => {
1780
- DEFAULT_STORE_DIR = join3(homedir4(), ".failproofai", "cache", "hook-activity");
1902
+ DEFAULT_STORE_DIR = join3(homedir5(), ".failproofai", "cache", "hook-activity");
1781
1903
  storeDir = DEFAULT_STORE_DIR;
1782
1904
  });
1783
1905
 
1784
1906
  // package.json
1785
- var version2 = "0.0.2-beta.6";
1907
+ var version2 = "0.0.2-beta.7";
1786
1908
  var init_package = () => {};
1787
1909
 
1788
1910
  // src/posthog-key.ts
@@ -1944,9 +2066,13 @@ async function handleHookEvent(eventType) {
1944
2066
  const config = readMergedHooksConfig(session.cwd);
1945
2067
  clearPolicies();
1946
2068
  registerBuiltinPolicies(config.enabledPolicies);
1947
- const customHooksList = await loadCustomHooks(config.customPoliciesPath);
2069
+ const loadResult = await loadAllCustomHooks(config.customPoliciesPath, { sessionCwd: session.cwd });
2070
+ const customHooksList = loadResult.hooks;
2071
+ const conventionHookNames = new Set(loadResult.conventionSources.flatMap((s) => s.hookNames));
1948
2072
  for (const hook of customHooksList) {
1949
2073
  const hookName = hook.name;
2074
+ const isConvention = hook.__conventionSource === true;
2075
+ const prefix = isConvention ? "convention" : "custom";
1950
2076
  const fn = async (ctx) => {
1951
2077
  try {
1952
2078
  const result2 = await Promise.race([
@@ -1957,16 +2083,17 @@ async function handleHookEvent(eventType) {
1957
2083
  } catch (err) {
1958
2084
  const msg = err instanceof Error ? err.message : String(err);
1959
2085
  const isTimeout = msg === "timeout";
1960
- hookLogWarn(`custom hook "${hookName}" failed: ${msg}`);
2086
+ hookLogWarn(`${prefix} hook "${hookName}" failed: ${msg}`);
1961
2087
  trackHookEvent(getInstanceId(), "custom_hook_error", {
1962
2088
  hook_name: hookName,
1963
2089
  error_type: isTimeout ? "timeout" : "exception",
1964
- event_type: eventType
2090
+ event_type: eventType,
2091
+ is_convention_policy: isConvention
1965
2092
  });
1966
2093
  return { decision: "allow" };
1967
2094
  }
1968
2095
  };
1969
- registerPolicy(`custom/${hookName}`, hook.description ?? "", fn, hook.match ?? {}, -1);
2096
+ registerPolicy(`${prefix}/${hookName}`, hook.description ?? "", fn, hook.match ?? {}, -1);
1970
2097
  }
1971
2098
  if (customHooksList.length > 0) {
1972
2099
  trackHookEvent(getInstanceId(), "custom_hooks_loaded", {
@@ -1975,7 +2102,16 @@ async function handleHookEvent(eventType) {
1975
2102
  event_types_covered: [...new Set(customHooksList.flatMap((h) => h.match?.events ?? []))]
1976
2103
  });
1977
2104
  }
1978
- hookLogInfo(`event=${eventType} policies=${config.enabledPolicies.length} custom=${customHooksList.length}`);
2105
+ if (loadResult.conventionSources.length > 0) {
2106
+ trackHookEvent(getInstanceId(), "convention_policies_loaded", {
2107
+ event_type: eventType,
2108
+ project_file_count: loadResult.conventionSources.filter((s) => s.scope === "project").length,
2109
+ user_file_count: loadResult.conventionSources.filter((s) => s.scope === "user").length,
2110
+ convention_hook_count: conventionHookNames.size,
2111
+ convention_hook_names: [...conventionHookNames]
2112
+ });
2113
+ }
2114
+ hookLogInfo(`event=${eventType} policies=${config.enabledPolicies.length} custom=${customHooksList.length} convention=${conventionHookNames.size}`);
1979
2115
  const result = await evaluatePolicies(eventType, parsed, session, config);
1980
2116
  const durationMs = Math.round(performance.now() - startTime);
1981
2117
  hookLogInfo(`result=${result.decision} policy=${result.policyName ?? "none"} duration=${durationMs}ms`);
@@ -2007,7 +2143,8 @@ async function handleHookEvent(eventType) {
2007
2143
  if (result.decision === "deny" || result.decision === "instruct") {
2008
2144
  try {
2009
2145
  const isCustomHook = result.policyName?.startsWith("custom/") ?? false;
2010
- const hasCustomParams = !isCustomHook && !!(result.policyName && config.policyParams?.[result.policyName]);
2146
+ const isConventionPolicy = result.policyName?.startsWith("convention/") ?? false;
2147
+ const hasCustomParams = !isCustomHook && !isConventionPolicy && !!(result.policyName && config.policyParams?.[result.policyName]);
2011
2148
  const paramKeysOverridden = hasCustomParams ? Object.keys(config.policyParams[result.policyName]) : [];
2012
2149
  const distinctId = getInstanceId();
2013
2150
  await trackHookEvent(distinctId, "hook_policy_triggered", {
@@ -2016,6 +2153,7 @@ async function handleHookEvent(eventType) {
2016
2153
  policy_name: result.policyName,
2017
2154
  decision: result.decision,
2018
2155
  is_custom_hook: isCustomHook,
2156
+ is_convention_policy: isConventionPolicy,
2019
2157
  has_custom_params: hasCustomParams,
2020
2158
  param_keys_overridden: paramKeysOverridden
2021
2159
  });
@@ -2348,13 +2486,13 @@ __export(exports_manager, {
2348
2486
  });
2349
2487
  import { execSync as execSync3 } from "node:child_process";
2350
2488
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync4 } from "node:fs";
2351
- import { resolve as resolve5, dirname as dirname3 } from "node:path";
2352
- import { homedir as homedir5, platform, arch, release, hostname } from "node:os";
2489
+ import { resolve as resolve5, dirname as dirname3, basename as basename2 } from "node:path";
2490
+ import { homedir as homedir6, platform, arch, release, hostname } from "node:os";
2353
2491
  function getSettingsPath(scope, cwd) {
2354
2492
  const base = cwd ? resolve5(cwd) : process.cwd();
2355
2493
  switch (scope) {
2356
2494
  case "user":
2357
- return resolve5(homedir5(), ".claude", "settings.json");
2495
+ return resolve5(homedir6(), ".claude", "settings.json");
2358
2496
  case "project":
2359
2497
  return resolve5(base, ".claude", "settings.json");
2360
2498
  case "local":
@@ -2481,7 +2619,7 @@ async function installHooks(policyNames, scope = "user", cwd, includeBeta = fals
2481
2619
  }
2482
2620
  }
2483
2621
  const binaryPath = resolveFailproofaiBinary();
2484
- const previousConfig = readHooksConfig();
2622
+ const previousConfig = readScopedHooksConfig(scope, cwd);
2485
2623
  const previousEnabled = new Set(previousConfig.enabledPolicies);
2486
2624
  let selectedPolicies;
2487
2625
  if (policyNames !== undefined) {
@@ -2515,7 +2653,7 @@ async function installHooks(policyNames, scope = "user", cwd, includeBeta = fals
2515
2653
  console.log(`
2516
2654
  Validated ${validatedHooks.length} custom hook(s): ${validatedHooks.map((h) => h.name).join(", ")}`);
2517
2655
  }
2518
- writeHooksConfig(configToWrite);
2656
+ writeScopedHooksConfig(configToWrite, scope, cwd);
2519
2657
  console.log(`
2520
2658
  Enabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}`);
2521
2659
  if (removeCustomHooks) {
@@ -2592,15 +2730,16 @@ Enabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}`)
2592
2730
  }
2593
2731
  }
2594
2732
  async function removeHooks(policyNames, scope = "user", cwd, opts) {
2733
+ const configScope = scope === "all" ? "user" : scope;
2595
2734
  if (opts?.removeCustomHooks) {
2596
- const config = readHooksConfig();
2735
+ const config = readScopedHooksConfig(configScope, cwd);
2597
2736
  delete config.customPoliciesPath;
2598
- writeHooksConfig(config);
2737
+ writeScopedHooksConfig(config, configScope, cwd);
2599
2738
  console.log("Custom hooks path cleared.");
2600
2739
  }
2601
2740
  if (policyNames && policyNames.length > 0 && !(policyNames.length === 1 && policyNames[0] === "all")) {
2602
2741
  validatePolicyNames(policyNames);
2603
- const config = readHooksConfig();
2742
+ const config = readScopedHooksConfig(configScope, cwd);
2604
2743
  const removeSet = new Set(policyNames);
2605
2744
  const remaining = config.enabledPolicies.filter((p) => !removeSet.has(p));
2606
2745
  const notEnabled = policyNames.filter((p) => !config.enabledPolicies.includes(p));
@@ -2614,7 +2753,7 @@ async function removeHooks(policyNames, scope = "user", cwd, opts) {
2614
2753
  enabledPolicies: remaining,
2615
2754
  ...filteredParams && Object.keys(filteredParams).length > 0 ? { policyParams: filteredParams } : {}
2616
2755
  };
2617
- writeHooksConfig(updatedConfig);
2756
+ writeScopedHooksConfig(updatedConfig, configScope, cwd);
2618
2757
  try {
2619
2758
  const distinctId = getInstanceId();
2620
2759
  const actuallyRemoved = policyNames.filter((p) => config.enabledPolicies.includes(p));
@@ -2635,7 +2774,7 @@ async function removeHooks(policyNames, scope = "user", cwd, opts) {
2635
2774
  console.log(`Remaining: ${remaining.length > 0 ? remaining.join(", ") : "(none)"}`);
2636
2775
  return;
2637
2776
  }
2638
- const configBeforeRemoval = readHooksConfig();
2777
+ const configBeforeRemoval = readScopedHooksConfig(configScope, cwd);
2639
2778
  const scopesToRemove = scope === "all" ? [...HOOK_SCOPES] : [scope];
2640
2779
  let totalRemoved = 0;
2641
2780
  for (const s of scopesToRemove) {
@@ -2682,10 +2821,18 @@ async function removeHooks(policyNames, scope = "user", cwd, opts) {
2682
2821
  hostname_hash: hashToId(hostname())
2683
2822
  });
2684
2823
  } catch {}
2685
- if (scope === "all" || !HOOK_SCOPES.some((s) => hooksInstalledInSettings(s, cwd))) {
2686
- const existingForClear = readHooksConfig();
2687
- const { customPoliciesPath: _drop, policyParams: _dropParams, ...restClear } = existingForClear;
2688
- writeHooksConfig({ ...restClear, enabledPolicies: [] });
2824
+ if (scope === "all") {
2825
+ for (const s of HOOK_SCOPES) {
2826
+ const existing = readScopedHooksConfig(s, cwd);
2827
+ if (existing.enabledPolicies.length > 0 || existing.customPoliciesPath || existing.policyParams) {
2828
+ const { customPoliciesPath: _drop, policyParams: _dropParams, ...rest } = existing;
2829
+ writeScopedHooksConfig({ ...rest, enabledPolicies: [] }, s, cwd);
2830
+ }
2831
+ }
2832
+ } else if (!HOOK_SCOPES.some((s) => hooksInstalledInSettings(s, cwd))) {
2833
+ const existing = readScopedHooksConfig(configScope, cwd);
2834
+ const { customPoliciesPath: _drop, policyParams: _dropParams, ...rest } = existing;
2835
+ writeScopedHooksConfig({ ...rest, enabledPolicies: [] }, configScope, cwd);
2689
2836
  }
2690
2837
  }
2691
2838
  async function listHooks(cwd) {
@@ -2822,6 +2969,35 @@ Failproof AI Hook Policies
2822
2969
  }
2823
2970
  console.log();
2824
2971
  }
2972
+ const base = cwd ? resolve5(cwd) : process.cwd();
2973
+ const conventionDirs = [
2974
+ { label: "Project", dir: resolve5(base, ".failproofai", "policies") },
2975
+ { label: "User", dir: resolve5(homedir6(), ".failproofai", "policies") }
2976
+ ];
2977
+ for (const { label, dir } of conventionDirs) {
2978
+ const files = discoverPolicyFiles(dir);
2979
+ if (files.length === 0)
2980
+ continue;
2981
+ console.log(`
2982
+ ── Convention Policies — ${label} (${dir}) ──────────`);
2983
+ for (const file of files) {
2984
+ try {
2985
+ const hooks = await loadCustomHooks(file);
2986
+ if (hooks.length === 0) {
2987
+ const filename = basename2(file);
2988
+ console.log(` \x1B[31m✗\x1B[0m ${filename.padEnd(nameColWidth)}\x1B[31mfailed to load\x1B[0m`);
2989
+ } else {
2990
+ const filename = basename2(file);
2991
+ const hookSummary = hooks.map((h) => h.name).join(", ");
2992
+ console.log(` \x1B[32m✓\x1B[0m ${filename.padEnd(nameColWidth)}${hooks.length} hook(s): ${hookSummary}`);
2993
+ }
2994
+ } catch {
2995
+ const filename = basename2(file);
2996
+ console.log(` \x1B[31m✗\x1B[0m ${filename.padEnd(nameColWidth)}\x1B[31merror\x1B[0m`);
2997
+ }
2998
+ }
2999
+ console.log();
3000
+ }
2825
3001
  }
2826
3002
  var VALID_POLICY_NAMES;
2827
3003
  var init_manager = __esm(() => {
@@ -2837,10 +3013,10 @@ var init_manager = __esm(() => {
2837
3013
  });
2838
3014
 
2839
3015
  // lib/paths.ts
2840
- import { homedir as homedir6 } from "os";
3016
+ import { homedir as homedir7 } from "os";
2841
3017
  import { join as join4 } from "path";
2842
3018
  function getDefaultClaudeProjectsPath() {
2843
- return join4(homedir6(), ".claude", "projects");
3019
+ return join4(homedir7(), ".claude", "projects");
2844
3020
  }
2845
3021
  var init_paths = () => {};
2846
3022
 
@@ -3009,7 +3185,7 @@ import { realpathSync as realpathSync2 } from "fs";
3009
3185
  import { dirname as dirname5, resolve as resolve8 } from "path";
3010
3186
  import { fileURLToPath as fileURLToPath2 } from "url";
3011
3187
  // package.json
3012
- var version = "0.0.2-beta.6";
3188
+ var version = "0.0.2-beta.7";
3013
3189
 
3014
3190
  // bin/failproofai.mjs
3015
3191
  if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
@@ -3073,6 +3249,11 @@ COMMANDS
3073
3249
  --version, -v Print version and exit
3074
3250
  --help, -h Show this help message
3075
3251
 
3252
+ CONVENTION POLICIES
3253
+ Drop *policies.{js,mjs,ts} files into .failproofai/policies/ for auto-loading.
3254
+ Works at project level (.failproofai/policies/) and user level (~/.failproofai/policies/).
3255
+ No --custom flag or config changes needed \u2014 just drop files and they're picked up.
3256
+
3076
3257
  EXAMPLES
3077
3258
  failproofai policies
3078
3259
  failproofai policies --install
@@ -491,14 +491,14 @@ pull requests. If `gh` is not installed or not authenticated, the policy fails o
491
491
  ### `require-ci-green-before-stop`
492
492
 
493
493
  **Event:** Stop
494
- **Default:** Denies stopping when CI checks are failing or still running on the current branch. Treats `skipped` conclusions as success. Returns an informational message when all checks pass.
494
+ **Default:** Denies stopping when CI checks are failing or still running on the current branch. Checks both GitHub Actions workflow runs and third-party bot checks (e.g. CodeRabbit, SonarCloud, Codecov). Treats `skipped` conclusions as success. Returns an informational message when all checks pass.
495
495
 
496
496
  No parameters.
497
497
 
498
498
  <Note>
499
499
  This policy requires [GitHub CLI](https://cli.github.com/) (`gh`) to be installed and authenticated.
500
500
  Run `gh auth login` with a personal access token that has `repo` scope for read access to
501
- Actions workflow runs. If `gh` is not installed or not authenticated, the policy fails open and reports the reason to Claude.
501
+ Actions workflow runs and the Checks API. If `gh` is not installed or not authenticated, the policy fails open and reports the reason to Claude.
502
502
  </Note>
503
503
 
504
504
  ---
@@ -116,6 +116,35 @@ If a policy has parameters but you don't specify them, the policy's built-in def
116
116
 
117
117
  Unknown keys inside a policy's params block are silently ignored at hook-fire time but flagged as warnings when you run `failproofai policies`.
118
118
 
119
+ #### `hint` (cross-cutting)
120
+
121
+ Type: `string` (optional)
122
+
123
+ A message appended to the reason when a policy returns `deny` or `instruct`. Use it to give Claude actionable guidance without modifying the policy itself.
124
+
125
+ Works with any policy type — built-in, custom (`custom/`), or convention (`convention/`).
126
+
127
+ ```json
128
+ {
129
+ "policyParams": {
130
+ "block-force-push": {
131
+ "hint": "Try creating a fresh branch instead."
132
+ },
133
+ "block-sudo": {
134
+ "allowPatterns": ["sudo apt-get"],
135
+ "hint": "Use apt-get directly without sudo."
136
+ },
137
+ "custom/my-policy": {
138
+ "hint": "Ask the user for approval first."
139
+ }
140
+ }
141
+ }
142
+ ```
143
+
144
+ When `block-force-push` denies, Claude sees: *"Force-pushing is blocked. Try creating a fresh branch instead."*
145
+
146
+ Non-string values and empty strings are silently ignored. If `hint` is not set, behavior is unchanged (backward-compatible).
147
+
119
148
  ### `customPoliciesPath`
120
149
 
121
150
  Type: `string` (absolute path)
@@ -124,6 +153,23 @@ Path to a JavaScript file containing custom hook policies. This is set automatic
124
153
 
125
154
  The file is loaded fresh on every hook event - there is no caching. See [Custom Policies](/custom-policies) for authoring details.
126
155
 
156
+ ### Convention-based policies (v0.0.2-beta.7+)
157
+
158
+ In addition to the explicit `customPoliciesPath`, failproofai automatically discovers and loads policy files from `.failproofai/policies/` directories:
159
+
160
+ | Level | Directory | Scope |
161
+ |-------|-----------|-------|
162
+ | Project | `.failproofai/policies/` | Shared with team via version control |
163
+ | User | `~/.failproofai/policies/` | Personal, applies to all projects |
164
+
165
+ **File matching:** Only files matching `*policies.{js,mjs,ts}` are loaded (e.g. `security-policies.mjs`, `workflow-policies.js`). Other files in the directory are ignored.
166
+
167
+ **No config needed:** Convention policies require no entries in `policies-config.json`. Just drop files into the directory and they're picked up on the next hook event.
168
+
169
+ **Union loading:** Both project and user convention directories are scanned. All matching files from both levels are loaded (unlike `customPoliciesPath` which uses first-scope-wins).
170
+
171
+ See [Custom Policies](/custom-policies) for more details and examples.
172
+
127
173
  ### `llm`
128
174
 
129
175
  Type: `object` (optional)