failproofai 0.0.2-beta.2 → 0.0.2-beta.3

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 (124) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  9. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  10. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  12. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  18. package/.next/standalone/.next/server/app/_not-found.rsc +17 -17
  19. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +17 -17
  20. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  21. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +11 -11
  22. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  24. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  25. package/.next/standalone/.next/server/app/index.html +1 -1
  26. package/.next/standalone/.next/server/app/index.rsc +16 -16
  27. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +16 -16
  29. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  30. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +11 -11
  31. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  32. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  33. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  35. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  36. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  38. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  39. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  42. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  43. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  46. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  48. package/.next/standalone/.next/server/chunks/[root-of-the-server]__02nt~6d._.js +1 -1
  49. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
  50. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0103jwf._.js → [root-of-the-server]__07k6eu-._.js} +2 -2
  51. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +2 -2
  52. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09icjsf._.js +2 -2
  53. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +2 -2
  54. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +2 -2
  55. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ovwjau._.js → [root-of-the-server]__0kfv9fw._.js} +2 -2
  56. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0okos0k._.js +2 -2
  57. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +5 -5
  58. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +2 -2
  59. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +2 -2
  60. package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +1 -1
  61. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
  62. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +1 -1
  63. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  64. package/.next/standalone/.next/server/pages/404.html +2 -2
  65. package/.next/standalone/.next/server/pages/500.html +1 -1
  66. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
  68. package/.next/standalone/.next/static/chunks/{0uftmw5od9kdz.js → 0.jo.465b6_k..js} +1 -1
  69. package/.next/standalone/.next/static/chunks/{0wtcha31~i7rm.js → 01haq0a3zrx0v.js} +1 -1
  70. package/.next/standalone/.next/static/chunks/08f78tecvx61l.css +1 -0
  71. package/.next/standalone/.next/static/chunks/{0tl2f-3yc.rqc.js → 0a6xi1a8f_qlp.js} +1 -1
  72. package/.next/standalone/.next/static/chunks/{0tbr0o7vwc~-s.js → 0mq7ze1vkeo1p.js} +1 -1
  73. package/.next/standalone/.next/static/chunks/{0sm1iqi3m~xiz.js → 0p_fpyfmmohnx.js} +1 -1
  74. package/.next/standalone/.next/static/chunks/{0pdd7~yp8ytu6.js → 0qwyj3m400l_g.js} +1 -1
  75. package/.next/standalone/.next/static/chunks/{001k0zayn2o.s.js → 0t94r_mk0s7e4.js} +1 -1
  76. package/.next/standalone/.next/static/chunks/{0jrzwsyo7wo26.js → 139~00zc9.u7s.js} +1 -1
  77. package/.next/standalone/Dockerfile.docs +12 -0
  78. package/.next/standalone/README.md +59 -46
  79. package/.next/standalone/dist/cli.mjs +215 -20
  80. package/.next/standalone/dist/index.js +2 -2
  81. package/.next/standalone/docs/{architecture.md → architecture.mdx} +40 -23
  82. package/.next/standalone/docs/{built-in-policies.md → built-in-policies.mdx} +134 -12
  83. package/.next/standalone/docs/cli/dashboard.mdx +28 -0
  84. package/.next/standalone/docs/cli/environment-variables.mdx +34 -0
  85. package/.next/standalone/docs/cli/hook.mdx +30 -0
  86. package/.next/standalone/docs/cli/install-policies.mdx +48 -0
  87. package/.next/standalone/docs/cli/list-policies.mdx +31 -0
  88. package/.next/standalone/docs/cli/remove-policies.mdx +44 -0
  89. package/.next/standalone/docs/cli/version.mdx +12 -0
  90. package/.next/standalone/docs/{configuration.md → configuration.mdx} +16 -16
  91. package/.next/standalone/docs/{custom-hooks.md → custom-policies.mdx} +80 -42
  92. package/.next/standalone/docs/{dashboard.md → dashboard.mdx} +26 -29
  93. package/.next/standalone/docs/docs.json +31 -4
  94. package/.next/standalone/docs/examples.mdx +253 -0
  95. package/.next/standalone/docs/for-agents.mdx +38 -0
  96. package/.next/standalone/docs/getting-started.mdx +134 -0
  97. package/.next/standalone/docs/introduction.mdx +57 -0
  98. package/.next/standalone/docs/logo/dark.svg +21 -0
  99. package/.next/standalone/docs/logo/light.svg +21 -0
  100. package/.next/standalone/docs/{package-aliases.md → package-aliases.mdx} +5 -5
  101. package/.next/standalone/docs/{testing.md → testing.mdx} +11 -11
  102. package/.next/standalone/package.json +6 -9
  103. package/.next/standalone/scripts/publish-aliases.mjs +4 -2
  104. package/.next/standalone/skills-lock.json +10 -0
  105. package/.next/standalone/src/hooks/builtin-policies.ts +259 -20
  106. package/.next/standalone/src/hooks/policy-evaluator.ts +19 -1
  107. package/.next/standalone/src/hooks/policy-helpers.ts +2 -2
  108. package/.next/standalone/vitest.config.e2e.mts +3 -0
  109. package/.next/standalone/vitest.config.mts +3 -0
  110. package/README.md +59 -46
  111. package/dist/cli.mjs +215 -20
  112. package/dist/index.js +2 -2
  113. package/package.json +6 -9
  114. package/scripts/publish-aliases.mjs +4 -2
  115. package/src/hooks/builtin-policies.ts +259 -20
  116. package/src/hooks/policy-evaluator.ts +19 -1
  117. package/src/hooks/policy-helpers.ts +2 -2
  118. package/.next/standalone/.next/static/chunks/15jpradyu_531.css +0 -1
  119. package/.next/standalone/docs/cli-reference.md +0 -175
  120. package/.next/standalone/docs/getting-started.md +0 -128
  121. package/.next/standalone/docs/introduction.md +0 -47
  122. /package/.next/standalone/.next/static/{JksWDLwDoPy6bcczVWlff → 7fR022u1Sj-s5MfKO1q9Y}/_buildManifest.js +0 -0
  123. /package/.next/standalone/.next/static/{JksWDLwDoPy6bcczVWlff → 7fR022u1Sj-s5MfKO1q9Y}/_clientMiddlewareManifest.js +0 -0
  124. /package/.next/standalone/.next/static/{JksWDLwDoPy6bcczVWlff → 7fR022u1Sj-s5MfKO1q9Y}/_ssgManifest.js +0 -0
@@ -179,8 +179,8 @@ var init_hooks_config = __esm(() => {
179
179
  });
180
180
 
181
181
  // src/hooks/policy-helpers.ts
182
- function allow() {
183
- return { decision: "allow" };
182
+ function allow(reason) {
183
+ return reason ? { decision: "allow", reason } : { decision: "allow" };
184
184
  }
185
185
  function deny(reason) {
186
186
  return { decision: "deny", reason };
@@ -248,7 +248,7 @@ var REGISTRY_KEY = "__FAILPROOFAI_POLICY_REGISTRY__", INDEX_CACHE_KEY = "__FAILP
248
248
  // src/hooks/builtin-policies.ts
249
249
  import { resolve as resolve2, join as join2 } from "node:path";
250
250
  import { readFile, writeFile } from "node:fs/promises";
251
- import { execSync } from "node:child_process";
251
+ import { execSync, execFileSync } from "node:child_process";
252
252
  import { homedir as homedir3 } from "node:os";
253
253
  function isClaudeInternalPath(resolved2) {
254
254
  const claudeDir = join2(homedir3(), ".claude");
@@ -266,6 +266,22 @@ function getFilePath(ctx) {
266
266
  function parseArgvTokens(cmd) {
267
267
  return cmd.trim().split(/\s+/).map((t) => t.replace(/^['"]|['"]$/g, ""));
268
268
  }
269
+ function getCurrentBranch(cwd) {
270
+ try {
271
+ let branch = gitBranchCache.get(cwd);
272
+ if (branch === undefined) {
273
+ branch = execSync("git rev-parse --abbrev-ref HEAD", {
274
+ cwd,
275
+ encoding: "utf8",
276
+ timeout: 3000
277
+ }).trim();
278
+ gitBranchCache.set(cwd, branch);
279
+ }
280
+ return branch || null;
281
+ } catch {
282
+ return null;
283
+ }
284
+ }
269
285
  function matchesAllowedPattern(cmd, pattern) {
270
286
  const cmdTokens = parseArgvTokens(cmd);
271
287
  const patTokens = parseArgvTokens(pattern);
@@ -653,22 +669,12 @@ function blockWorkOnMain(ctx) {
653
669
  const cwd = ctx.session?.cwd;
654
670
  if (!cwd)
655
671
  return allow();
656
- try {
657
- let branch = gitBranchCache.get(cwd);
658
- if (branch === undefined) {
659
- branch = execSync("git rev-parse --abbrev-ref HEAD", {
660
- cwd,
661
- encoding: "utf8",
662
- timeout: 3000
663
- }).trim();
664
- gitBranchCache.set(cwd, branch);
665
- }
666
- const protectedBranches = ctx.params?.protectedBranches ?? ["main", "master"];
667
- if (protectedBranches.includes(branch)) {
668
- return deny(`Git ${cmd.match(/git\s+(\S+)/)?.[1] ?? "operation"} on ${branch} is blocked. Create a feature branch first.`);
669
- }
670
- } catch {
672
+ const branch = getCurrentBranch(cwd);
673
+ if (!branch)
671
674
  return allow();
675
+ const protectedBranches = ctx.params?.protectedBranches ?? ["main", "master"];
676
+ if (protectedBranches.includes(branch)) {
677
+ return deny(`Git ${cmd.match(/git\s+(\S+)/)?.[1] ?? "operation"} on ${branch} is blocked. Create a feature branch first.`);
672
678
  }
673
679
  return allow();
674
680
  }
@@ -767,6 +773,137 @@ function warnBackgroundProcess(ctx) {
767
773
  }
768
774
  return allow();
769
775
  }
776
+ function requireCommitBeforeStop(ctx) {
777
+ const cwd = ctx.session?.cwd;
778
+ if (!cwd)
779
+ return allow("No working directory available, skipping commit check.");
780
+ try {
781
+ const status = execSync("git status --porcelain", {
782
+ cwd,
783
+ encoding: "utf8",
784
+ timeout: 5000
785
+ }).trim();
786
+ if (status.length > 0) {
787
+ return deny("You have uncommitted changes in the working directory. Commit all changes before stopping.");
788
+ }
789
+ return allow("All changes are committed.");
790
+ } catch {
791
+ return allow("Not a git repository, skipping commit check.");
792
+ }
793
+ }
794
+ function requirePushBeforeStop(ctx) {
795
+ const cwd = ctx.session?.cwd;
796
+ if (!cwd)
797
+ return allow("No working directory available, skipping push check.");
798
+ try {
799
+ const remotes = execSync("git remote", {
800
+ cwd,
801
+ encoding: "utf8",
802
+ timeout: 3000
803
+ }).trim();
804
+ if (!remotes)
805
+ return allow("No git remote configured, skipping push check.");
806
+ const remote = ctx.params?.remote ?? "origin";
807
+ const branch = getCurrentBranch(cwd);
808
+ if (!branch || branch === "HEAD")
809
+ return allow("Detached HEAD, skipping push check.");
810
+ let hasTracking = false;
811
+ try {
812
+ execFileSync("git", ["rev-parse", "--verify", `${remote}/${branch}`], {
813
+ cwd,
814
+ encoding: "utf8",
815
+ timeout: 3000
816
+ });
817
+ hasTracking = true;
818
+ } catch {}
819
+ if (!hasTracking) {
820
+ return deny(`Branch "${branch}" has not been pushed to remote "${remote}". ` + `Push your branch with: git push -u ${remote} ${branch}`);
821
+ }
822
+ const unpushed = execFileSync("git", ["log", `${remote}/${branch}..HEAD`, "--oneline"], {
823
+ cwd,
824
+ encoding: "utf8",
825
+ timeout: 5000
826
+ }).trim();
827
+ if (unpushed.length > 0) {
828
+ const commitCount = unpushed.split(`
829
+ `).length;
830
+ return deny(`You have ${commitCount} unpushed commit${commitCount > 1 ? "s" : ""} on branch "${branch}". ` + `Push your changes with: git push`);
831
+ }
832
+ return allow(`All commits pushed to "${remote}".`);
833
+ } catch {
834
+ return allow("Could not check push status, skipping.");
835
+ }
836
+ }
837
+ function requirePrBeforeStop(ctx) {
838
+ const cwd = ctx.session?.cwd;
839
+ if (!cwd)
840
+ return allow("No working directory available, skipping PR check.");
841
+ try {
842
+ try {
843
+ execSync("gh --version", { cwd, encoding: "utf8", timeout: 3000 });
844
+ } catch {
845
+ return allow("GitHub CLI (gh) not installed, skipping PR check.");
846
+ }
847
+ const branch = getCurrentBranch(cwd);
848
+ if (!branch || branch === "HEAD")
849
+ return allow("Detached HEAD, skipping PR check.");
850
+ let prJson;
851
+ try {
852
+ prJson = execSync("gh pr view --json number,url,state", {
853
+ cwd,
854
+ encoding: "utf8",
855
+ timeout: 15000
856
+ }).trim();
857
+ } catch {
858
+ return deny(`No pull request found for branch "${branch}". ` + `Create one with: gh pr create`);
859
+ }
860
+ const pr = JSON.parse(prJson);
861
+ if (pr.state === "OPEN") {
862
+ return allow(`PR #${pr.number} exists: ${pr.url}`);
863
+ }
864
+ return deny(`Pull request for branch "${branch}" is ${pr.state.toLowerCase()}. Create a new PR with: gh pr create`);
865
+ } catch {
866
+ return allow("Could not check PR status, skipping.");
867
+ }
868
+ }
869
+ function requireCiGreenBeforeStop(ctx) {
870
+ const cwd = ctx.session?.cwd;
871
+ if (!cwd)
872
+ return allow("No working directory available, skipping CI check.");
873
+ try {
874
+ try {
875
+ execSync("gh --version", { cwd, encoding: "utf8", timeout: 3000 });
876
+ } catch {
877
+ return allow("GitHub CLI (gh) not installed, skipping CI check.");
878
+ }
879
+ const branch = getCurrentBranch(cwd);
880
+ if (!branch || branch === "HEAD")
881
+ return allow("Detached HEAD, skipping CI check.");
882
+ const runsJson = execFileSync("gh", ["run", "list", "--branch", branch, "--limit", "5", "--json", "status,conclusion,name"], {
883
+ cwd,
884
+ encoding: "utf8",
885
+ timeout: 15000
886
+ }).trim();
887
+ if (!runsJson || runsJson === "[]")
888
+ return allow(`No CI runs found for branch "${branch}".`);
889
+ const runs = JSON.parse(runsJson);
890
+ if (runs.length === 0)
891
+ return allow(`No CI runs found for branch "${branch}".`);
892
+ const failing = runs.filter((r) => r.status === "completed" && r.conclusion !== "success" && r.conclusion !== "skipped");
893
+ if (failing.length > 0) {
894
+ const names = failing.map((r) => `"${r.name}"`).join(", ");
895
+ return deny(`CI checks are failing on branch "${branch}": ${names}. Fix the failing checks before stopping.`);
896
+ }
897
+ const pending = runs.filter((r) => r.status === "in_progress" || r.status === "queued" || r.status === "waiting");
898
+ if (pending.length > 0) {
899
+ const names = pending.map((r) => `"${r.name}"`).join(", ");
900
+ return deny(`CI checks are still running on branch "${branch}": ${names}. Wait for all checks to complete and verify they pass.`);
901
+ }
902
+ return allow(`All CI checks passed on branch "${branch}".`);
903
+ } catch {
904
+ return allow("Could not check CI status, skipping.");
905
+ }
906
+ }
770
907
  function registerBuiltinPolicies(enabledNames) {
771
908
  const enabledSet = new Set(enabledNames);
772
909
  for (const policy of BUILTIN_POLICIES) {
@@ -1102,6 +1239,49 @@ var init_builtin_policies = __esm(() => {
1102
1239
  match: { events: ["PreToolUse"] },
1103
1240
  defaultEnabled: false,
1104
1241
  category: "AI Behavior"
1242
+ },
1243
+ {
1244
+ name: "require-commit-before-stop",
1245
+ description: "Require all changes to be committed before Claude stops",
1246
+ fn: requireCommitBeforeStop,
1247
+ match: { events: ["Stop"] },
1248
+ defaultEnabled: false,
1249
+ category: "Workflow",
1250
+ beta: true
1251
+ },
1252
+ {
1253
+ name: "require-push-before-stop",
1254
+ description: "Require all commits to be pushed to remote before Claude stops",
1255
+ fn: requirePushBeforeStop,
1256
+ match: { events: ["Stop"] },
1257
+ defaultEnabled: false,
1258
+ category: "Workflow",
1259
+ beta: true,
1260
+ params: {
1261
+ remote: {
1262
+ type: "string",
1263
+ description: "Remote name to push to (default: origin)",
1264
+ default: "origin"
1265
+ }
1266
+ }
1267
+ },
1268
+ {
1269
+ name: "require-pr-before-stop",
1270
+ description: "Require a pull request to exist for the current branch before Claude stops",
1271
+ fn: requirePrBeforeStop,
1272
+ match: { events: ["Stop"] },
1273
+ defaultEnabled: false,
1274
+ category: "Workflow",
1275
+ beta: true
1276
+ },
1277
+ {
1278
+ name: "require-ci-green-before-stop",
1279
+ description: "Require CI checks to pass on the current branch before Claude stops",
1280
+ fn: requireCiGreenBeforeStop,
1281
+ match: { events: ["Stop"] },
1282
+ defaultEnabled: false,
1283
+ category: "Workflow",
1284
+ beta: true
1105
1285
  }
1106
1286
  ];
1107
1287
  });
@@ -1124,6 +1304,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
1124
1304
  };
1125
1305
  let instructPolicyName = null;
1126
1306
  let instructReason = null;
1307
+ const allowMessages = [];
1127
1308
  for (const policy of policies) {
1128
1309
  const schema = POLICY_PARAMS_MAP.get(policy.name);
1129
1310
  let ctx;
@@ -1195,6 +1376,9 @@ async function evaluatePolicies(eventType, payload, session, config) {
1195
1376
  instructReason = result.reason ?? `Instruction from policy: ${policy.name}`;
1196
1377
  hookLogInfo(`instruct by "${policy.name}": ${instructReason}`);
1197
1378
  }
1379
+ if (result.decision === "allow" && result.reason) {
1380
+ allowMessages.push(result.reason);
1381
+ }
1198
1382
  }
1199
1383
  if (instructPolicyName && instructReason) {
1200
1384
  if (eventType === "Stop") {
@@ -1222,6 +1406,17 @@ async function evaluatePolicies(eventType, payload, session, config) {
1222
1406
  decision: "instruct"
1223
1407
  };
1224
1408
  }
1409
+ if (allowMessages.length > 0) {
1410
+ const combined = allowMessages.join(`
1411
+ `);
1412
+ const response = {
1413
+ hookSpecificOutput: {
1414
+ hookEventName: eventType,
1415
+ additionalContext: combined
1416
+ }
1417
+ };
1418
+ return { exitCode: 0, stdout: JSON.stringify(response), stderr: "", policyName: null, reason: combined, decision: "allow" };
1419
+ }
1225
1420
  return { exitCode: 0, stdout: "", stderr: "", policyName: null, reason: null, decision: "allow" };
1226
1421
  }
1227
1422
  var POLICY_PARAMS_MAP;
@@ -1536,7 +1731,7 @@ var init_hook_activity_store = __esm(() => {
1536
1731
  });
1537
1732
 
1538
1733
  // package.json
1539
- var version2 = "0.0.2-beta.2";
1734
+ var version2 = "0.0.2-beta.3";
1540
1735
  var init_package = () => {};
1541
1736
 
1542
1737
  // src/posthog-key.ts
@@ -2762,7 +2957,7 @@ import { realpathSync as realpathSync2 } from "fs";
2762
2957
  import { dirname as dirname5, resolve as resolve8 } from "path";
2763
2958
  import { fileURLToPath as fileURLToPath2 } from "url";
2764
2959
  // package.json
2765
- var version = "0.0.2-beta.2";
2960
+ var version = "0.0.2-beta.3";
2766
2961
 
2767
2962
  // bin/failproofai.mjs
2768
2963
  if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
@@ -69,8 +69,8 @@ function clearCustomHooks() {
69
69
  g[REGISTRY_KEY] = [];
70
70
  }
71
71
  // src/hooks/policy-helpers.ts
72
- function allow() {
73
- return { decision: "allow" };
72
+ function allow(reason) {
73
+ return reason ? { decision: "allow", reason } : { decision: "allow" };
74
74
  }
75
75
  function deny(reason) {
76
76
  return { decision: "deny", reason };
@@ -4,7 +4,7 @@ description: "How the hook handler, config loading, and policy evaluation work i
4
4
  icon: sitemap
5
5
  ---
6
6
 
7
- This document explains how failproofai works internally: how the hook system processes events, how configuration is loaded and merged, how policies are evaluated, and how the dashboard fits in.
7
+ This document explains how failproofai works internally: how the hook system intercepts agent tool calls, how configuration is loaded and merged, how policies are evaluated, and how the dashboard monitors agent activity.
8
8
 
9
9
  ---
10
10
 
@@ -12,8 +12,8 @@ This document explains how failproofai works internally: how the hook system pro
12
12
 
13
13
  failproofai has two independent subsystems:
14
14
 
15
- 1. **Hook handler** A fast CLI subprocess that Claude Code invokes on every tool call. Evaluates policies and returns a decision.
16
- 2. **Dashboard** A Next.js web application for browsing sessions and managing policies.
15
+ 1. **Hook handler** - A fast CLI subprocess that Claude Code invokes on every agent tool call. Evaluates policies and returns a decision.
16
+ 2. **Agent Monitor (Dashboard)** - A Next.js web application for monitoring agent sessions and managing policies.
17
17
 
18
18
  Both subsystems share configuration files in `~/.failproofai/` and the project's `.failproofai/` directory, but they run as separate processes and communicate only through the filesystem.
19
19
 
@@ -23,7 +23,7 @@ Both subsystems share configuration files in `~/.failproofai/` and the project's
23
23
 
24
24
  ### Integration with Claude Code
25
25
 
26
- When you run `failproofai --install-policies`, it writes entries like this into `~/.claude/settings.json`:
26
+ When you run `failproofai policies --install`, it writes entries like this into `~/.claude/settings.json`:
27
27
 
28
28
  ```json
29
29
  {
@@ -60,7 +60,7 @@ Claude Code then invokes `failproofai --hook PreToolUse` as a subprocess before
60
60
  }
61
61
  ```
62
62
 
63
- For `PostToolUse` events, the payload additionally contains `tool_result` with the tool's output.
63
+ For `PostToolUse` events, the payload also contains `tool_result` with the tool's output.
64
64
 
65
65
  The handler enforces a 1 MB stdin limit. Payloads exceeding this are discarded and all policies implicitly allow.
66
66
 
@@ -102,21 +102,38 @@ The handler enforces a 1 MB stdin limit. Payloads exceeding this are discarded a
102
102
  - Exit code: `0`
103
103
  - Empty stdout
104
104
 
105
+ **Allow with message (beta):**
106
+
107
+ Since v0.0.2-beta.3, `allow(message)` lets a policy send informational context back to Claude even when the operation is permitted. The hook handler writes the following JSON to **stdout** (not a config file — this is the handler's response to Claude Code, just like deny and instruct responses above):
108
+
109
+ ```json
110
+ // Written to stdout by the hook handler process
111
+ {
112
+ "hookSpecificOutput": {
113
+ "additionalContext": "All CI checks passed on branch 'feat/my-feature'."
114
+ }
115
+ }
116
+ ```
117
+ - Exit code: `0` (operation is allowed)
118
+ - When multiple policies return `allow` with a message, their messages are joined with newlines into a single `additionalContext` string
119
+ - If no policy provides a message, stdout is empty (same as before)
120
+
105
121
  ### Processing pipeline
106
122
 
107
123
  `src/hooks/handler.ts` implements the full pipeline:
108
124
 
109
- ```
125
+ ```text
110
126
  stdin JSON
111
127
  → parse payload (max 1 MB)
112
128
  → extract session metadata (session_id, cwd, tool_name, tool_input, etc.)
113
129
  → readMergedHooksConfig(cwd) ← merges project + local + global config
114
130
  → register enabled builtin policies with resolved params
115
- → load custom hooks from customPoliciesPath (if set)
116
- → register custom hooks into policy registry
131
+ → load custom policies from customPoliciesPath (if set)
132
+ → register custom policies into policy registry
117
133
  → evaluate all policies (builtins first, then custom)
118
134
  → first deny short-circuits
119
135
  → instruct decisions accumulate
136
+ → allow messages accumulate
120
137
  → write JSON decision to stdout
121
138
  → persist event to ~/.failproofai/hook-activity.jsonl
122
139
  → exit
@@ -130,17 +147,17 @@ The entire process runs in under 100ms for typical payloads with no LLM calls.
130
147
 
131
148
  `src/hooks/hooks-config.ts` implements three-scope config loading.
132
149
 
133
- ```
150
+ ```text
134
151
  [1] {cwd}/.failproofai/policies-config.json ← project (highest priority)
135
152
  [2] {cwd}/.failproofai/policies-config.local.json ← local
136
153
  [3] ~/.failproofai/policies-config.json ← global (lowest priority)
137
154
  ```
138
155
 
139
156
  Merge logic:
140
- - `enabledPolicies` deduplicated union across all three files
141
- - `policyParams` per-policy key, first file that defines it wins entirely
142
- - `customPoliciesPath` first file that defines it wins
143
- - `llm` first file that defines it wins
157
+ - `enabledPolicies` - deduplicated union across all three files
158
+ - `policyParams` - per-policy key, first file that defines it wins entirely
159
+ - `customPoliciesPath` - first file that defines it wins
160
+ - `llm` - first file that defines it wins
144
161
 
145
162
  The web dashboard uses `readHooksConfig()` (global only) for reading and writing, since it is not invoked with a project cwd.
146
163
 
@@ -169,7 +186,7 @@ After all policies run:
169
186
 
170
187
  ## Builtin policies
171
188
 
172
- `src/hooks/builtin-policies.ts` defines all 35+ built-in policies as `BuiltinPolicyDefinition` objects:
189
+ `src/hooks/builtin-policies.ts` defines all 26 built-in policies as `BuiltinPolicyDefinition` objects:
173
190
 
174
191
  ```typescript
175
192
  interface BuiltinPolicyDefinition {
@@ -193,7 +210,7 @@ Pattern matching inside policies uses parsed command tokens (argv), not raw stri
193
210
 
194
211
  ---
195
212
 
196
- ## Custom hooks
213
+ ## Custom policies
197
214
 
198
215
  `src/hooks/custom-hooks-registry.ts` implements a `globalThis`-backed registry:
199
216
 
@@ -208,7 +225,7 @@ export function getCustomHooks(): CustomHook[] { ... }
208
225
  export function clearCustomHooks(): void { ... } // used in tests
209
226
  ```
210
227
 
211
- `src/hooks/custom-hooks-loader.ts` loads the user's hooks file:
228
+ `src/hooks/custom-hooks-loader.ts` loads the user's policy file:
212
229
 
213
230
  1. Read `customPoliciesPath` from config; skip if absent.
214
231
  2. Resolve to absolute path; check file exists.
@@ -220,7 +237,7 @@ export function clearCustomHooks(): void { ... } // used in tests
220
237
 
221
238
  On any error (file not found, syntax error, import failure), the error is logged to `~/.failproofai/hook.log` and the loader returns an empty array. Built-in policies are unaffected.
222
239
 
223
- Custom hooks are evaluated after all built-in policies. A custom hook `deny` still short-circuits further custom hooks (but all built-ins have already run by that point).
240
+ Custom policies are evaluated after all built-in policies. A custom policy `deny` still short-circuits further custom policies (but all built-ins have already run by that point).
224
241
 
225
242
  ---
226
243
 
@@ -249,7 +266,7 @@ One line per policy that made a non-allow decision. Allow decisions are not logg
249
266
 
250
267
  The dashboard is a **Next.js 16** application using the App Router with React Server Components and Server Actions.
251
268
 
252
- ```
269
+ ```text
253
270
  app/
254
271
  layout.tsx ← Root layout (theme, telemetry, nav)
255
272
  projects/page.tsx ← Server component: list all Claude projects
@@ -275,22 +292,22 @@ app/
275
292
 
276
293
  **Key design decisions:**
277
294
 
278
- - No database all persistent state is in plain files (`~/.failproofai/`, `~/.claude/projects/`).
279
- - Server Actions for mutations no REST API needed for CRUD operations.
280
- - React Server Components for read pages faster initial load, no client bundle for data fetching.
295
+ - No database - all persistent state is in plain files (`~/.failproofai/`, `~/.claude/projects/`).
296
+ - Server Actions for mutations - no REST API needed for CRUD operations.
297
+ - React Server Components for read pages - faster initial load, no client bundle for data fetching.
281
298
  - Client components only where interactivity is needed (policy toggles, activity search, log viewer).
282
299
 
283
300
  ---
284
301
 
285
302
  ## File layout
286
303
 
287
- ```
304
+ ```text
288
305
  failproofai/
289
306
  ├── bin/
290
307
  │ └── failproofai.mjs # CLI router (hook / dashboard / install / etc.)
291
308
  ├── src/hooks/
292
309
  │ ├── handler.ts # Hook event pipeline
293
- │ ├── builtin-policies.ts # 35+ policy definitions
310
+ │ ├── builtin-policies.ts # 26 policy definitions
294
311
  │ ├── policy-evaluator.ts # Policy execution engine
295
312
  │ ├── policy-registry.ts # Policy registration and lookup
296
313
  │ ├── policy-types.ts # TypeScript interfaces