failproofai 1.0.1 → 1.0.2-beta.1

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 (390) 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 +6 -6
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js +2 -2
  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 +6 -6
  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/_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 +2 -2
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js +3 -2
  24. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js +5 -4
  26. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +2 -2
  29. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +4 -3
  31. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/logout/route.js +4 -4
  33. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/auth/status/route.js +4 -4
  35. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  38. package/.next/standalone/.next/server/app/audit/page.js +2 -2
  39. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/index.html +1 -1
  42. package/.next/standalone/.next/server/app/index.rsc +15 -15
  43. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  44. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  45. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  46. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  47. package/.next/standalone/.next/server/app/page.js +2 -2
  48. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +66 -22
  51. package/.next/standalone/.next/server/app/policies/page.js +5 -4
  52. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  55. package/.next/standalone/.next/server/app/project/[name]/page.js +2 -2
  56. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  59. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  60. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +2 -2
  61. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  64. package/.next/standalone/.next/server/app/projects/page.js +2 -2
  65. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  68. package/.next/standalone/.next/server/app/settings/page.js +1 -1
  69. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/chunks/{[externals]__1qzqcio._.js → [externals]__0e32-7c._.js} +1 -1
  72. package/.next/standalone/.next/server/chunks/[externals]__1s462yl._.js +3 -0
  73. package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +3 -0
  74. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1p8poxd._.js → [root-of-the-server]__0igxt32._.js} +2 -2
  76. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0neqlu-._.js +3 -0
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +3 -0
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t9oqnn._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +3 -0
  80. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/_185gyk9._.js +3 -0
  83. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +5 -6
  84. package/.next/standalone/.next/server/chunks/lib_telemetry-id_ts_1k8gj9o._.js +3 -0
  85. package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_09d5c79._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_04op_ou._.js} +1 -1
  86. package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_1d2g73o._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0dr20uz._.js} +1 -1
  87. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/src_hooks_0xbw3bu._.js +12 -0
  89. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +4 -0
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +4 -0
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +4 -0
  93. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0spkm68._.js → [root-of-the-server]__07gm3zl._.js} +21 -20
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +4 -0
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +4 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0l4748x._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +4 -0
  99. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +6 -0
  100. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__192nhvh._.js +5 -0
  101. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
  102. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/_0bwu160._.js +23 -0
  104. package/.next/standalone/.next/server/chunks/ssr/{_0u5zauv._.js → _0ns6k3a._.js} +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +13 -13
  108. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  110. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/src_hooks_08o7bqo._.js +10 -0
  112. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  113. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  114. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  115. package/.next/standalone/.next/server/middleware.js +1 -1
  116. package/.next/standalone/.next/server/pages/404.html +1 -1
  117. package/.next/standalone/.next/server/pages/500.html +1 -1
  118. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  119. package/.next/standalone/.next/server/server-reference-manifest.json +71 -27
  120. package/.next/standalone/.next/static/chunks/{3c808qrfcq0im.js → 01u1cf8js2yd6.js} +1 -1
  121. package/.next/standalone/.next/static/chunks/{0k5050ezjvv49.js → 0izgu7nqh2zp-.js} +1 -1
  122. package/.next/standalone/.next/static/chunks/0u4-org_e7zde.js +1 -0
  123. package/.next/standalone/.next/static/chunks/1mfzsja6ftim5.css +2 -0
  124. package/.next/standalone/.next/static/chunks/{2zpp1we2jv6tb.js → 1tynnrrh711lr.js} +1 -1
  125. package/.next/standalone/.next/static/chunks/{1ide1wl2f7zi1.js → 22rqvlr7p_1mv.js} +13 -13
  126. package/.next/standalone/.next/static/chunks/{31s1smp-g7gks.js → 2whxqyskpirbk.js} +1 -1
  127. package/.next/standalone/.next/static/chunks/31mwk8k0q3_3g.js +6 -0
  128. package/.next/standalone/.next/static/chunks/3ms6gxfm82t6s.js +1 -0
  129. package/.next/standalone/.next/static/chunks/{37fcpc36x9gpe.js → 3tbbs_d8xrvyp.js} +1 -1
  130. package/.next/standalone/.next/static/chunks/43nlc236oo898.js +1 -0
  131. package/.next/standalone/SECURITY.md +5 -4
  132. package/.next/standalone/app/actions/get-hooks-config.ts +108 -17
  133. package/.next/standalone/app/actions/pack-actions.ts +163 -0
  134. package/.next/standalone/app/audit/_components/run-progress.tsx +1 -1
  135. package/.next/standalone/app/audit/_components/share-templates.ts +13 -13
  136. package/.next/standalone/app/policies/hooks-client.tsx +320 -15
  137. package/.next/standalone/components/navbar.tsx +2 -28
  138. package/.next/standalone/components/reach-developers.tsx +8 -1
  139. package/.next/standalone/docs-old/agenteye/python-sdk-skill.mdx +1 -1
  140. package/.next/standalone/docs-old/agenteye/python-sdk.mdx +59 -35
  141. package/.next/standalone/docs-old/i18n/README.ar.md +1 -0
  142. package/.next/standalone/docs-old/i18n/README.de.md +1 -0
  143. package/.next/standalone/docs-old/i18n/README.es.md +1 -0
  144. package/.next/standalone/docs-old/i18n/README.fr.md +1 -0
  145. package/.next/standalone/docs-old/i18n/README.he.md +1 -0
  146. package/.next/standalone/docs-old/i18n/README.hi.md +1 -0
  147. package/.next/standalone/docs-old/i18n/README.it.md +1 -0
  148. package/.next/standalone/docs-old/i18n/README.ja.md +1 -0
  149. package/.next/standalone/docs-old/i18n/README.ko.md +1 -0
  150. package/.next/standalone/docs-old/i18n/README.pt-br.md +1 -0
  151. package/.next/standalone/docs-old/i18n/README.ru.md +1 -0
  152. package/.next/standalone/docs-old/i18n/README.tr.md +1 -0
  153. package/.next/standalone/docs-old/i18n/README.vi.md +1 -0
  154. package/.next/standalone/docs-old/i18n/README.zh.md +1 -0
  155. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +431 -0
  156. package/.next/standalone/fp-cloud-cli/LICENSE +42 -0
  157. package/.next/standalone/fp-cloud-cli/MANIFEST.in +10 -0
  158. package/.next/standalone/fp-cloud-cli/README.md +259 -0
  159. package/.next/standalone/fp-cloud-cli/fp_cli/__init__.py +10 -0
  160. package/.next/standalone/fp-cloud-cli/fp_cli/__main__.py +4 -0
  161. package/.next/standalone/fp-cloud-cli/fp_cli/_click_compat.py +64 -0
  162. package/.next/standalone/fp-cloud-cli/fp_cli/_context.py +332 -0
  163. package/.next/standalone/fp-cloud-cli/fp_cli/_version.py +1 -0
  164. package/.next/standalone/fp-cloud-cli/fp_cli/analytics.py +432 -0
  165. package/.next/standalone/fp-cloud-cli/fp_cli/analytics_config.py +77 -0
  166. package/.next/standalone/fp-cloud-cli/fp_cli/analytics_registry.py +83 -0
  167. package/.next/standalone/fp-cloud-cli/fp_cli/app.py +492 -0
  168. package/.next/standalone/fp-cloud-cli/fp_cli/auth.py +160 -0
  169. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +1694 -0
  170. package/.next/standalone/fp-cloud-cli/fp_cli/commands/__init__.py +0 -0
  171. package/.next/standalone/fp-cloud-cli/fp_cli/commands/_write.py +214 -0
  172. package/.next/standalone/fp-cloud-cli/fp_cli/commands/agent_cmds.py +407 -0
  173. package/.next/standalone/fp-cloud-cli/fp_cli/commands/alerts_cmds.py +445 -0
  174. package/.next/standalone/fp-cloud-cli/fp_cli/commands/audits_cmds.py +1054 -0
  175. package/.next/standalone/fp-cloud-cli/fp_cli/commands/auth_cmds.py +512 -0
  176. package/.next/standalone/fp-cloud-cli/fp_cli/commands/errors_cmds.py +190 -0
  177. package/.next/standalone/fp-cloud-cli/fp_cli/commands/evals_cmds.py +161 -0
  178. package/.next/standalone/fp-cloud-cli/fp_cli/commands/events_cmds.py +159 -0
  179. package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +416 -0
  180. package/.next/standalone/fp-cloud-cli/fp_cli/commands/guardrails_cmds.py +148 -0
  181. package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +472 -0
  182. package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +407 -0
  183. package/.next/standalone/fp-cloud-cli/fp_cli/commands/list_cmds.py +63 -0
  184. package/.next/standalone/fp-cloud-cli/fp_cli/commands/orgs_cmds.py +319 -0
  185. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +499 -0
  186. package/.next/standalone/fp-cloud-cli/fp_cli/commands/queries_cmds.py +378 -0
  187. package/.next/standalone/fp-cloud-cli/fp_cli/commands/sessions_cmds.py +151 -0
  188. package/.next/standalone/fp-cloud-cli/fp_cli/commands/settings_cmds.py +150 -0
  189. package/.next/standalone/fp-cloud-cli/fp_cli/commands/usage_cmds.py +35 -0
  190. package/.next/standalone/fp-cloud-cli/fp_cli/commands/users_cmds.py +404 -0
  191. package/.next/standalone/fp-cloud-cli/fp_cli/config.py +330 -0
  192. package/.next/standalone/fp-cloud-cli/fp_cli/dates.py +78 -0
  193. package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +345 -0
  194. package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +98 -0
  195. package/.next/standalone/fp-cloud-cli/fp_cli/models.py +891 -0
  196. package/.next/standalone/fp-cloud-cli/fp_cli/orgs.py +30 -0
  197. package/.next/standalone/fp-cloud-cli/fp_cli/output.py +6593 -0
  198. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +208 -0
  199. package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +290 -0
  200. package/.next/standalone/fp-cloud-cli/fp_cli/py.typed +0 -0
  201. package/.next/standalone/fp-cloud-cli/fp_cli/select.py +322 -0
  202. package/.next/standalone/fp-cloud-cli/fp_cli/theme.py +53 -0
  203. package/.next/standalone/fp-cloud-cli/pyproject.toml +106 -0
  204. package/.next/standalone/fp-cloud-cli/skill/SKILL.md +302 -0
  205. package/.next/standalone/fp-cloud-cli/skill/agents/openai.yaml +8 -0
  206. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +254 -0
  207. package/.next/standalone/fp-cloud-cli/tests/__init__.py +0 -0
  208. package/.next/standalone/fp-cloud-cli/tests/conftest.py +92 -0
  209. package/.next/standalone/fp-cloud-cli/tests/test_alerting.py +644 -0
  210. package/.next/standalone/fp-cloud-cli/tests/test_analytics.py +385 -0
  211. package/.next/standalone/fp-cloud-cli/tests/test_audits.py +950 -0
  212. package/.next/standalone/fp-cloud-cli/tests/test_auth.py +98 -0
  213. package/.next/standalone/fp-cloud-cli/tests/test_auth_mode.py +272 -0
  214. package/.next/standalone/fp-cloud-cli/tests/test_click_compat.py +118 -0
  215. package/.next/standalone/fp-cloud-cli/tests/test_client.py +299 -0
  216. package/.next/standalone/fp-cloud-cli/tests/test_commands.py +814 -0
  217. package/.next/standalone/fp-cloud-cli/tests/test_config.py +99 -0
  218. package/.next/standalone/fp-cloud-cli/tests/test_dashboards_agent.py +340 -0
  219. package/.next/standalone/fp-cloud-cli/tests/test_dates.py +76 -0
  220. package/.next/standalone/fp-cloud-cli/tests/test_enforcement_logic.py +481 -0
  221. package/.next/standalone/fp-cloud-cli/tests/test_facets.py +170 -0
  222. package/.next/standalone/fp-cloud-cli/tests/test_failproofai_home.py +722 -0
  223. package/.next/standalone/fp-cloud-cli/tests/test_fp_home_contract.py +198 -0
  224. package/.next/standalone/fp-cloud-cli/tests/test_hardening.py +186 -0
  225. package/.next/standalone/fp-cloud-cli/tests/test_help_table_coverage.py +127 -0
  226. package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +496 -0
  227. package/.next/standalone/fp-cloud-cli/tests/test_list.py +59 -0
  228. package/.next/standalone/fp-cloud-cli/tests/test_multivalue.py +183 -0
  229. package/.next/standalone/fp-cloud-cli/tests/test_no_customer_identifiers.py +168 -0
  230. package/.next/standalone/fp-cloud-cli/tests/test_operator.py +492 -0
  231. package/.next/standalone/fp-cloud-cli/tests/test_orgs.py +692 -0
  232. package/.next/standalone/fp-cloud-cli/tests/test_output.py +1732 -0
  233. package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +191 -0
  234. package/.next/standalone/fp-cloud-cli/tests/test_readme_matches_reality.py +125 -0
  235. package/.next/standalone/fp-cloud-cli/tests/test_review_fixes.py +161 -0
  236. package/.next/standalone/fp-cloud-cli/tests/test_telemetry_completeness.py +90 -0
  237. package/.next/standalone/fp-cloud-cli/tests/test_usage.py +115 -0
  238. package/.next/standalone/fp-cloud-cli/tests/test_v1_origin_diagnostic.py +70 -0
  239. package/.next/standalone/fp-cloud-cli/tests/test_v1_routing.py +214 -0
  240. package/.next/standalone/fp-cloud-cli/tests/test_whoami.py +104 -0
  241. package/.next/standalone/fp-cloud-cli/uv.lock +552 -0
  242. package/.next/standalone/package.json +27 -21
  243. package/.next/standalone/sdk/python/CHANGELOG.md +70 -0
  244. package/.next/standalone/sdk/python/LICENSE +42 -0
  245. package/.next/standalone/sdk/python/MANIFEST.in +21 -0
  246. package/.next/standalone/sdk/python/README.md +466 -0
  247. package/.next/standalone/sdk/python/docs/README.md +249 -0
  248. package/.next/standalone/sdk/python/docs/_shared/README.md +43 -0
  249. package/.next/standalone/sdk/python/docs/_shared/__init__.py +195 -0
  250. package/.next/standalone/sdk/python/docs/crewai/README.md +328 -0
  251. package/.next/standalone/sdk/python/docs/crewai/examples/quickstart.py +61 -0
  252. package/.next/standalone/sdk/python/docs/crewai/examples/research_crew.py +92 -0
  253. package/.next/standalone/sdk/python/docs/langgraph/README.md +481 -0
  254. package/.next/standalone/sdk/python/docs/langgraph/examples/quickstart.py +51 -0
  255. package/.next/standalone/sdk/python/docs/langgraph/examples/research_agent.py +86 -0
  256. package/.next/standalone/sdk/python/docs/langgraph/examples/supervisor_handoff.py +83 -0
  257. package/.next/standalone/sdk/python/docs/llama_index/README.md +332 -0
  258. package/.next/standalone/sdk/python/docs/llama_index/examples/quickstart.py +62 -0
  259. package/.next/standalone/sdk/python/docs/llama_index/examples/research_agent.py +93 -0
  260. package/.next/standalone/sdk/python/docs/manual/README.md +491 -0
  261. package/.next/standalone/sdk/python/docs/manual/examples/quickstart.py +57 -0
  262. package/.next/standalone/sdk/python/docs/manual/examples/research_agent.py +133 -0
  263. package/.next/standalone/sdk/python/docs/pydantic_ai/README.md +323 -0
  264. package/.next/standalone/sdk/python/docs/pydantic_ai/examples/quickstart.py +48 -0
  265. package/.next/standalone/sdk/python/docs/pydantic_ai/examples/research_agent.py +94 -0
  266. package/.next/standalone/sdk/python/failproofai_sdk/__init__.py +131 -0
  267. package/.next/standalone/sdk/python/failproofai_sdk/_context.py +218 -0
  268. package/.next/standalone/sdk/python/failproofai_sdk/_environment.py +78 -0
  269. package/.next/standalone/sdk/python/failproofai_sdk/_events.py +752 -0
  270. package/.next/standalone/sdk/python/failproofai_sdk/_resolver.py +118 -0
  271. package/.next/standalone/sdk/python/failproofai_sdk/_runtime.py +21 -0
  272. package/.next/standalone/sdk/python/failproofai_sdk/_schema.py +317 -0
  273. package/.next/standalone/sdk/python/failproofai_sdk/_scopes.py +422 -0
  274. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -0
  275. package/.next/standalone/sdk/python/failproofai_sdk/_writer.py +750 -0
  276. package/.next/standalone/sdk/python/failproofai_sdk/integrations/__init__.py +274 -0
  277. package/.next/standalone/sdk/python/failproofai_sdk/integrations/_compat.py +247 -0
  278. package/.next/standalone/sdk/python/failproofai_sdk/integrations/_core.py +1076 -0
  279. package/.next/standalone/sdk/python/failproofai_sdk/integrations/crewai.py +1669 -0
  280. package/.next/standalone/sdk/python/failproofai_sdk/integrations/langchain.py +2151 -0
  281. package/.next/standalone/sdk/python/failproofai_sdk/integrations/llama_index.py +1588 -0
  282. package/.next/standalone/sdk/python/failproofai_sdk/integrations/pydantic_ai.py +977 -0
  283. package/.next/standalone/sdk/python/failproofai_sdk/py.typed +0 -0
  284. package/.next/standalone/sdk/python/pyproject.toml +92 -0
  285. package/.next/standalone/sdk/python/skill/SKILL.md +399 -0
  286. package/.next/standalone/sdk/python/skill/agents/openai.yaml +8 -0
  287. package/.next/standalone/sdk/python/skill/references/events.md +234 -0
  288. package/.next/standalone/sdk/python/skill/references/frameworks.md +314 -0
  289. package/.next/standalone/sdk/python/skill/references/install.md +100 -0
  290. package/.next/standalone/sdk/python/skill/references/integration.md +352 -0
  291. package/.next/standalone/sdk/python/tests/__init__.py +0 -0
  292. package/.next/standalone/sdk/python/tests/conftest.py +169 -0
  293. package/.next/standalone/sdk/python/tests/integrations/__init__.py +6 -0
  294. package/.next/standalone/sdk/python/tests/integrations/test_crewai.py +1299 -0
  295. package/.next/standalone/sdk/python/tests/integrations/test_langchain.py +2338 -0
  296. package/.next/standalone/sdk/python/tests/integrations/test_llama_index.py +1576 -0
  297. package/.next/standalone/sdk/python/tests/integrations/test_pydantic_ai.py +1102 -0
  298. package/.next/standalone/sdk/python/tests/test_context.py +410 -0
  299. package/.next/standalone/sdk/python/tests/test_docs.py +411 -0
  300. package/.next/standalone/sdk/python/tests/test_durability.py +1352 -0
  301. package/.next/standalone/sdk/python/tests/test_encoding.py +571 -0
  302. package/.next/standalone/sdk/python/tests/test_integrations.py +1127 -0
  303. package/.next/standalone/sdk/python/tests/test_no_customer_identifiers.py +214 -0
  304. package/.next/standalone/sdk/python/tests/test_packaging.py +185 -0
  305. package/.next/standalone/sdk/python/tests/test_resolver_umbrella.py +178 -0
  306. package/.next/standalone/sdk/python/tests/test_scopes.py +487 -0
  307. package/.next/standalone/sdk/python/tests/test_sdk.py +923 -0
  308. package/.next/standalone/sdk/python/tests/test_server_contract.py +650 -0
  309. package/.next/standalone/sdk/python/tests/test_site_docs.py +371 -0
  310. package/.next/standalone/sdk/python/tests/test_skill_snippets.py +165 -0
  311. package/.next/standalone/sdk/python/tests/test_spool_contract.py +287 -0
  312. package/.next/standalone/sdk/python/tests/test_spool_creation.py +360 -0
  313. package/.next/standalone/sdk/python/tests/test_wire_format.py +279 -0
  314. package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +378 -0
  315. package/.next/standalone/sdk/python/uv.lock +6005 -0
  316. package/.next/standalone/server.js +1 -1
  317. package/README.md +77 -21
  318. package/bin/failproofai.mjs +579 -306
  319. package/dist/cli.mjs +6499 -3662
  320. package/dist/index.js +5 -5
  321. package/dist/worker.mjs +2390 -1978
  322. package/package.json +27 -21
  323. package/scripts/build-policy-pack.mjs +121 -0
  324. package/scripts/changelog-section.py +77 -0
  325. package/scripts/prune-standalone.mjs +3 -0
  326. package/scripts/python-version.py +232 -0
  327. package/scripts/translate-docs/cli.ts +21 -0
  328. package/scripts/translate-docs/mintlify-nav.ts +85 -13
  329. package/scripts/translate-docs/translator.ts +66 -11
  330. package/src/audit/cache.ts +40 -3
  331. package/src/audit/cli.ts +1 -1
  332. package/src/audit/index.ts +1 -1
  333. package/src/audit/replay.ts +89 -3
  334. package/src/audit/report.ts +5 -5
  335. package/src/audit/schedule-cli.ts +49 -53
  336. package/src/hooks/builtin-policies.ts +217 -579
  337. package/src/hooks/cloud-enrollment-cli.ts +66 -27
  338. package/src/hooks/cloud-managed-policies.ts +6 -1
  339. package/src/hooks/configure-wizard.ts +56 -369
  340. package/src/hooks/custom-hooks-loader.ts +89 -6
  341. package/src/hooks/fp-home.ts +92 -4
  342. package/src/hooks/fp-reset.ts +34 -2
  343. package/src/hooks/handler.ts +181 -11
  344. package/src/hooks/harness-cli.ts +79 -25
  345. package/src/hooks/hook-activity-store.ts +13 -3
  346. package/src/hooks/install-prompt.ts +25 -3
  347. package/src/hooks/manager.ts +424 -172
  348. package/src/hooks/migrations.ts +2 -2
  349. package/src/hooks/pack-cli.ts +885 -0
  350. package/src/hooks/pack-failclosed.ts +189 -0
  351. package/src/hooks/pack-manifest.ts +352 -0
  352. package/src/hooks/pack-store.ts +891 -0
  353. package/src/hooks/policy-catalog.ts +535 -0
  354. package/src/hooks/policy-evaluator.ts +24 -16
  355. package/src/hooks/policy-registry.ts +8 -2
  356. package/src/hooks/policy-types.ts +26 -0
  357. package/src/hooks/session-pause-cli.ts +33 -9
  358. package/src/hooks/tui.ts +695 -39
  359. package/src/hooks/types.ts +15 -1
  360. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_usx0j._.js +0 -3
  361. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_zk340._.js +0 -3
  362. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0agdw2a._.js +0 -3
  363. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d1u_na._.js +0 -3
  364. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +0 -3
  365. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +0 -3
  366. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tvn4wf._.js +0 -3
  367. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +0 -3
  368. package/.next/standalone/.next/server/chunks/_1ger0a2._.js +0 -3
  369. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +0 -3
  370. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_1iiwm5e._.js +0 -3
  371. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__07tbbf3._.js +0 -3
  372. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kc8_v5._.js +0 -4
  373. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kgy9da._.js +0 -4
  374. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mhqk5a._.js +0 -4
  375. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pltpn8._.js +0 -4
  376. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qkcyf-._.js +0 -4
  377. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w0x_ok._.js +0 -4
  378. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1_dfocn._.js +0 -4
  379. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1bfex62._.js +0 -5
  380. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1kzapg5._.js +0 -6
  381. package/.next/standalone/.next/server/chunks/ssr/_0lvyuv4._.js +0 -23
  382. package/.next/standalone/.next/static/chunks/2_2lhuojq2xop.js +0 -1
  383. package/.next/standalone/.next/static/chunks/36fw6yk1s6x_f.js +0 -1
  384. package/.next/standalone/.next/static/chunks/3k7mxbp9hiom0.js +0 -1
  385. package/.next/standalone/.next/static/chunks/3tzq0260n17ae.js +0 -6
  386. package/.next/standalone/.next/static/chunks/3wq9x0c-7ld5d.css +0 -2
  387. package/src/hooks/policy-presets.ts +0 -135
  388. /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → I5kEb9kSgYS63j4iqfVLO}/_buildManifest.js +0 -0
  389. /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → I5kEb9kSgYS63j4iqfVLO}/_clientMiddlewareManifest.js +0 -0
  390. /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → I5kEb9kSgYS63j4iqfVLO}/_ssgManifest.js +0 -0
package/dist/worker.mjs CHANGED
@@ -370,7 +370,7 @@ function failproofaiHome(home) {
370
370
  return resolve(home, ".failproofai");
371
371
  return process.env.FAILPROOFAI_HOME || resolve(homedir(), ".failproofai");
372
372
  }
373
- var LAYOUT_VERSION = 4, at = (...parts) => resolve(failproofaiHome(), ...parts), atHome = (home, ...parts) => home ? resolve(failproofaiHome(home), ...parts) : at(...parts), versionFile = (home) => atHome(home, "VERSION"), configFile = (home) => atHome(home, "config.json"), policiesDir = (home) => atHome(home, "policies"), customPoliciesDir, globalPolicyConfigFile = (home) => atHome(home, "policies-config.json"), cloudPoliciesDir = (home) => resolve(policiesDir(home), "cloud-policies"), hookActivityDir = (home) => atHome(home, "hook-activity"), stateDir = (home) => atHome(home, "state"), shimsDir = (home) => resolve(stateDir(home), "shims"), sessionPauseDir = () => resolve(stateDir(), "sessions"), telemetryIdFile = (home) => resolve(stateDir(home), "telemetry-id"), codexSessionPathsFile = (home) => resolve(stateDir(home), "codex-session-paths.json"), logsDir = (home) => atHome(home, "logs"), legacy;
373
+ var LAYOUT_VERSION = 4, at = (...parts) => resolve(failproofaiHome(), ...parts), atHome = (home, ...parts) => home ? resolve(failproofaiHome(home), ...parts) : at(...parts), versionFile = (home) => atHome(home, "VERSION"), configFile = (home) => atHome(home, "config.json"), policiesDir = (home) => atHome(home, "policies"), customPoliciesDir, globalPolicyConfigFile = (home) => atHome(home, "policies-config.json"), cloudPoliciesDir = (home) => resolve(policiesDir(home), "cloud-policies"), packsDir = (home) => resolve(policiesDir(home), "packs"), packsInstalledFile = (home) => resolve(packsDir(home), "installed.json"), hookActivityDir = (home) => atHome(home, "hook-activity"), stateDir = (home) => atHome(home, "state"), shimsDir = (home) => resolve(stateDir(home), "shims"), sessionPauseDir = () => resolve(stateDir(), "sessions"), telemetryIdFile = (home) => resolve(stateDir(home), "telemetry-id"), codexSessionPathsFile = (home) => resolve(stateDir(home), "codex-session-paths.json"), logsDir = (home) => atHome(home, "logs"), legacy;
374
374
  var init_fp_home = __esm(() => {
375
375
  customPoliciesDir = policiesDir;
376
376
  legacy = {
@@ -566,1963 +566,2000 @@ var init_hooks_config = __esm(() => {
566
566
  init_fp_home();
567
567
  });
568
568
 
569
- // src/hooks/policy-helpers.ts
570
- function allow(reason) {
571
- return reason ? { decision: "allow", reason } : { decision: "allow" };
572
- }
573
- function deny(reason) {
574
- return { decision: "deny", reason };
575
- }
576
- function instruct(reason) {
577
- return { decision: "instruct", reason };
578
- }
579
-
580
- // src/hooks/policy-registry.ts
581
- function normalizePolicyName(name) {
582
- return name.includes("/") ? name : `${DEFAULT_POLICY_NAMESPACE}/${name}`;
583
- }
584
- function getIndexCache() {
585
- return globalThis[INDEX_CACHE_KEY];
586
- }
587
- function setIndexCache(cache) {
588
- globalThis[INDEX_CACHE_KEY] = cache;
589
- }
590
- function getRegistry() {
591
- const g = globalThis;
592
- if (!g[REGISTRY_KEY]) {
593
- g[REGISTRY_KEY] = [];
594
- }
595
- return g[REGISTRY_KEY];
596
- }
597
- function registerPolicy(name, description, fn, match, priority = 0) {
598
- const canonical = normalizePolicyName(name);
599
- const registry = getRegistry();
600
- const idx = registry.findIndex((p) => p.name === canonical);
601
- const entry = { name: canonical, description, fn, match, priority };
602
- if (idx >= 0) {
603
- registry[idx] = entry;
604
- } else {
605
- registry.push(entry);
606
- }
607
- setIndexCache(null);
608
- }
609
- function getPoliciesForEvent(eventType, toolName) {
610
- let cache = getIndexCache();
611
- if (!cache) {
612
- cache = new Map;
613
- setIndexCache(cache);
614
- }
615
- const key = `${eventType}:${toolName ?? ""}`;
616
- const cached = cache.get(key);
617
- if (cached)
618
- return cached;
619
- const result = getRegistry().filter((p) => {
620
- if (p.match.events && p.match.events.length > 0) {
621
- if (!p.match.events.includes(eventType))
622
- return false;
623
- }
624
- if (p.match.toolNames && p.match.toolNames.length > 0) {
625
- if (!toolName || !p.match.toolNames.includes(toolName))
626
- return false;
627
- }
628
- return true;
629
- }).sort((a, b) => b.priority - a.priority);
630
- cache.set(key, result);
631
- return result;
632
- }
633
- function clearPolicies() {
634
- const g = globalThis;
635
- g[REGISTRY_KEY] = [];
636
- setIndexCache(null);
637
- }
638
- var REGISTRY_KEY = "__FAILPROOFAI_POLICY_REGISTRY__", INDEX_CACHE_KEY = "__FAILPROOFAI_POLICY_INDEX_CACHE__", DEFAULT_POLICY_NAMESPACE = "failproofai";
639
-
640
- // src/hooks/builtin-policies.ts
641
- import { resolve as resolve3, join as join2 } from "node:path";
642
- import { statSync as statSync3 } from "node:fs";
643
- import { readFile, writeFile } from "node:fs/promises";
644
- import { execSync, execFileSync } from "node:child_process";
645
- import { homedir as homedir3 } from "node:os";
646
- function isAgentInternalPath(resolved2) {
647
- const normResolved = resolved2.replaceAll("\\", "/");
648
- for (const dir of [".claude", ".codex", ".copilot", ".cursor", ".opencode", ".pi", ".gemini"]) {
649
- const root = join2(homedir3(), dir).replaceAll("\\", "/");
650
- if (normResolved === root || normResolved.startsWith(root + "/"))
651
- return true;
652
- }
653
- for (const sub of [join2(".config", "opencode"), join2(".local", "share", "opencode")]) {
654
- const root = join2(homedir3(), sub).replaceAll("\\", "/");
655
- if (normResolved === root || normResolved.startsWith(root + "/"))
656
- return true;
657
- }
658
- return false;
659
- }
660
- function isAgentSettingsFile(resolved2) {
661
- if (/[\\/]\.claude[\\/]settings(?:\.[^/\\]+)?\.json$/.test(resolved2))
662
- return true;
663
- if (/[\\/]\.codex[\\/]hooks\.json$/.test(resolved2))
664
- return true;
665
- if (/[\\/]\.copilot[\\/]hooks[\\/][^/\\]+\.json$/.test(resolved2))
666
- return true;
667
- if (/[\\/]\.github[\\/]hooks[\\/][^/\\]+\.json$/.test(resolved2))
668
- return true;
669
- if (/[\\/]\.cursor[\\/]hooks\.json$/.test(resolved2))
670
- return true;
671
- if (/[\\/]\.opencode[\\/]opencode\.jsonc?$/.test(resolved2))
672
- return true;
673
- if (/[\\/]\.opencode[\\/]plugins[\\/][^/\\]+\.(?:mjs|js|ts)$/.test(resolved2))
674
- return true;
675
- if (/[\\/]\.config[\\/]opencode[\\/]opencode\.jsonc?$/.test(resolved2))
676
- return true;
677
- if (/[\\/]\.config[\\/]opencode[\\/]config\.json$/.test(resolved2))
678
- return true;
679
- if (/[\\/]\.config[\\/]opencode[\\/]plugins[\\/][^/\\]+\.(?:mjs|js|ts)$/.test(resolved2))
680
- return true;
681
- if (/[\\/]\.pi[\\/](?:agent[\\/])?settings\.json$/.test(resolved2))
682
- return true;
683
- if (/[\\/]\.pi[\\/](?:agent[\\/])?extensions[\\/]/.test(resolved2))
684
- return true;
685
- if (/[\\/]\.gemini[\\/]settings\.json$/.test(resolved2))
686
- return true;
687
- if (/[\\/]\.gemini[\\/]config[\\/]hooks\.json$/.test(resolved2))
688
- return true;
689
- return false;
690
- }
691
- function getCommand(ctx) {
692
- return ctx.toolInput?.command ?? "";
693
- }
694
- function getFilePath(ctx) {
695
- return ctx.toolInput?.file_path ?? "";
696
- }
697
- function parseArgvTokens(cmd) {
698
- return cmd.trim().split(/\s+/).map((t) => t.replace(/^['"]|['"]$/g, ""));
699
- }
700
- function namesSelfPause(command) {
701
- for (const segment of command.split(SEGMENT_SEPARATORS)) {
702
- const tokens = segment.split(/\s+/).filter(Boolean);
703
- let i = 0;
704
- while (i < tokens.length) {
705
- const token = tokens[i];
706
- const isSkippable = ENV_ASSIGNMENT_RE.test(token) || token.startsWith("-") || token.startsWith(">") || token.startsWith("<") || RUNNER_OPERAND_RE.test(token) || COMMAND_PREFIX_TOKENS.has(token.slice(token.lastIndexOf("/") + 1));
707
- if (!isSkippable)
708
- break;
709
- i++;
710
- }
711
- if (i >= tokens.length)
712
- continue;
713
- if (!SELF_BINARY_TOKEN_RE.test(tokens[i]))
714
- continue;
715
- const args = tokens.slice(i + 1);
716
- const configAt = args.findIndex((a) => CONFIG_SUBCOMMAND_RE.test(a));
717
- if (configAt === -1)
718
- continue;
719
- if (args.slice(configAt + 1).some((a) => PAUSE_FLAG_RE.test(a)))
720
- return true;
721
- }
722
- return false;
723
- }
724
- function decodeAnsiC(body) {
725
- return body.replace(/\\(x[0-9A-Fa-f]{1,2}|u[0-9A-Fa-f]{1,4}|U[0-9A-Fa-f]{1,8}|[0-7]{1,3}|.)/g, (_, seq) => {
726
- try {
727
- if (seq[0] === "x")
728
- return String.fromCharCode(parseInt(seq.slice(1), 16));
729
- if (seq[0] === "u" || seq[0] === "U")
730
- return String.fromCodePoint(parseInt(seq.slice(1), 16));
731
- if (/^[0-7]+$/.test(seq))
732
- return String.fromCharCode(parseInt(seq, 8));
733
- } catch {
734
- return seq;
735
- }
736
- const named = {
737
- a: "\x07",
738
- b: "\b",
739
- e: "\x1B",
740
- f: "\f",
741
- n: `
742
- `,
743
- r: "\r",
744
- t: "\t",
745
- v: "\v",
746
- "\\": "\\",
747
- "'": "'",
748
- '"': '"',
749
- "?": "?"
750
- };
751
- return named[seq] ?? seq;
752
- });
753
- }
754
- function stripShellQuoting(command) {
755
- const joined = command.replace(/\\\r?\n/g, "");
756
- const ansiDecoded = joined.replace(/\$'((?:[^'\\]|\\.)*)'/g, (_, body) => decodeAnsiC(body));
757
- return ansiDecoded.replace(/\\(.)/g, "$1").replace(/['"]/g, "");
758
- }
759
- function statGitHeadMtimeMs(cwd) {
760
- try {
761
- return statSync3(join2(cwd, ".git", "HEAD")).mtimeMs;
762
- } catch {
763
- return null;
764
- }
765
- }
766
- function getCurrentBranch(cwd) {
767
- try {
768
- const headMtimeMs = statGitHeadMtimeMs(cwd);
769
- const cached = gitBranchCache.get(cwd);
770
- if (cached && headMtimeMs !== null && cached.headMtimeMs === headMtimeMs) {
771
- return cached.branch || null;
772
- }
773
- const branch = execSync("git rev-parse --abbrev-ref HEAD", {
774
- cwd,
775
- encoding: "utf8",
776
- stdio: ["pipe", "pipe", "pipe"],
777
- timeout: 3000
778
- }).trim();
779
- if (headMtimeMs !== null) {
780
- if (gitBranchCache.size >= GIT_BRANCH_CACHE_MAX_ENTRIES)
781
- gitBranchCache.clear();
782
- gitBranchCache.set(cwd, { branch, headMtimeMs });
783
- }
784
- return branch || null;
785
- } catch {
786
- return null;
787
- }
788
- }
789
- function getHeadSha(cwd) {
790
- try {
791
- const sha = execSync("git rev-parse HEAD", {
792
- cwd,
793
- encoding: "utf8",
794
- stdio: ["pipe", "pipe", "pipe"],
795
- timeout: 3000
796
- }).trim();
797
- return sha || null;
798
- } catch {
799
- return null;
800
- }
801
- }
802
- function getThirdPartyCheckRuns(cwd, sha) {
803
- try {
804
- const json = execFileSync("gh", [
805
- "api",
806
- `repos/{owner}/{repo}/commits/${sha}/check-runs`,
807
- "--jq",
808
- '.check_runs | map(select(.app.slug != "github-actions")) | map({name: .name, status: .status, conclusion: (.conclusion // "")})'
809
- ], {
810
- cwd,
811
- encoding: "utf8",
812
- stdio: ["pipe", "pipe", "pipe"],
813
- timeout: 15000
814
- }).trim();
815
- if (!json || json === "[]")
816
- return [];
817
- return JSON.parse(json);
818
- } catch {
819
- return [];
820
- }
821
- }
822
- function getCommitStatuses(cwd, sha) {
823
- try {
824
- const json = execFileSync("gh", [
825
- "api",
826
- `repos/{owner}/{repo}/commits/${sha}/statuses`,
827
- "--jq",
828
- "map({name: .context, state: .state}) | unique_by(.name)"
829
- ], {
830
- cwd,
831
- encoding: "utf8",
832
- stdio: ["pipe", "pipe", "pipe"],
833
- timeout: 15000
834
- }).trim();
835
- if (!json || json === "[]")
836
- return [];
837
- const statuses = JSON.parse(json);
838
- return statuses.map((s) => ({
839
- name: s.name,
840
- status: s.state === "pending" ? "in_progress" : "completed",
841
- conclusion: s.state === "pending" ? "" : s.state === "success" ? "success" : "failure"
842
- }));
843
- } catch {
844
- return [];
845
- }
846
- }
847
- function matchesAllowedPattern(cmd, pattern) {
848
- const cmdTokens = parseArgvTokens(cmd);
849
- const patTokens = parseArgvTokens(pattern);
850
- if (cmdTokens.length < patTokens.length)
851
- return false;
852
- if (cmdTokens.some((tok) => SHELL_OPERATORS.has(tok)))
853
- return false;
854
- if (cmdTokens.some((tok) => SHELL_METACHAR_RE.test(tok)))
855
- return false;
856
- return patTokens.every((tok, i) => tok === "*" || tok === cmdTokens[i]);
857
- }
858
- function sanitizeJwt(ctx) {
859
- const output = JSON.stringify(ctx.payload);
860
- if (JWT_RE.test(output)) {
861
- return {
862
- decision: "deny",
863
- reason: "JWT token detected in tool output",
864
- message: "[REDACTED: JWT token removed by failproofai]"
865
- };
866
- }
867
- return allow();
868
- }
869
- function sanitizeApiKeys(ctx) {
870
- const output = JSON.stringify(ctx.payload);
871
- for (const [pattern, label] of API_KEY_PATTERNS) {
872
- if (pattern.test(output)) {
873
- return {
874
- decision: "deny",
875
- reason: `${label} detected in tool output`,
876
- message: `[REDACTED: ${label} removed by failproofai]`
877
- };
878
- }
879
- }
880
- const additional = ctx.params?.additionalPatterns ?? [];
881
- for (const { regex, label } of additional) {
882
- try {
883
- if (new RegExp(regex).test(output)) {
884
- return {
885
- decision: "deny",
886
- reason: `${label} detected in tool output`,
887
- message: `[REDACTED: ${label} removed by failproofai]`
888
- };
889
- }
890
- } catch {
891
- hookLogWarn(`additionalPatterns: invalid regex "${regex}", skipping`);
892
- }
893
- }
894
- return allow();
895
- }
896
- function sanitizeConnectionStrings(ctx) {
897
- const output = JSON.stringify(ctx.payload);
898
- if (CONNECTION_STRING_RE.test(output)) {
899
- return {
900
- decision: "deny",
901
- reason: "Database connection string with credentials detected in tool output",
902
- message: "[REDACTED: connection string removed by failproofai]"
903
- };
904
- }
905
- return allow();
906
- }
907
- function sanitizePrivateKeyContent(ctx) {
908
- const output = JSON.stringify(ctx.payload);
909
- if (PRIVATE_KEY_RE.test(output)) {
910
- return {
911
- decision: "deny",
912
- reason: "Private key content detected in tool output",
913
- message: "[REDACTED: private key content removed by failproofai]"
914
- };
915
- }
916
- return allow();
917
- }
918
- function sanitizeBearerTokens(ctx) {
919
- const output = JSON.stringify(ctx.payload);
920
- if (BEARER_TOKEN_RE.test(output)) {
921
- return {
922
- decision: "deny",
923
- reason: "Bearer token detected in tool output",
924
- message: "[REDACTED: Bearer token removed by failproofai]"
925
- };
926
- }
927
- return allow();
928
- }
929
- function warnDestructiveSql(ctx) {
930
- if (ctx.toolName !== "Bash")
931
- return allow();
932
- const cmd = getCommand(ctx);
933
- if (!SQL_TOOL_RE.test(cmd))
934
- return allow();
935
- if (DESTRUCTIVE_SQL_RE.test(cmd)) {
936
- return instruct("STOP: This command contains destructive SQL (DROP/TRUNCATE/DELETE). Confirm with the user before executing.");
937
- }
938
- if (DELETE_NO_WHERE_RE.test(cmd) && !SQL_WHERE_RE.test(cmd)) {
939
- return instruct("STOP: This command contains destructive SQL (DROP/TRUNCATE/DELETE). Confirm with the user before executing.");
940
- }
941
- return allow();
942
- }
943
- function warnLargeFileWrite(ctx) {
944
- if (ctx.toolName !== "Write")
945
- return allow();
946
- const content = ctx.toolInput?.content;
947
- if (typeof content !== "string")
948
- return allow();
949
- const thresholdKb = ctx.params?.thresholdKb ?? 1024;
950
- const thresholdBytes = thresholdKb * 1024;
951
- if (content.length > thresholdBytes) {
952
- return instruct(`STOP: You are writing a file larger than ${thresholdKb}KB (${Math.round(content.length / 1024)}KB). This is unusually large. Confirm this is intentional before proceeding.`);
953
- }
954
- return allow();
955
- }
956
- function warnPackagePublish(ctx) {
957
- if (ctx.toolName !== "Bash")
958
- return allow();
959
- const cmd = getCommand(ctx);
960
- if (PUBLISH_CMD_RE.test(cmd)) {
961
- return instruct("STOP: This command publishes a package to a public registry. Confirm with the user that this is intentional.");
962
- }
963
- return allow();
964
- }
965
- function protectEnvVars(ctx) {
966
- if (ctx.toolName !== "Bash")
967
- return allow();
968
- const cmd = getCommand(ctx);
969
- if (ENV_PRINTENV_RE.test(cmd)) {
970
- return deny("Command reads environment variables");
971
- }
972
- if (ECHO_ENV_RE.test(cmd)) {
973
- return deny("Command echoes environment variable");
974
- }
975
- if (EXPORT_RE.test(cmd)) {
976
- return deny("Command exports environment variable");
977
- }
978
- if (PS_ENV_VAR_RE.test(cmd)) {
979
- return deny("Command reads environment variable via PowerShell");
980
- }
981
- if (PS_CHILDITEM_ENV_RE.test(cmd)) {
982
- return deny("Command reads environment variables via PowerShell");
983
- }
984
- if (DOTNET_GETENV_RE.test(cmd)) {
985
- return deny("Command reads environment variable via .NET");
986
- }
987
- if (CMD_ECHO_ENV_RE.test(cmd)) {
988
- return deny("Command echoes environment variable via cmd");
989
- }
990
- return allow();
991
- }
992
- function blockEnvFiles(ctx) {
993
- const cmd = getCommand(ctx);
994
- const filePath = getFilePath(ctx);
995
- if (filePath && ENV_FILE_PATH_RE.test(filePath)) {
996
- return deny("Access to .env file blocked");
997
- }
998
- if (ctx.toolName === "Bash" && ENV_CMD_RE.test(cmd)) {
999
- return deny("Command references .env file");
1000
- }
1001
- return allow();
1002
- }
1003
- function quoteAwareSegments(command) {
1004
- const segments = [];
1005
- let tokens = [];
1006
- let token = "";
1007
- let quote = null;
1008
- const endToken = () => {
1009
- if (token)
1010
- tokens.push(token);
1011
- token = "";
1012
- };
1013
- const endSegment = () => {
1014
- endToken();
1015
- if (tokens.length)
1016
- segments.push(tokens);
1017
- tokens = [];
1018
- };
1019
- for (let i = 0;i < command.length; i++) {
1020
- const c = command[i];
1021
- if (quote) {
1022
- if (c === "\\" && quote === '"' && i + 1 < command.length) {
1023
- token += command[++i];
1024
- continue;
1025
- }
1026
- if (c === quote) {
1027
- quote = null;
1028
- continue;
1029
- }
1030
- token += c;
1031
- continue;
1032
- }
1033
- if (c === "\\" && i + 1 < command.length) {
1034
- token += command[++i];
1035
- continue;
1036
- }
1037
- if (c === '"' || c === "'") {
1038
- quote = c;
1039
- continue;
1040
- }
1041
- if (/\s/.test(c)) {
1042
- endToken();
1043
- continue;
1044
- }
1045
- if (/[;&|\n\r(){}`]/.test(c)) {
1046
- endSegment();
1047
- continue;
1048
- }
1049
- token += c;
1050
- }
1051
- endSegment();
1052
- return segments;
1053
- }
1054
- function namesElevation(command, depth = 0) {
1055
- for (const tokens of quoteAwareSegments(command)) {
1056
- let i = 0;
1057
- while (i < tokens.length) {
1058
- const token = tokens[i];
1059
- const base = token.slice(token.lastIndexOf("/") + 1);
1060
- if (base === "sudo" || base === "doas")
1061
- return true;
1062
- const isSkippable = ENV_ASSIGNMENT_RE.test(token) || token.startsWith("-") || token.startsWith(">") || token.startsWith("<") || RUNNER_OPERAND_RE.test(token) || COMMAND_PREFIX_TOKENS.has(base);
1063
- if (!isSkippable)
1064
- break;
1065
- if (EVAL_FLAG_RE.test(token) && depth < 3 && i + 1 < tokens.length) {
1066
- if (namesElevation(tokens[i + 1], depth + 1))
1067
- return true;
1068
- }
1069
- i++;
1070
- }
1071
- }
1072
- return false;
1073
- }
1074
- function blockSudo(ctx) {
1075
- if (ctx.toolName !== "Bash")
1076
- return allow();
1077
- const cmd = getCommand(ctx).trimStart();
1078
- if (namesElevation(cmd)) {
1079
- const allowPatterns = ctx.params?.allowPatterns ?? [];
1080
- if (allowPatterns.some((p) => matchesAllowedPattern(cmd, p)))
1081
- return allow();
1082
- return deny("sudo commands are blocked");
1083
- }
1084
- if (PS_ELEVATION_RE.test(cmd)) {
1085
- return deny("Elevated process launch is blocked");
1086
- }
1087
- if (RUNAS_RE.test(cmd)) {
1088
- return deny("runas elevation is blocked");
1089
- }
1090
- return allow();
1091
- }
1092
- function blockSelfPause(ctx) {
1093
- if (ctx.toolName !== "Bash")
1094
- return allow();
1095
- const cmd = getCommand(ctx);
1096
- if (namesSelfPause(cmd) || namesSelfPause(stripShellQuoting(cmd))) {
1097
- return deny("Pausing failproofai enforcement is a human action, not an agent one. " + "If a policy is blocking legitimate work, say so and let the operator decide.");
1098
- }
1099
- return allow();
1100
- }
1101
- function blockCurlPipeSh(ctx) {
1102
- if (ctx.toolName !== "Bash")
1103
- return allow();
1104
- const cmd = getCommand(ctx);
1105
- if (CURL_PIPE_SH_RE.test(cmd)) {
1106
- return deny("Piping downloads to shell is blocked");
1107
- }
1108
- if (PS_WEB_PIPE_RE.test(cmd)) {
1109
- return deny("Piping downloads to Invoke-Expression is blocked");
1110
- }
1111
- return allow();
1112
- }
1113
- function extractGitPushArgs(cmd) {
1114
- return cmd.split(/&&|\|\||[|;\n]/).map((s) => s.trim()).filter((s) => /^git\s+push\s/.test(s)).map((s) => s.replace(/^git\s+push\s+/, ""));
1115
- }
1116
- function blockPushMaster(ctx) {
1117
- if (ctx.toolName !== "Bash")
1118
- return allow();
1119
- const protectedBranches = ctx.params?.protectedBranches ?? ["main", "master"];
1120
- if (protectedBranches.length === 0)
1121
- return allow();
1122
- const args = extractGitPushArgs(getCommand(ctx));
1123
- const branchPattern = new RegExp(`\\b(?:${protectedBranches.map((b) => b.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b`);
1124
- if (args.some((a) => branchPattern.test(a))) {
1125
- return deny(`Pushing to ${protectedBranches.join("/")} is blocked`);
1126
- }
1127
- return allow();
1128
- }
1129
- function expandHomePrefix(path) {
1130
- const m = path.match(/^(?:~|\$HOME|\$\{HOME\})(?=$|\/)/);
1131
- return m ? homedir3() + path.slice(m[0].length) : path;
1132
- }
1133
- function stripTrailingGlob(path) {
1134
- return path.replace(/\/\*$/, "").replace(/\/+$/, "");
1135
- }
1136
- function isCatastrophicTarget(token) {
1137
- const raw = token.replace(/^['"]|['"]$/g, "");
1138
- if (raw === "")
1139
- return false;
1140
- const homePrefix = raw.match(HOME_PREFIX_RE);
1141
- if (!homePrefix && /^[$`]/.test(raw))
1142
- return true;
1143
- const belowRoot = homePrefix ? raw.slice(homePrefix[0].length) : raw.startsWith("/") ? raw : null;
1144
- if (belowRoot === null)
1145
- return false;
1146
- const segments = stripTrailingGlob(belowRoot).split("/").filter(Boolean);
1147
- if (!homePrefix && SCRATCH_ROOTS.some((r) => `/${segments.join("/")}`.startsWith(`${r}/`)))
1148
- return false;
1149
- return segments.length <= CATASTROPHIC_DEPTH;
1150
- }
1151
- function recursiveDeletionTargets(seg) {
1152
- const tokens = parseArgvTokens(seg);
1153
- const findIdx = tokens.findIndex((t) => FIND_CMD_RE.test(t));
1154
- if (findIdx >= 0) {
1155
- const expr = tokens.slice(findIdx + 1);
1156
- const execIdx = expr.findIndex((t) => FIND_EXEC_RE.test(t));
1157
- const deletes = expr.includes("-delete") || execIdx >= 0 && RM_CMD_RE.test(expr[execIdx + 1] ?? "");
1158
- if (deletes) {
1159
- let start = 0;
1160
- while (start < expr.length && FIND_GLOBAL_OPT_RE.test(expr[start])) {
1161
- start += expr[start] === "-D" ? 2 : 1;
1162
- }
1163
- const rest = expr.slice(start);
1164
- const end = rest.findIndex((t) => FIND_EXPR_START_RE.test(t));
1165
- return end < 0 ? rest : rest.slice(0, end);
1166
- }
1167
- }
1168
- const rmIdx = tokens.findIndex((t) => RM_CMD_RE.test(t));
1169
- if (rmIdx >= 0) {
1170
- const args = tokens.slice(rmIdx + 1);
1171
- const shortFlags = args.filter((t) => /^-[^-]/.test(t)).join("");
1172
- const longFlags = args.filter((t) => /^--/.test(t));
1173
- const recursive = /r/i.test(shortFlags) || longFlags.some((f) => /^--recursive$/i.test(f));
1174
- const force = /f/.test(shortFlags) || longFlags.some((f) => /^--force$/i.test(f));
1175
- if (recursive && force)
1176
- return args.filter((t) => !t.startsWith("-"));
1177
- }
1178
- return null;
1179
- }
1180
- function shellSegments(cmd) {
1181
- return cmd.split(/&&|\|\||[|;\n]/).map((s) => s.trim()).filter((s) => s !== "");
1182
- }
1183
- function deletionTargetIsAllowed(cmd, allowPaths) {
1184
- if (allowPaths.length === 0)
1185
- return false;
1186
- const normalizedAllowPaths = allowPaths.map((p) => stripTrailingGlob(expandHomePrefix(p)) || "/");
1187
- let sawRecursiveDelete = false;
1188
- for (const seg of shellSegments(cmd)) {
1189
- const targets = recursiveDeletionTargets(seg);
1190
- if (targets === null)
1191
- continue;
1192
- sawRecursiveDelete = true;
1193
- for (const target of targets) {
1194
- const normalized = stripTrailingGlob(expandHomePrefix(target)) || "/";
1195
- const covered = normalizedAllowPaths.some((np) => normalized === np || normalized.startsWith(np + "/"));
1196
- if (!covered) {
1197
- const segCovered = allowPaths.some((p) => {
1198
- const escaped = p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1199
- return new RegExp(`${escaped}(?:[/"'\\s/*]|$)`).test(seg);
1200
- });
1201
- if (!segCovered)
1202
- return false;
1203
- }
1204
- }
1205
- }
1206
- return sawRecursiveDelete;
1207
- }
1208
- function blockRmRf(ctx) {
1209
- if (ctx.toolName !== "Bash")
1210
- return allow();
1211
- const cmd = getCommand(ctx);
1212
- const hasCatastrophicTarget = shellSegments(cmd).some((seg) => {
1213
- const targets = recursiveDeletionTargets(seg);
1214
- return targets !== null && targets.some(isCatastrophicTarget);
1215
- });
1216
- if (hasCatastrophicTarget) {
1217
- const allowPaths = ctx.params?.allowPaths ?? [];
1218
- if (deletionTargetIsAllowed(cmd, allowPaths))
1219
- return allow();
1220
- return deny("Catastrophic deletion blocked");
1221
- }
1222
- if (/Remove-Item\s+.*-Recurse.*-Force.*(?:[A-Z]:\\(?:\s|$)|\\\*)/i.test(cmd)) {
1223
- return deny("Catastrophic deletion blocked");
1224
- }
1225
- if (/(?:rd|rmdir)\s+\/s\s+\/q\s+[A-Z]:\\/i.test(cmd)) {
1226
- return deny("Catastrophic deletion blocked");
1227
- }
1228
- return allow();
1229
- }
1230
- function blockForcePush(ctx) {
1231
- if (ctx.toolName !== "Bash")
1232
- return allow();
1233
- for (const segment of extractGitPushArgs(getCommand(ctx))) {
1234
- let sawEndOfOptions = false;
1235
- for (const token of segment.split(/\s+/)) {
1236
- if (token === "--") {
1237
- sawEndOfOptions = true;
1238
- continue;
1239
- }
1240
- if (sawEndOfOptions)
1241
- continue;
1242
- if (isForcePushFlag(token)) {
1243
- return deny("Force-pushing is blocked");
1244
- }
1245
- }
1246
- }
1247
- return allow();
1248
- }
1249
- function isForcePushFlag(token) {
1250
- if (token === "--force")
1251
- return true;
1252
- if (SAFE_FORCE_PREFIXES.some((prefix) => token.startsWith(prefix)))
1253
- return false;
1254
- if (token.startsWith("--force"))
1255
- return true;
1256
- return SHORT_FLAG_BUNDLE_RE.test(token);
1257
- }
1258
- function blockSecretsWrite(ctx) {
1259
- if (ctx.toolName !== "Write")
1260
- return allow();
1261
- const filePath = getFilePath(ctx);
1262
- if (SECRET_FILE_RE.test(filePath) || SECRET_FILE_ID_RSA_RE.test(filePath) || SECRET_FILE_CREDENTIALS_RE.test(filePath)) {
1263
- return deny("Writing secret key files is blocked");
1264
- }
1265
- const additionalPatterns = ctx.params?.additionalPatterns ?? [];
1266
- for (const pattern of additionalPatterns) {
1267
- if (filePath.includes(pattern)) {
1268
- return deny(`Writing blocked file pattern: ${pattern}`);
1269
- }
1270
- }
1271
- return allow();
1272
- }
1273
- function extractAbsolutePaths(command) {
1274
- const paths = [];
1275
- const pathRe = /(?<![a-zA-Z0-9_.\-~\\*?:=])(?:~\/[^\s;|&"'()\[\]{}]*|~(?=\s|$|[;|&"'()\[\]{}])|\/[^\s;|&"'()\[\]{}]*)/g;
1276
- function addPaths(s) {
1277
- pathRe.lastIndex = 0;
1278
- let m;
1279
- while ((m = pathRe.exec(s)) !== null) {
1280
- let p = m[0];
1281
- if (p === "~")
1282
- p = homedir3();
1283
- else if (p.startsWith("~/"))
1284
- p = join2(homedir3(), p.slice(2));
1285
- paths.push(p);
1286
- }
1287
- }
1288
- let firstBarePipe = command.length;
1289
- let inDouble = false, inSingle = false;
1290
- for (let i = 0;i < command.length; i++) {
1291
- const c = command[i];
1292
- if (c === '"' && !inSingle)
1293
- inDouble = !inDouble;
1294
- else if (c === "'" && !inDouble)
1295
- inSingle = !inSingle;
1296
- else if (c === "|" && !inDouble && !inSingle) {
1297
- firstBarePipe = i;
1298
- break;
1299
- }
1300
- }
1301
- const firstSegment = command.slice(0, firstBarePipe);
1302
- const quotedRe = /"([^"]*)"|'([^']*)'/g;
1303
- let qm;
1304
- while ((qm = quotedRe.exec(firstSegment)) !== null) {
1305
- const content = qm[1] ?? qm[2] ?? "";
1306
- if (/[*?\[\]^$+()\\]/.test(content))
1307
- continue;
1308
- addPaths(content);
1309
- }
1310
- const stripped = command.replace(/"[^"]*"/g, (m) => " ".repeat(m.length)).replace(/'[^']*'/g, (m) => " ".repeat(m.length));
1311
- addPaths(stripped);
1312
- return paths;
1313
- }
1314
- function blockReadOutsideCwd(ctx) {
1315
- const cwd = process.env.CLAUDE_PROJECT_DIR || ctx.session?.cwd;
1316
- if (!cwd)
1317
- return allow();
1318
- const allowPaths = ctx.params?.allowPaths ?? [];
1319
- if (ctx.toolName === "Bash") {
1320
- const cmd = getCommand(ctx);
1321
- if (!READ_LIKE_CMDS.test(cmd))
1322
- return allow();
1323
- const paths = extractAbsolutePaths(cmd);
1324
- const cwdWithSep2 = cwd.endsWith("/") ? cwd : cwd + "/";
1325
- for (const p of paths) {
1326
- const resolved3 = resolve3(cwd, p);
1327
- if (isClaudeSettingsFile(resolved3)) {
1328
- return deny(`Reading agent settings file blocked: ${resolved3}`);
1329
- }
1330
- if (isClaudeInternalPath(resolved3))
1331
- continue;
1332
- if (resolved3 === "/dev/null")
1333
- continue;
1334
- if (resolved3 !== cwd && !resolved3.startsWith(cwdWithSep2)) {
1335
- if (allowPaths.some((ap) => resolved3 === ap || resolved3.startsWith(ap.endsWith("/") ? ap : ap + "/")))
1336
- continue;
1337
- return deny(`Bash read outside project directory blocked: ${resolved3}`);
1338
- }
1339
- }
1340
- return allow();
1341
- }
1342
- const filePath = getFilePath(ctx);
1343
- const searchPath = ctx.toolInput?.path ?? "";
1344
- const target = filePath || searchPath;
1345
- if (!target)
1346
- return allow();
1347
- const resolved2 = resolve3(cwd, target);
1348
- if (isClaudeSettingsFile(resolved2)) {
1349
- return deny(`Reading agent settings file blocked: ${resolved2}`);
1350
- }
1351
- if (isClaudeInternalPath(resolved2))
1352
- return allow();
1353
- if (resolved2 === "/dev/null")
1354
- return allow();
1355
- const cwdWithSep = cwd.endsWith("/") ? cwd : cwd + "/";
1356
- if (resolved2 !== cwd && !resolved2.startsWith(cwdWithSep)) {
1357
- if (allowPaths.some((ap) => resolved2 === ap || resolved2.startsWith(ap.endsWith("/") ? ap : ap + "/")))
1358
- return allow();
1359
- return deny(`Access outside project directory blocked: ${resolved2}`);
1360
- }
1361
- return allow();
1362
- }
1363
- function blockWorkOnMain(ctx) {
1364
- if (ctx.toolName !== "Bash")
1365
- return allow();
1366
- const cmd = getCommand(ctx);
1367
- const match = cmd.match(GIT_COMMIT_MERGE_RE);
1368
- if (!match)
1369
- return allow();
1370
- const cwd = ctx.session?.cwd;
1371
- if (!cwd)
1372
- return allow();
1373
- const branch = getCurrentBranch(cwd);
1374
- if (!branch)
1375
- return allow();
1376
- const protectedBranches = ctx.params?.protectedBranches ?? ["main", "master"];
1377
- if (protectedBranches.includes(branch)) {
1378
- return deny(`Git ${match[1]} on ${branch} is blocked. Create a feature branch first.`);
1379
- }
1380
- return allow();
1381
- }
1382
- function blockFailproofaiCommands(ctx) {
1383
- if (ctx.toolName !== "Bash")
1384
- return allow();
1385
- const cmd = getCommand(ctx);
1386
- if (FAILPROOFAI_CLI_RE.test(cmd)) {
1387
- return deny("Running failproofai CLI commands is blocked");
1388
- }
1389
- if (FAILPROOFAI_UNINSTALL_RE.test(cmd)) {
1390
- return deny("Uninstalling failproofai is blocked");
1391
- }
1392
- return allow();
1393
- }
1394
- function blockInfraCli(ctx, re, denyMsg) {
1395
- if (ctx.toolName !== "Bash")
1396
- return allow();
1397
- const cmd = getCommand(ctx);
1398
- if (!re.test(cmd))
1399
- return allow();
1400
- const allowPatterns = ctx.params?.allowPatterns ?? [];
1401
- if (allowPatterns.some((p) => matchesAllowedPattern(cmd, p)))
1402
- return allow();
1403
- return deny(denyMsg);
1404
- }
1405
- function blockKubectl(ctx) {
1406
- return blockInfraCli(ctx, KUBECTL_RE, "kubectl commands are blocked");
1407
- }
1408
- function blockTerraform(ctx) {
1409
- return blockInfraCli(ctx, TERRAFORM_RE, "terraform/tofu commands are blocked");
1410
- }
1411
- function blockAwsCli(ctx) {
1412
- return blockInfraCli(ctx, AWS_CLI_RE, "aws CLI commands are blocked");
1413
- }
1414
- function blockGcloud(ctx) {
1415
- return blockInfraCli(ctx, GCLOUD_RE, "gcloud commands are blocked");
1416
- }
1417
- function blockAzCli(ctx) {
1418
- return blockInfraCli(ctx, AZ_CLI_RE, "az (Azure) CLI commands are blocked");
1419
- }
1420
- function blockHelm(ctx) {
1421
- return blockInfraCli(ctx, HELM_RE, "helm commands are blocked");
1422
- }
1423
- function blockGhPipeline(ctx) {
1424
- return blockInfraCli(ctx, GH_PIPELINE_RE, "gh pipeline-trigger commands are blocked");
1425
- }
1426
- async function warnRepeatedToolCalls(ctx) {
1427
- const THRESHOLD = 3;
1428
- const transcriptPath = ctx.session?.transcriptPath;
1429
- if (!transcriptPath || !ctx.toolName || !ctx.toolInput)
1430
- return allow();
1431
- const trackerPath = `${transcriptPath}.tool-calls.json`;
1432
- const fingerprint = JSON.stringify({ tool: ctx.toolName, input: ctx.toolInput });
1433
- let counts = {};
1434
- try {
1435
- const raw = await readFile(trackerPath, "utf8");
1436
- counts = JSON.parse(raw);
1437
- } catch {}
1438
- const prevCount = counts[fingerprint] ?? 0;
1439
- if (prevCount >= THRESHOLD) {
1440
- return instruct(`STOP: You have already called ${ctx.toolName} ${prevCount} times with identical parameters. This is wasteful and unproductive. Do NOT repeat this call — use a different approach or ask the user for clarification.`);
1441
- }
1442
- counts[fingerprint] = prevCount + 1;
1443
- try {
1444
- const serialized = JSON.stringify(counts);
1445
- if (serialized.length <= TOOL_CALL_TRACKER_MAX_BYTES) {
1446
- await writeFile(trackerPath, serialized, "utf8");
1447
- }
1448
- } catch {}
1449
- return allow();
1450
- }
1451
- function warnGitAmend(ctx) {
1452
- if (ctx.toolName !== "Bash")
1453
- return allow();
1454
- const cmd = getCommand(ctx);
1455
- if (GIT_AMEND_RE.test(cmd)) {
1456
- return instruct("STOP: This command amends the last commit, which rewrites git history. If this commit has already been pushed to a shared branch, this will cause divergence for other contributors. Confirm with the user before executing.");
1457
- }
1458
- return allow();
1459
- }
1460
- function warnGitStashDrop(ctx) {
1461
- if (ctx.toolName !== "Bash")
1462
- return allow();
1463
- const cmd = getCommand(ctx);
1464
- if (GIT_STASH_DROP_RE.test(cmd)) {
1465
- return instruct("STOP: This command permanently deletes stashed changes (git stash drop/clear). Stash entries cannot be recovered after deletion. Confirm with the user before executing.");
1466
- }
1467
- return allow();
1468
- }
1469
- function warnAllFilesStaged(ctx) {
1470
- if (ctx.toolName !== "Bash")
1471
- return allow();
1472
- const cmd = getCommand(ctx);
1473
- if (GIT_ADD_ALL_RE.test(cmd)) {
1474
- return instruct("STOP: This command stages all files in the working tree (git add -A / --all / .). This may inadvertently include build artifacts, generated files, or sensitive files not covered by .gitignore. Confirm with the user before executing.");
1475
- }
1476
- return allow();
1477
- }
1478
- function warnSchemaAlteration(ctx) {
1479
- if (ctx.toolName !== "Bash")
1480
- return allow();
1481
- const cmd = getCommand(ctx);
1482
- if (!SQL_TOOL_RE.test(cmd))
1483
- return allow();
1484
- if (SCHEMA_ALTER_RE.test(cmd)) {
1485
- return instruct("STOP: This command contains a schema-altering SQL statement (ALTER TABLE with column or rename operation). Schema changes on production databases are irreversible or disruptive. Confirm with the user before executing.");
1486
- }
1487
- return allow();
1488
- }
1489
- function warnGlobalPackageInstall(ctx) {
1490
- if (ctx.toolName !== "Bash")
1491
- return allow();
1492
- const cmd = getCommand(ctx);
1493
- const isGlobal = NPM_GLOBAL_RE.test(cmd) || YARN_GLOBAL_RE.test(cmd) || PNPM_GLOBAL_RE.test(cmd) || BUN_GLOBAL_RE.test(cmd) || CARGO_INSTALL_RE.test(cmd) || PIP_SYSTEM_RE.test(cmd);
1494
- if (isGlobal) {
1495
- return instruct("STOP: This command installs a package globally, which modifies the system-wide environment outside the project. This can conflict with other projects or system tools. Confirm with the user before executing.");
1496
- }
1497
- return allow();
1498
- }
1499
- function preferPackageManager(ctx) {
1500
- if (ctx.toolName !== "Bash")
1501
- return allow();
1502
- const cmd = getCommand(ctx);
1503
- if (!cmd)
1504
- return allow();
1505
- const allowed = ctx.params?.allowed ?? [];
1506
- if (allowed.length === 0)
1507
- return allow();
1508
- const allowedSet = new Set(allowed.map((a) => a.toLowerCase()));
1509
- const blocked = ctx.params?.blocked ?? [];
1510
- const allowedList = allowed.join(", ");
1511
- const segments = cmd.split(SEGMENT_SPLIT_RE);
1512
- for (const segment of segments) {
1513
- const trimmed = segment.trim();
1514
- if (!trimmed)
1515
- continue;
1516
- let segmentAllowed = false;
1517
- for (const manager of allowedSet) {
1518
- const patterns = PKG_MANAGER_DETECTORS[manager];
1519
- if (!patterns)
1520
- continue;
1521
- for (const pattern of patterns) {
1522
- if (pattern.test(trimmed)) {
1523
- segmentAllowed = true;
1524
- break;
1525
- }
1526
- }
1527
- if (segmentAllowed)
1528
- break;
1529
- }
1530
- if (segmentAllowed)
1531
- continue;
1532
- for (const [manager, patterns] of Object.entries(PKG_MANAGER_DETECTORS)) {
1533
- if (allowedSet.has(manager))
1534
- continue;
1535
- for (const pattern of patterns) {
1536
- if (pattern.test(trimmed)) {
1537
- return deny(`"${manager}" is not an allowed package manager. ` + `Allowed package managers for this project: ${allowedList}. ` + `Rewrite this command using an allowed package manager.`);
1538
- }
1539
- }
1540
- }
1541
- for (const name of blocked) {
1542
- const lower = name.toLowerCase();
1543
- if (allowedSet.has(lower))
1544
- continue;
1545
- const re = new RegExp(`\\b${lower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
1546
- if (re.test(trimmed)) {
1547
- return deny(`"${lower}" is not an allowed package manager. ` + `Allowed package managers for this project: ${allowedList}. ` + `Rewrite this command using an allowed package manager.`);
1548
- }
1549
- }
1550
- }
1551
- return allow();
1552
- }
1553
- function warnBackgroundProcess(ctx) {
1554
- if (ctx.toolName !== "Bash")
1555
- return allow();
1556
- const cmd = getCommand(ctx);
1557
- const isBackground = NOHUP_RE.test(cmd) || SCREEN_DETACH_RE.test(cmd) || TMUX_DETACH_RE.test(cmd) || DISOWN_RE.test(cmd) || BACKGROUND_AMPERSAND_RE.test(cmd);
1558
- if (isBackground) {
1559
- return instruct("STOP: This command starts a background or detached process (nohup, screen -d, tmux -d, or trailing &). Background processes persist after Claude's session and may be difficult to track or stop. Confirm with the user before executing.");
1560
- }
1561
- return allow();
1562
- }
1563
- function isPlanMode(ctx) {
1564
- return ctx.session?.permissionMode === "plan";
1565
- }
1566
- function requireCommitBeforeStop(ctx) {
1567
- if (isPlanMode(ctx))
1568
- return allow("Plan mode — no changes made, skipping commit check.");
1569
- const cwd = ctx.session?.cwd;
1570
- if (!cwd)
1571
- return allow("No working directory available, skipping commit check.");
1572
- try {
1573
- const status = execSync("git status --porcelain", {
1574
- cwd,
1575
- encoding: "utf8",
1576
- stdio: ["pipe", "pipe", "pipe"],
1577
- timeout: 5000
1578
- }).trim();
1579
- if (status.length > 0) {
1580
- return deny("You have uncommitted changes in the working directory. Commit all changes now.");
1581
- }
1582
- return allow("All changes are committed.");
1583
- } catch {
1584
- return allow("Not a git repository, skipping commit check.");
1585
- }
1586
- }
1587
- function requirePushBeforeStop(ctx) {
1588
- if (isPlanMode(ctx))
1589
- return allow("Plan mode — no changes made, skipping push check.");
1590
- const cwd = ctx.session?.cwd;
1591
- if (!cwd)
1592
- return allow("No working directory available, skipping push check.");
1593
- try {
1594
- const remotes = execSync("git remote", {
1595
- cwd,
1596
- encoding: "utf8",
1597
- stdio: ["pipe", "pipe", "pipe"],
1598
- timeout: 3000
1599
- }).trim();
1600
- if (!remotes)
1601
- return allow("No git remote configured, skipping push check.");
1602
- const remote = ctx.params?.remote ?? "origin";
1603
- const branch = getCurrentBranch(cwd);
1604
- if (!branch || branch === "HEAD")
1605
- return allow("Detached HEAD, skipping push check.");
1606
- const baseBranch = ctx.params?.baseBranch ?? "main";
1607
- if (branch === baseBranch) {
1608
- return allow(`On base branch "${baseBranch}", skipping push check.`);
1609
- }
1610
- try {
1611
- const ahead = execFileSync("git", ["log", `${remote}/${baseBranch}..HEAD`, "--oneline"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
1612
- if (!ahead) {
1613
- return allow(`No commits ahead of ${remote}/${baseBranch}, skipping push check.`);
1614
- }
1615
- const diff = execFileSync("git", ["diff", "--stat", `${remote}/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
1616
- if (!diff) {
1617
- return allow(`No file changes compared to ${remote}/${baseBranch}, skipping push check.`);
1618
- }
1619
- } catch {}
1620
- let hasTracking = false;
1621
- try {
1622
- execFileSync("git", ["rev-parse", "--verify", `${remote}/${branch}`], {
1623
- cwd,
1624
- encoding: "utf8",
1625
- stdio: ["pipe", "pipe", "pipe"],
1626
- timeout: 3000
1627
- });
1628
- hasTracking = true;
1629
- } catch {}
1630
- if (!hasTracking) {
1631
- return deny(`Branch "${branch}" has not been pushed to remote "${remote}". ` + `Run now: git push -u ${remote} ${branch}`);
1632
- }
1633
- const unpushed = execFileSync("git", ["log", `${remote}/${branch}..HEAD`, "--oneline"], {
1634
- cwd,
1635
- encoding: "utf8",
1636
- stdio: ["pipe", "pipe", "pipe"],
1637
- timeout: 5000
1638
- }).trim();
1639
- if (unpushed.length > 0) {
1640
- const commitCount = unpushed.split(`
1641
- `).length;
1642
- return deny(`You have ${commitCount} unpushed commit${commitCount > 1 ? "s" : ""} on branch "${branch}". ` + `Run now: git push`);
1643
- }
1644
- return allow(`All commits pushed to "${remote}".`);
1645
- } catch {
1646
- return allow("Could not check push status, skipping.");
1647
- }
1648
- }
1649
- function requirePrBeforeStop(ctx) {
1650
- if (isPlanMode(ctx))
1651
- return allow("Plan mode — no changes made, skipping PR check.");
1652
- const cwd = ctx.session?.cwd;
1653
- if (!cwd)
1654
- return allow("No working directory available, skipping PR check.");
1655
- try {
1656
- try {
1657
- execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
1658
- } catch {
1659
- return allow("GitHub CLI (gh) not installed, skipping PR check.");
1660
- }
1661
- const branch = getCurrentBranch(cwd);
1662
- if (!branch || branch === "HEAD")
1663
- return allow("Detached HEAD, skipping PR check.");
1664
- const baseBranch = ctx.params?.baseBranch ?? "main";
1665
- if (branch === baseBranch) {
1666
- return allow(`On base branch "${baseBranch}", skipping PR check.`);
1667
- }
1668
- try {
1669
- const ahead = execFileSync("git", ["log", `origin/${baseBranch}..HEAD`, "--oneline"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
1670
- if (!ahead) {
1671
- return allow(`No commits ahead of origin/${baseBranch}, skipping PR check.`);
1672
- }
1673
- const diff = execFileSync("git", ["diff", "--stat", `origin/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
1674
- if (!diff) {
1675
- return allow(`No file changes compared to origin/${baseBranch}, skipping PR check.`);
569
+ // src/hooks/policy-catalog.ts
570
+ var POLICY_CATALOG;
571
+ var init_policy_catalog = __esm(() => {
572
+ POLICY_CATALOG = [
573
+ {
574
+ name: "sanitize-jwt",
575
+ description: "Stop Claude from reading JWTs in tool responses",
576
+ displayTitle: "Redacted JWT tokens from tool output",
577
+ impact: "Stops the agent from echoing auth tokens it saw in command output.",
578
+ match: { events: ["PostToolUse"] },
579
+ defaultEnabled: true,
580
+ category: "Sanitize"
581
+ },
582
+ {
583
+ name: "sanitize-api-keys",
584
+ description: "Stop Claude from reading API keys (OpenAI, Anthropic, GitHub, AWS, Stripe, Google) in tool responses",
585
+ displayTitle: "Redacted API keys from tool output",
586
+ impact: "Catches OpenAI / Anthropic / GitHub / AWS / Stripe / Google keys before the model sees them.",
587
+ match: { events: ["PostToolUse"] },
588
+ defaultEnabled: true,
589
+ category: "Sanitize",
590
+ params: {
591
+ additionalPatterns: {
592
+ type: "pattern[]",
593
+ description: "Additional API key patterns to scrub, each with { regex, label }",
594
+ default: []
595
+ }
1676
596
  }
1677
- } catch {}
1678
- let prJson;
1679
- try {
1680
- prJson = execSync("gh pr view --json number,url,state", {
1681
- cwd,
1682
- encoding: "utf8",
1683
- stdio: ["pipe", "pipe", "pipe"],
1684
- timeout: 15000
1685
- }).trim();
1686
- } catch {
1687
- return deny(`No pull request found for branch "${branch}". ` + `Run now: gh pr create`);
1688
- }
1689
- const pr = JSON.parse(prJson);
1690
- if (pr.state === "OPEN") {
1691
- return allow(`PR #${pr.number} exists: ${pr.url}`);
1692
- }
1693
- if (pr.state === "MERGED") {
1694
- return allow(`PR #${pr.number} was merged: ${pr.url}. ` + `Switch off this branch (e.g. 'git checkout ${baseBranch} && git pull') before stopping again.`);
1695
- }
1696
- return deny(`Pull request for branch "${branch}" is ${pr.state.toLowerCase()}. Run now: gh pr create`);
1697
- } catch {
1698
- return allow("Could not check PR status, skipping.");
1699
- }
1700
- }
1701
- function requireNoConflictsBeforeStop(ctx) {
1702
- if (isPlanMode(ctx))
1703
- return allow("Plan mode — no changes made, skipping conflict check.");
1704
- const cwd = ctx.session?.cwd;
1705
- if (!cwd)
1706
- return allow("No working directory available, skipping conflict check.");
1707
- const branch = getCurrentBranch(cwd);
1708
- if (!branch || branch === "HEAD")
1709
- return allow("Detached HEAD, skipping conflict check.");
1710
- const baseBranch = ctx.params?.baseBranch ?? "main";
1711
- if (branch === baseBranch) {
1712
- return allow(`On base branch "${baseBranch}", skipping conflict check.`);
1713
- }
1714
- try {
1715
- execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
1716
- } catch {
1717
- return allow("gh CLI not installed, skipping conflict check.");
1718
- }
1719
- let prJson;
1720
- try {
1721
- prJson = execSync("gh pr view --json mergeable,number,url,state", {
1722
- cwd,
1723
- encoding: "utf8",
1724
- stdio: ["pipe", "pipe", "pipe"],
1725
- timeout: 15000
1726
- }).trim();
1727
- } catch {
1728
- return allow("No pull request found for branch, skipping conflict check.");
1729
- }
1730
- let pr;
1731
- try {
1732
- pr = JSON.parse(prJson);
1733
- } catch {
1734
- return allow("Could not parse gh pr view output, skipping conflict check.");
1735
- }
1736
- if (pr.state !== "OPEN") {
1737
- return allow(`PR #${pr.number} is ${pr.state.toLowerCase()}; skipping conflict check.`);
1738
- }
1739
- try {
1740
- execFileSync("git", ["rev-parse", "--verify", `origin/${baseBranch}`], {
1741
- cwd,
1742
- encoding: "utf8",
1743
- stdio: ["pipe", "pipe", "pipe"],
1744
- timeout: 3000
1745
- });
1746
- const ahead = execFileSync("git", ["log", `origin/${baseBranch}..HEAD`, "--oneline"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
1747
- if (ahead) {
1748
- execFileSync("git", ["merge-tree", "--write-tree", "--name-only", `origin/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 1e4 });
1749
- }
1750
- } catch (err) {
1751
- const e = err;
1752
- if (e.status === 1) {
1753
- const out = (typeof e.stdout === "string" ? e.stdout : e.stdout?.toString("utf8") ?? "").trim();
1754
- const lines = out.split(`
1755
- `);
1756
- const files = [];
1757
- for (let i = 1;i < lines.length; i++) {
1758
- const line = lines[i];
1759
- if (line === "")
1760
- break;
1761
- files.push(line);
597
+ },
598
+ {
599
+ name: "sanitize-connection-strings",
600
+ description: "Stop Claude from reading database connection strings with embedded credentials in tool responses",
601
+ displayTitle: "Redacted database connection strings from tool output",
602
+ impact: "Strips embedded DB credentials before they reach the model context.",
603
+ match: { events: ["PostToolUse"] },
604
+ defaultEnabled: true,
605
+ category: "Sanitize"
606
+ },
607
+ {
608
+ name: "sanitize-private-key-content",
609
+ description: "Stop Claude from reading PEM private key content in tool responses",
610
+ displayTitle: "Redacted PEM private keys from tool output",
611
+ impact: "Prevents private key bodies from being echoed into chat context.",
612
+ match: { events: ["PostToolUse"] },
613
+ defaultEnabled: true,
614
+ category: "Sanitize"
615
+ },
616
+ {
617
+ name: "sanitize-bearer-tokens",
618
+ displayTitle: "Redacted bearer tokens from tool output",
619
+ impact: "Strips Authorization: Bearer values before they hit the model.",
620
+ description: "Stop Claude from reading Authorization Bearer tokens in tool responses",
621
+ match: { events: ["PostToolUse"] },
622
+ defaultEnabled: true,
623
+ category: "Sanitize"
624
+ },
625
+ {
626
+ name: "protect-env-vars",
627
+ displayTitle: "Tried to dump environment variables to chat",
628
+ impact: "Env vars often contain secrets; blocking `env` / `printenv` keeps them out of the model context.",
629
+ description: "Prevent commands that read environment variables",
630
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
631
+ defaultEnabled: true,
632
+ category: "Environment"
633
+ },
634
+ {
635
+ name: "block-env-files",
636
+ displayTitle: "Tried to read or write a .env file",
637
+ impact: "`.env` files routinely contain API keys and DB credentials.",
638
+ description: "Block reading/writing .env files",
639
+ match: { events: ["PreToolUse"] },
640
+ defaultEnabled: true,
641
+ category: "Environment"
642
+ },
643
+ {
644
+ name: "block-read-outside-cwd",
645
+ displayTitle: "Tried to read files outside your project directory",
646
+ impact: "Stops the agent from peeking at neighboring repos or your home directory.",
647
+ description: "Block file reads outside the session working directory",
648
+ match: { events: ["PreToolUse"], toolNames: ["Read", "Glob", "Grep", "Bash"] },
649
+ defaultEnabled: false,
650
+ category: "Environment",
651
+ params: {
652
+ allowPaths: {
653
+ type: "string[]",
654
+ description: "Absolute paths outside cwd that are allowed to be read",
655
+ default: []
656
+ }
1762
657
  }
1763
- const fileList = files.length ? files.join(", ") : "one or more files";
1764
- return deny(`Branch "${branch}" has merge conflicts with ${baseBranch} in: ${fileList}. ` + `Rebase or merge origin/${baseBranch} now and resolve the conflicts.`);
1765
- }
1766
- }
1767
- if (pr.mergeable === "CONFLICTING") {
1768
- return deny(`PR #${pr.number} has merge conflicts per GitHub (${pr.url}). ` + `Rebase or merge origin/${baseBranch} now and resolve the conflicts.`);
1769
- }
1770
- if (pr.mergeable === "UNKNOWN") {
1771
- return deny(`GitHub is still computing mergeability for PR #${pr.number} (${pr.url}). ` + `Wait ~10 seconds, then re-check with \`gh pr view --json mergeable\` before attempting to stop again.`);
1772
- }
1773
- return allow(`PR #${pr.number} merges cleanly per GitHub.`);
1774
- }
1775
- function requireCiGreenBeforeStop(ctx) {
1776
- if (isPlanMode(ctx))
1777
- return allow("Plan mode — no changes made, skipping CI check.");
1778
- const cwd = ctx.session?.cwd;
1779
- if (!cwd)
1780
- return allow("No working directory available, skipping CI check.");
1781
- try {
1782
- try {
1783
- execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
1784
- } catch {
1785
- return allow("GitHub CLI (gh) not installed, skipping CI check.");
1786
- }
1787
- const branch = getCurrentBranch(cwd);
1788
- if (!branch || branch === "HEAD")
1789
- return allow("Detached HEAD, skipping CI check.");
1790
- const sha = getHeadSha(cwd);
1791
- let workflowRuns = [];
1792
- try {
1793
- const runsJson = execFileSync("gh", ["run", "list", "--branch", branch, "--limit", "20", "--json", "status,conclusion,name,headSha"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 }).trim();
1794
- if (runsJson && runsJson !== "[]") {
1795
- const allWorkflowRuns = JSON.parse(runsJson);
1796
- const headRuns = sha ? allWorkflowRuns.filter((r) => r.headSha === sha) : allWorkflowRuns;
1797
- const seen = new Set;
1798
- workflowRuns = headRuns.filter((r) => {
1799
- if (seen.has(r.name))
1800
- return false;
1801
- seen.add(r.name);
1802
- return true;
1803
- });
658
+ },
659
+ {
660
+ name: "block-sudo",
661
+ displayTitle: "Tried to run a command with sudo",
662
+ impact: "Sudo gives the agent root — blocked unless explicitly allow-listed.",
663
+ description: "Block sudo commands",
664
+ match: { events: ["PreToolUse", "PermissionRequest"], toolNames: ["Bash"] },
665
+ defaultEnabled: true,
666
+ category: "Dangerous Commands",
667
+ params: {
668
+ allowPatterns: {
669
+ type: "string[]",
670
+ description: "Sudo command patterns to allow, matched token-by-token (e.g. 'sudo systemctl status')",
671
+ default: []
672
+ }
1804
673
  }
1805
- } catch {}
1806
- let thirdPartyChecks = [];
1807
- let commitStatuses = [];
1808
- if (sha) {
1809
- thirdPartyChecks = getThirdPartyCheckRuns(cwd, sha);
1810
- commitStatuses = getCommitStatuses(cwd, sha);
1811
- }
1812
- const allChecks = [...workflowRuns, ...thirdPartyChecks, ...commitStatuses];
1813
- if (allChecks.length === 0)
1814
- return allow(`No CI runs found for branch "${branch}".`);
1815
- const failing = allChecks.filter((r) => r.status === "completed" && r.conclusion !== "success" && r.conclusion !== "skipped" && r.conclusion !== "cancelled" && r.conclusion !== "neutral");
1816
- if (failing.length > 0) {
1817
- const names = failing.map((r) => `"${r.name}"`).join(", ");
1818
- return deny(`CI checks are failing on branch "${branch}": ${names}. Fix the failing checks now.`);
1819
- }
1820
- const pending = allChecks.filter((r) => r.status === "in_progress" || r.status === "queued" || r.status === "waiting");
1821
- if (pending.length > 0) {
1822
- const names = pending.map((r) => `"${r.name}"`).join(", ");
1823
- return deny(`CI checks are still running on branch "${branch}": ${names}. Wait for all checks to complete, then verify they pass.`);
1824
- }
1825
- return allow(`All CI checks passed on branch "${branch}".`);
1826
- } catch {
1827
- return allow("Could not check CI status, skipping.");
1828
- }
1829
- }
1830
- function registerBuiltinPolicies(enabledNames) {
1831
- const enabledSet = new Set(enabledNames.map(normalizePolicyName));
1832
- for (const policy of BUILTIN_POLICIES) {
1833
- if (enabledSet.has(normalizePolicyName(policy.name))) {
1834
- registerPolicy(policy.name, policy.description, policy.fn, policy.match);
1835
- }
1836
- }
1837
- }
1838
- var isClaudeInternalPath, isClaudeSettingsFile, SHELL_OPERATORS, SHELL_METACHAR_RE, JWT_RE, API_KEY_PATTERNS, CONNECTION_STRING_RE, PRIVATE_KEY_RE, BEARER_TOKEN_RE, SECRET_PATTERNS, 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, PS_ELEVATION_RE, RUNAS_RE, CURL_PIPE_SH_RE, SEGMENT_SEPARATORS, COMMAND_PREFIX_TOKENS, SELF_BINARY_TOKEN_RE, CONFIG_SUBCOMMAND_RE, PAUSE_FLAG_RE, ENV_ASSIGNMENT_RE, RUNNER_OPERAND_RE, PS_WEB_PIPE_RE, SHORT_FLAG_BUNDLE_RE, SAFE_FORCE_PREFIXES, 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, GIT_BRANCH_CACHE_MAX_ENTRIES = 500, EVAL_FLAG_RE, HOME_PREFIX_RE, RM_CMD_RE, FIND_CMD_RE, FIND_EXEC_RE, FIND_GLOBAL_OPT_RE, FIND_EXPR_START_RE, SCRATCH_ROOTS, CATASTROPHIC_DEPTH = 2, READ_LIKE_CMDS, TOOL_CALL_TRACKER_MAX_BYTES = 65536, SEGMENT_SPLIT_RE, BUILTIN_POLICIES;
1839
- var init_builtin_policies = __esm(() => {
1840
- init_hook_logger();
1841
- isClaudeInternalPath = isAgentInternalPath;
1842
- isClaudeSettingsFile = isAgentSettingsFile;
1843
- SHELL_OPERATORS = new Set(["&&", "||", "|", ";"]);
1844
- SHELL_METACHAR_RE = /[;&<>`$()\\]/;
1845
- JWT_RE = /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/;
1846
- API_KEY_PATTERNS = [
1847
- [/sk-ant-[A-Za-z0-9\-_]{20,}/, "Anthropic API key"],
1848
- [/sk-proj-[A-Za-z0-9\-_]{20,}/, "OpenAI project API key"],
1849
- [/sk-[A-Za-z0-9]{20,}/, "OpenAI API key"],
1850
- [/ghp_[A-Za-z0-9]{36}/, "GitHub personal access token"],
1851
- [/github_pat_[A-Za-z0-9_]{82}/, "GitHub fine-grained token"],
1852
- [/AKIA[A-Z0-9]{16}/, "AWS access key ID"],
1853
- [/sk_live_[A-Za-z0-9]{24,}/, "Stripe live secret key"],
1854
- [/sk_test_[A-Za-z0-9]{24,}/, "Stripe test secret key"],
1855
- [/AIza[0-9A-Za-z\-_]{35}/, "Google API key"]
1856
- ];
1857
- CONNECTION_STRING_RE = /(?:postgresql|postgres|mysql|mongodb(?:\+srv)?|redis|amqps?|smtps?):\/\/[^@\s]+@/;
1858
- PRIVATE_KEY_RE = /-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----/;
1859
- BEARER_TOKEN_RE = /Authorization:\s*Bearer\s+[A-Za-z0-9\-._~+/]{20,}/i;
1860
- SECRET_PATTERNS = [
1861
- [PRIVATE_KEY_RE, "private key"],
1862
- [JWT_RE, "JWT"],
1863
- [BEARER_TOKEN_RE, "bearer token"],
1864
- [CONNECTION_STRING_RE, "database credentials"],
1865
- ...API_KEY_PATTERNS
1866
- ];
1867
- SQL_TOOL_RE = /\b(?:psql|mysql|sqlite3|pgcli|clickhouse-client)\b/;
1868
- DESTRUCTIVE_SQL_RE = /\b(?:DROP\s+(?:TABLE|DATABASE|SCHEMA)|TRUNCATE\b)/i;
1869
- DELETE_NO_WHERE_RE = /\bDELETE\s+FROM\b/i;
1870
- SQL_WHERE_RE = /\bWHERE\b/i;
1871
- SCHEMA_ALTER_RE = /\bALTER\s+TABLE\b[\s\S]*\b(?:DROP\s+COLUMN|ADD\s+COLUMN|RENAME\s+(?:COLUMN|TO)|MODIFY\s+COLUMN)\b/i;
1872
- PUBLISH_CMD_RE = /(?:npm\s+publish|bun\s+publish|pnpm\s+publish|yarn\s+npm\s+publish|twine\s+upload|poetry\s+publish|cargo\s+publish|gem\s+push)\b/;
1873
- ENV_PRINTENV_RE = /(?:^|\s|;|&&|\|\|)(?:env|printenv)(?:\s|$|;|&&|\|)/;
1874
- ECHO_ENV_RE = /echo\s+.*\$\{?[A-Za-z_]/;
1875
- EXPORT_RE = /(?:^|\s|;|&&|\|\|)export\s+\w+/;
1876
- PS_ENV_VAR_RE = /\$env:[A-Za-z_]/i;
1877
- PS_CHILDITEM_ENV_RE = /(?:Get-ChildItem|dir|gci|ls)\s+Env:/i;
1878
- DOTNET_GETENV_RE = /\[Environment\]::GetEnvironment/i;
1879
- CMD_ECHO_ENV_RE = /echo\s+%[A-Za-z_]/i;
1880
- ENV_FILE_PATH_RE = /(?:^|[\\/])\.env(?:\.|$)/;
1881
- ENV_CMD_RE = /\.env(?:\b|\s|$|\.)/;
1882
- PS_ELEVATION_RE = /Start-Process\s+.*-Verb\s+RunAs/i;
1883
- RUNAS_RE = /(?:^|;|&&|\|\|)\s*runas\s/i;
1884
- CURL_PIPE_SH_RE = /(?:curl|wget)\s.*\|\s*(?:sh|bash|zsh|dash|ksh|csh|tcsh|fish|ash)\b/;
1885
- SEGMENT_SEPARATORS = /[;&|\n\r(){}`]+/;
1886
- COMMAND_PREFIX_TOKENS = new Set([
1887
- "npx",
1888
- "bunx",
1889
- "pnpx",
1890
- "npm",
1891
- "pnpm",
1892
- "yarn",
1893
- "dlx",
1894
- "exec",
1895
- "run",
1896
- "node",
1897
- "bun",
1898
- "deno",
1899
- "env",
1900
- "command",
1901
- "builtin",
1902
- "nohup",
1903
- "setsid",
1904
- "time",
1905
- "timeout",
1906
- "nice",
1907
- "stdbuf",
1908
- "xargs",
1909
- "sudo",
1910
- "doas",
1911
- "sh",
1912
- "bash",
1913
- "zsh",
1914
- "dash",
1915
- "ksh",
1916
- "fish",
1917
- "ash"
1918
- ]);
1919
- SELF_BINARY_TOKEN_RE = /(?:^|\/)failproofai[^/]*$/;
1920
- CONFIG_SUBCOMMAND_RE = /^(?:config|configure|setup)$/;
1921
- PAUSE_FLAG_RE = /^--pause(?:=|$)/;
1922
- ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
1923
- RUNNER_OPERAND_RE = /^\d+[a-z]*$/i;
1924
- PS_WEB_PIPE_RE = /(?:Invoke-WebRequest|iwr|Invoke-RestMethod|irm)\s+.*\|\s*(?:Invoke-Expression|iex)/i;
1925
- SHORT_FLAG_BUNDLE_RE = /^-[a-zA-Z]*f[a-zA-Z]*$/;
1926
- SAFE_FORCE_PREFIXES = ["--force-with-lease", "--force-if-includes"];
1927
- SECRET_FILE_RE = /\.(?:pem|key)$/;
1928
- SECRET_FILE_ID_RSA_RE = /id_rsa/;
1929
- SECRET_FILE_CREDENTIALS_RE = /credentials/;
1930
- GIT_COMMIT_MERGE_RE = /git\s+(commit|merge|rebase|cherry-pick)\b/;
1931
- FAILPROOFAI_CLI_RE = /(?:^|;|&&|\|\||\|)\s*failproofai(?:\s|$)/;
1932
- FAILPROOFAI_UNINSTALL_RE = /(?:npm\s+(?:uninstall|remove|un|r)\s.*failproofai|bun\s+remove\s.*failproofai|yarn\s+global\s+remove\s+failproofai|pnpm\s+(?:remove|uninstall|un)\s.*failproofai)/;
1933
- GIT_AMEND_RE = /\bgit\s+commit\b.*--amend\b/;
1934
- GIT_STASH_DROP_RE = /\bgit\s+stash\s+(?:drop|clear)\b/;
1935
- GIT_ADD_ALL_RE = /\bgit\s+add\s+(?:-A\b|--all\b|\.(?:\s|$|;|&&|\|\|))/;
1936
- NPM_GLOBAL_RE = /\bnpm\s+(?:install|i)\b(?=.*(?:\s-g\b|--global\b))/;
1937
- YARN_GLOBAL_RE = /\byarn\s+global\s+add\b/;
1938
- PNPM_GLOBAL_RE = /\bpnpm\s+(?:add|install|i)\b(?=.*(?:\s-g\b|--global\b))/;
1939
- BUN_GLOBAL_RE = /\bbun\s+(?:install|add)\b(?=.*(?:\s-g\b|--global\b))/;
1940
- CARGO_INSTALL_RE = /\bcargo\s+install\b/;
1941
- PIP_SYSTEM_RE = /\bpip(?:3)?\s+install\b(?=.*(?:--user\b|--break-system-packages\b))/;
1942
- PKG_MANAGER_DETECTORS = {
1943
- pip: [/\bpip\b/, /\bpip3\b/, /\bpython3?\s+-m\s+pip\b/],
1944
- npm: [/\bnpm\b/, /\bnpx\b/],
1945
- yarn: [/\byarn\b/],
1946
- pnpm: [/\bpnpm\b/, /\bpnpx\b/],
1947
- bun: [/\bbun\b/, /\bbunx\b/],
1948
- uv: [/\buv\b/],
1949
- poetry: [/\bpoetry\b/],
1950
- pipenv: [/\bpipenv\b/],
1951
- conda: [/\bconda\b/],
1952
- cargo: [/\bcargo\b/]
1953
- };
1954
- NOHUP_RE = /\bnohup\s+\S/;
1955
- SCREEN_DETACH_RE = /\bscreen\s+-[A-Za-z]*d[A-Za-z]*\b/;
1956
- TMUX_DETACH_RE = /\btmux\s+(?:new-session|new)\b[^|&;]*-d\b/;
1957
- DISOWN_RE = /\bdisown\b/;
1958
- BACKGROUND_AMPERSAND_RE = /(?<![&|])\s?&\s*(?:$|#|;)/;
1959
- KUBECTL_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*kubectl(?:\s|$)/;
1960
- TERRAFORM_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*(?:terraform|tofu)(?:\s|$)/;
1961
- AWS_CLI_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*aws(?:\s|$)/;
1962
- GCLOUD_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*gcloud(?:\s|$)/;
1963
- AZ_CLI_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*az(?:\s|$)/;
1964
- HELM_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*helm(?:\s|$)/;
1965
- GH_PIPELINE_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*gh\s+(?:workflow\s+(?:run|enable|disable)|run\s+(?:rerun|cancel)|pr\s+merge|release\s+(?:create|delete)|cache\s+delete|secret\s+(?:set|delete))\b/;
1966
- gitBranchCache = new Map;
1967
- EVAL_FLAG_RE = /^-{1,2}c$/;
1968
- HOME_PREFIX_RE = /^(?:~[A-Za-z0-9_.-]*|\$HOME|\$\{HOME\})(?=$|\/)/;
1969
- RM_CMD_RE = /^(?:\/\S*\/)?rm$/;
1970
- FIND_CMD_RE = /^(?:\/\S*\/)?find$/;
1971
- FIND_EXEC_RE = /^-(?:exec|execdir|ok|okdir)$/;
1972
- FIND_GLOBAL_OPT_RE = /^-(?:[HLP]|D|O\d*)$/;
1973
- FIND_EXPR_START_RE = /^(?:-|\\?[(!])/;
1974
- SCRATCH_ROOTS = ["/tmp", "/var/tmp"];
1975
- READ_LIKE_CMDS = /(?:^|;|&&|\|\||\|)\s*(?:ls|find|cat|head|tail|less|more|wc|file|stat|tree|du)\s/;
1976
- SEGMENT_SPLIT_RE = /\s*(?:&&|\|\||\||;)\s*/;
1977
- BUILTIN_POLICIES = [
674
+ },
675
+ {
676
+ name: "block-curl-pipe-sh",
677
+ displayTitle: "Tried to pipe a downloaded script straight to a shell",
678
+ impact: "`curl ... | sh` runs unverified remote code on your machine.",
679
+ description: "Block piping downloads to shell",
680
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
681
+ defaultEnabled: true,
682
+ category: "Dangerous Commands"
683
+ },
684
+ {
685
+ name: "block-rm-rf",
686
+ displayTitle: "Tried to recursively delete a system path",
687
+ impact: "Catches catastrophic `rm -rf /` and Windows equivalents.",
688
+ description: "Prevent catastrophic deletions",
689
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
690
+ defaultEnabled: false,
691
+ category: "Dangerous Commands",
692
+ params: {
693
+ allowPaths: {
694
+ type: "string[]",
695
+ description: "Paths that are allowed to be recursively deleted",
696
+ default: []
697
+ }
698
+ }
699
+ },
1978
700
  {
1979
- name: "sanitize-jwt",
1980
- description: "Stop Claude from reading JWTs in tool responses",
1981
- displayTitle: "Redacted JWT tokens from tool output",
1982
- impact: "Stops the agent from echoing auth tokens it saw in command output.",
1983
- fn: sanitizeJwt,
1984
- match: { events: ["PostToolUse"] },
701
+ name: "block-failproofai-commands",
702
+ displayTitle: "Tried to disable, pause or modify failproofai itself",
703
+ impact: "An agent that can pause or remove enforcement can switch off every other policy.",
704
+ description: "Block failproofai CLI commands, self-pause and uninstallation",
705
+ match: { events: ["PreToolUse", "PermissionRequest"], toolNames: ["Bash"] },
1985
706
  defaultEnabled: true,
1986
- category: "Sanitize"
707
+ alwaysOn: true,
708
+ category: "Dangerous Commands"
1987
709
  },
1988
710
  {
1989
- name: "sanitize-api-keys",
1990
- description: "Stop Claude from reading API keys (OpenAI, Anthropic, GitHub, AWS, Stripe, Google) in tool responses",
1991
- displayTitle: "Redacted API keys from tool output",
1992
- impact: "Catches OpenAI / Anthropic / GitHub / AWS / Stripe / Google keys before the model sees them.",
1993
- fn: sanitizeApiKeys,
1994
- match: { events: ["PostToolUse"] },
1995
- defaultEnabled: true,
1996
- category: "Sanitize",
711
+ name: "block-kubectl",
712
+ displayTitle: "Tried to run a Kubernetes command",
713
+ impact: "kubectl can change live cluster state — gated unless allow-listed.",
714
+ description: "Block kubectl commands (Kubernetes cluster mutations)",
715
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
716
+ defaultEnabled: false,
717
+ category: "Infra Commands",
718
+ params: {
719
+ allowPatterns: {
720
+ type: "string[]",
721
+ description: "kubectl command patterns to allow, matched token-by-token (e.g. 'kubectl get *', 'kubectl describe *')",
722
+ default: []
723
+ }
724
+ }
725
+ },
726
+ {
727
+ name: "block-terraform",
728
+ displayTitle: "Tried to run a Terraform/OpenTofu command",
729
+ impact: "Terraform mutates real infrastructure — gated unless allow-listed.",
730
+ description: "Block terraform and tofu (OpenTofu) commands",
731
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
732
+ defaultEnabled: false,
733
+ category: "Infra Commands",
734
+ params: {
735
+ allowPatterns: {
736
+ type: "string[]",
737
+ description: "terraform/tofu command patterns to allow (e.g. 'terraform plan', 'terraform validate')",
738
+ default: []
739
+ }
740
+ }
741
+ },
742
+ {
743
+ name: "block-aws-cli",
744
+ displayTitle: "Tried to run an AWS CLI command",
745
+ impact: "AWS CLI can spend money or break prod — gated.",
746
+ description: "Block aws CLI commands",
747
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
748
+ defaultEnabled: false,
749
+ category: "Infra Commands",
750
+ params: {
751
+ allowPatterns: {
752
+ type: "string[]",
753
+ description: "aws CLI command patterns to allow (e.g. 'aws s3 ls *', 'aws sts get-caller-identity')",
754
+ default: []
755
+ }
756
+ }
757
+ },
758
+ {
759
+ name: "block-gcloud",
760
+ displayTitle: "Tried to run a Google Cloud command",
761
+ impact: "gcloud can spend money or break prod — gated.",
762
+ description: "Block gcloud (Google Cloud) CLI commands",
763
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
764
+ defaultEnabled: false,
765
+ category: "Infra Commands",
766
+ params: {
767
+ allowPatterns: {
768
+ type: "string[]",
769
+ description: "gcloud command patterns to allow (e.g. 'gcloud auth list', 'gcloud config list')",
770
+ default: []
771
+ }
772
+ }
773
+ },
774
+ {
775
+ name: "block-az-cli",
776
+ displayTitle: "Tried to run an Azure CLI command",
777
+ impact: "az can spend money or break prod — gated.",
778
+ description: "Block az (Azure) CLI commands",
779
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
780
+ defaultEnabled: false,
781
+ category: "Infra Commands",
782
+ params: {
783
+ allowPatterns: {
784
+ type: "string[]",
785
+ description: "az CLI command patterns to allow (e.g. 'az account show', 'az group list')",
786
+ default: []
787
+ }
788
+ }
789
+ },
790
+ {
791
+ name: "block-helm",
792
+ displayTitle: "Tried to run a Helm command",
793
+ impact: "Helm releases mutate cluster state — gated.",
794
+ description: "Block helm commands",
795
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
796
+ defaultEnabled: false,
797
+ category: "Infra Commands",
798
+ params: {
799
+ allowPatterns: {
800
+ type: "string[]",
801
+ description: "helm command patterns to allow (e.g. 'helm list', 'helm status *')",
802
+ default: []
803
+ }
804
+ }
805
+ },
806
+ {
807
+ name: "block-gh-pipeline",
808
+ displayTitle: "Tried to run a privileged GitHub CLI pipeline command",
809
+ impact: "Catches `gh workflow run`, `gh pr merge`, `gh secret set`, etc.",
810
+ description: "Block gh CLI pipeline-trigger subcommands (workflow run, run rerun/cancel, pr merge, release create/delete, cache delete, secret set/delete)",
811
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
812
+ defaultEnabled: false,
813
+ category: "Infra Commands",
814
+ params: {
815
+ allowPatterns: {
816
+ type: "string[]",
817
+ description: "gh pipeline command patterns to allow (e.g. specific scripted invocations); read-only gh subcommands like 'gh pr view' and 'gh run list' are not matched by this policy",
818
+ default: []
819
+ }
820
+ }
821
+ },
822
+ {
823
+ name: "block-secrets-write",
824
+ displayTitle: "Tried to write a secret-key file",
825
+ impact: "Stops the agent from creating `.pem`, `id_rsa`, `credentials.json`, etc.",
826
+ description: "Block writing secret key files",
827
+ match: { events: ["PreToolUse"], toolNames: ["Write"] },
828
+ defaultEnabled: false,
829
+ category: "Dangerous Commands",
1997
830
  params: {
1998
831
  additionalPatterns: {
1999
- type: "pattern[]",
2000
- description: "Additional API key patterns to scrub, each with { regex, label }",
832
+ type: "string[]",
833
+ description: "Additional filename patterns (substrings) to block",
2001
834
  default: []
2002
835
  }
2003
836
  }
2004
837
  },
2005
838
  {
2006
- name: "sanitize-connection-strings",
2007
- description: "Stop Claude from reading database connection strings with embedded credentials in tool responses",
2008
- displayTitle: "Redacted database connection strings from tool output",
2009
- impact: "Strips embedded DB credentials before they reach the model context.",
2010
- fn: sanitizeConnectionStrings,
2011
- match: { events: ["PostToolUse"] },
839
+ name: "block-push-master",
840
+ displayTitle: "Tried to push directly to main/master",
841
+ impact: "Direct pushes to a protected branch bypass review.",
842
+ description: "Block pushing to main/master",
843
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
2012
844
  defaultEnabled: true,
2013
- category: "Sanitize"
845
+ category: "Git",
846
+ params: {
847
+ protectedBranches: {
848
+ type: "string[]",
849
+ description: "Branch names to protect from direct pushes",
850
+ default: ["main", "master"]
851
+ }
852
+ }
2014
853
  },
2015
854
  {
2016
- name: "sanitize-private-key-content",
2017
- description: "Stop Claude from reading PEM private key content in tool responses",
2018
- displayTitle: "Redacted PEM private keys from tool output",
2019
- impact: "Prevents private key bodies from being echoed into chat context.",
2020
- fn: sanitizePrivateKeyContent,
2021
- match: { events: ["PostToolUse"] },
2022
- defaultEnabled: true,
2023
- category: "Sanitize"
855
+ name: "block-force-push",
856
+ displayTitle: "Tried to force-push",
857
+ impact: "Force-pushes rewrite history and can clobber teammates' work.",
858
+ description: "Prevent force-pushing to any branch",
859
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
860
+ defaultEnabled: false,
861
+ category: "Git"
2024
862
  },
2025
863
  {
2026
- name: "sanitize-bearer-tokens",
2027
- displayTitle: "Redacted bearer tokens from tool output",
2028
- impact: "Strips Authorization: Bearer values before they hit the model.",
2029
- description: "Stop Claude from reading Authorization Bearer tokens in tool responses",
2030
- fn: sanitizeBearerTokens,
2031
- match: { events: ["PostToolUse"] },
2032
- defaultEnabled: true,
2033
- category: "Sanitize"
864
+ name: "block-work-on-main",
865
+ displayTitle: "Tried to commit or merge on main/master",
866
+ impact: "Work should land via PR — direct commits skip review.",
867
+ description: "Block git commits and merges on main/master branch",
868
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
869
+ defaultEnabled: false,
870
+ category: "Git",
871
+ params: {
872
+ protectedBranches: {
873
+ type: "string[]",
874
+ description: "Branch names where commits/merges are blocked",
875
+ default: ["main", "master"]
876
+ }
877
+ }
2034
878
  },
2035
879
  {
2036
- name: "protect-env-vars",
2037
- displayTitle: "Tried to dump environment variables to chat",
2038
- impact: "Env vars often contain secrets; blocking `env` / `printenv` keeps them out of the model context.",
2039
- description: "Prevent commands that read environment variables",
2040
- fn: protectEnvVars,
880
+ name: "warn-git-amend",
881
+ displayTitle: "Used git commit --amend",
882
+ impact: "Amending after a push rewrites history that others may have pulled.",
883
+ description: "Warns before amending git commits, which rewrites history",
2041
884
  match: { events: ["PreToolUse"], toolNames: ["Bash"] },
2042
- defaultEnabled: true,
2043
- category: "Environment"
885
+ defaultEnabled: false,
886
+ category: "Git"
887
+ },
888
+ {
889
+ name: "warn-git-stash-drop",
890
+ displayTitle: "Tried to drop or clear git stash",
891
+ impact: "Stash deletions are permanent and silent.",
892
+ description: "Warns before permanently deleting stashed changes",
893
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
894
+ defaultEnabled: false,
895
+ category: "Git"
896
+ },
897
+ {
898
+ name: "warn-all-files-staged",
899
+ displayTitle: "Staged all files with git add -A / .",
900
+ impact: "Wide stages routinely catch generated files or secrets you didn't intend to commit.",
901
+ description: "Warns before staging all working tree files with git add -A / . / --all",
902
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
903
+ defaultEnabled: false,
904
+ category: "Git"
905
+ },
906
+ {
907
+ name: "warn-destructive-sql",
908
+ displayTitle: "Ran destructive SQL (DROP / TRUNCATE / DELETE without WHERE)",
909
+ impact: "Easy way to wipe a table by accident.",
910
+ description: "Warn before executing destructive SQL (DROP/TRUNCATE/DELETE without WHERE) via database clients",
911
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
912
+ defaultEnabled: false,
913
+ category: "Database"
914
+ },
915
+ {
916
+ name: "warn-schema-alteration",
917
+ displayTitle: "Altered a database schema column",
918
+ impact: "ALTER TABLE operations can lock tables and break readers.",
919
+ description: "Warns before SQL schema changes (ALTER TABLE with column or rename operations)",
920
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
921
+ defaultEnabled: false,
922
+ category: "Database"
923
+ },
924
+ {
925
+ name: "warn-package-publish",
926
+ displayTitle: "Tried to publish a package",
927
+ impact: "Publishes are irreversible — `npm publish` / `cargo publish` shouldn't happen without intent.",
928
+ description: "Warn before publishing packages to public registries (npm, PyPI, crates.io, RubyGems, etc.)",
929
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
930
+ defaultEnabled: false,
931
+ category: "Packages & System"
2044
932
  },
2045
933
  {
2046
- name: "block-env-files",
2047
- displayTitle: "Tried to read or write a .env file",
2048
- impact: "`.env` files routinely contain API keys and DB credentials.",
2049
- description: "Block reading/writing .env files",
2050
- fn: blockEnvFiles,
2051
- match: { events: ["PreToolUse"] },
2052
- defaultEnabled: true,
2053
- category: "Environment"
934
+ name: "warn-global-package-install",
935
+ displayTitle: "Installed a package globally",
936
+ impact: "`npm i -g`, `cargo install`, `pip --user` pollute your machine outside the project.",
937
+ description: "Warns before installing packages globally (npm -g, cargo install, etc.)",
938
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
939
+ defaultEnabled: false,
940
+ category: "Packages & System"
2054
941
  },
2055
942
  {
2056
- name: "block-read-outside-cwd",
2057
- displayTitle: "Tried to read files outside your project directory",
2058
- impact: "Stops the agent from peeking at neighboring repos or your home directory.",
2059
- description: "Block file reads outside the session working directory",
2060
- fn: blockReadOutsideCwd,
2061
- match: { events: ["PreToolUse"], toolNames: ["Read", "Glob", "Grep", "Bash"] },
943
+ name: "prefer-package-manager",
944
+ displayTitle: "Used a non-preferred package manager",
945
+ impact: "Mixing package managers creates lockfile churn for your team.",
946
+ description: "Blocks non-preferred package managers and tells Claude to use an allowed one (e.g., uv instead of pip)",
947
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
2062
948
  defaultEnabled: false,
2063
- category: "Environment",
949
+ category: "Packages & System",
2064
950
  params: {
2065
- allowPaths: {
951
+ allowed: {
2066
952
  type: "string[]",
2067
- description: "Absolute paths outside cwd that are allowed to be read",
953
+ description: "Allowed package manager names (e.g. ['uv', 'bun']). Any detected manager not in this list is blocked.",
954
+ default: []
955
+ },
956
+ blocked: {
957
+ type: "string[]",
958
+ description: "Additional manager names to block beyond the built-in list (e.g. ['pdm', 'pipx']).",
2068
959
  default: []
2069
960
  }
2070
961
  }
2071
962
  },
2072
963
  {
2073
- name: "block-self-pause",
2074
- displayTitle: "Tried to pause failproofai enforcement",
2075
- impact: "An agent that can pause enforcement can switch off every other policy.",
2076
- description: "Block agents from pausing failproofai enforcement",
2077
- fn: blockSelfPause,
2078
- match: { events: ["PreToolUse", "PermissionRequest"], toolNames: ["Bash"] },
2079
- defaultEnabled: true,
2080
- category: "Dangerous Commands"
2081
- },
2082
- {
2083
- name: "block-sudo",
2084
- displayTitle: "Tried to run a command with sudo",
2085
- impact: "Sudo gives the agent root — blocked unless explicitly allow-listed.",
2086
- description: "Block sudo commands",
2087
- fn: blockSudo,
2088
- match: { events: ["PreToolUse", "PermissionRequest"], toolNames: ["Bash"] },
2089
- defaultEnabled: true,
2090
- category: "Dangerous Commands",
964
+ name: "warn-large-file-write",
965
+ displayTitle: "Wrote a file larger than the configured threshold",
966
+ impact: "Catches accidentally large file writes (logs, binaries, model dumps).",
967
+ description: "Warn before writing files larger than 1MB (configurable via thresholdKb param)",
968
+ match: { events: ["PreToolUse"], toolNames: ["Write"] },
969
+ defaultEnabled: false,
970
+ category: "Packages & System",
2091
971
  params: {
2092
- allowPatterns: {
2093
- type: "string[]",
2094
- description: "Sudo command patterns to allow, matched token-by-token (e.g. 'sudo systemctl status')",
2095
- default: []
972
+ thresholdKb: {
973
+ type: "number",
974
+ description: "File size threshold in KB above which a warning is issued",
975
+ default: 1024
2096
976
  }
2097
977
  }
2098
978
  },
2099
979
  {
2100
- name: "block-curl-pipe-sh",
2101
- displayTitle: "Tried to pipe a downloaded script straight to a shell",
2102
- impact: "`curl ... | sh` runs unverified remote code on your machine.",
2103
- description: "Block piping downloads to shell",
2104
- fn: blockCurlPipeSh,
980
+ name: "warn-background-process",
981
+ displayTitle: "Started a long-lived background process",
982
+ impact: "Catches `nohup` / `&` / `screen` / `tmux` / `disown` patterns that the agent often forgets to clean up.",
983
+ description: "Warns before starting detached or background processes",
2105
984
  match: { events: ["PreToolUse"], toolNames: ["Bash"] },
2106
- defaultEnabled: true,
2107
- category: "Dangerous Commands"
985
+ defaultEnabled: false,
986
+ category: "Packages & System"
2108
987
  },
2109
988
  {
2110
- name: "block-rm-rf",
2111
- displayTitle: "Tried to recursively delete a system path",
2112
- impact: "Catches catastrophic `rm -rf /` and Windows equivalents.",
2113
- description: "Prevent catastrophic deletions",
2114
- fn: blockRmRf,
2115
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
989
+ name: "warn-repeated-tool-calls",
990
+ displayTitle: "Called the same tool 3+ times with identical arguments",
991
+ impact: "Usually a sign of a stuck loop burning tokens.",
992
+ description: "Warn when the same tool is called 3+ times with identical parameters",
993
+ match: { events: ["PreToolUse"] },
2116
994
  defaultEnabled: false,
2117
- category: "Dangerous Commands",
995
+ category: "AI Behavior"
996
+ },
997
+ {
998
+ name: "require-commit-before-stop",
999
+ displayTitle: "Stopped with uncommitted changes",
1000
+ impact: "Work not in a commit is invisible to teammates and easy to lose.",
1001
+ description: "Require all changes to be committed before Claude stops",
1002
+ match: { events: ["Stop"] },
1003
+ defaultEnabled: false,
1004
+ category: "Workflow"
1005
+ },
1006
+ {
1007
+ name: "require-push-before-stop",
1008
+ displayTitle: "Stopped with unpushed commits",
1009
+ impact: "Local-only commits won't trigger CI or be reviewable.",
1010
+ description: "Require all commits to be pushed to remote before Claude stops",
1011
+ match: { events: ["Stop"] },
1012
+ defaultEnabled: false,
1013
+ category: "Workflow",
2118
1014
  params: {
2119
- allowPaths: {
2120
- type: "string[]",
2121
- description: "Paths that are allowed to be recursively deleted",
2122
- default: []
1015
+ remote: {
1016
+ type: "string",
1017
+ description: "Remote name to push to (default: origin)",
1018
+ default: "origin"
1019
+ },
1020
+ baseBranch: {
1021
+ type: "string",
1022
+ description: "Base branch to compare against (default: main)",
1023
+ default: "main"
2123
1024
  }
2124
1025
  }
2125
1026
  },
2126
1027
  {
2127
- name: "block-failproofai-commands",
2128
- displayTitle: "Tried to disable or modify failproofai itself",
2129
- impact: "Prevents the agent from turning off the policies that protect you.",
2130
- description: "Block failproofai CLI commands and uninstallation",
2131
- fn: blockFailproofaiCommands,
2132
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
2133
- defaultEnabled: true,
2134
- category: "Dangerous Commands"
2135
- },
2136
- {
2137
- name: "block-kubectl",
2138
- displayTitle: "Tried to run a Kubernetes command",
2139
- impact: "kubectl can change live cluster state — gated unless allow-listed.",
2140
- description: "Block kubectl commands (Kubernetes cluster mutations)",
2141
- fn: blockKubectl,
2142
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1028
+ name: "require-pr-before-stop",
1029
+ displayTitle: "Stopped without a PR for the branch",
1030
+ impact: "Branches without PRs don't get reviewed.",
1031
+ description: "Require a pull request to exist for the current branch before Claude stops",
1032
+ match: { events: ["Stop"] },
2143
1033
  defaultEnabled: false,
2144
- category: "Infra Commands",
1034
+ category: "Workflow",
2145
1035
  params: {
2146
- allowPatterns: {
2147
- type: "string[]",
2148
- description: "kubectl command patterns to allow, matched token-by-token (e.g. 'kubectl get *', 'kubectl describe *')",
2149
- default: []
1036
+ baseBranch: {
1037
+ type: "string",
1038
+ description: "Base branch to compare against (default: main)",
1039
+ default: "main"
2150
1040
  }
2151
1041
  }
2152
1042
  },
2153
1043
  {
2154
- name: "block-terraform",
2155
- displayTitle: "Tried to run a Terraform/OpenTofu command",
2156
- impact: "Terraform mutates real infrastructure — gated unless allow-listed.",
2157
- description: "Block terraform and tofu (OpenTofu) commands",
2158
- fn: blockTerraform,
2159
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1044
+ name: "require-no-conflicts-before-stop",
1045
+ displayTitle: "Stopped with a branch that conflicts with main",
1046
+ impact: "Conflicting branches can't merge — surface them early.",
1047
+ description: "Require the current branch to merge cleanly with the base branch before Claude stops",
1048
+ match: { events: ["Stop"] },
2160
1049
  defaultEnabled: false,
2161
- category: "Infra Commands",
1050
+ category: "Workflow",
2162
1051
  params: {
2163
- allowPatterns: {
2164
- type: "string[]",
2165
- description: "terraform/tofu command patterns to allow (e.g. 'terraform plan', 'terraform validate')",
2166
- default: []
1052
+ baseBranch: {
1053
+ type: "string",
1054
+ description: "Base branch to check for conflicts against (default: main)",
1055
+ default: "main"
2167
1056
  }
2168
1057
  }
2169
- },
2170
- {
2171
- name: "block-aws-cli",
2172
- displayTitle: "Tried to run an AWS CLI command",
2173
- impact: "AWS CLI can spend money or break prod — gated.",
2174
- description: "Block aws CLI commands",
2175
- fn: blockAwsCli,
2176
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
2177
- defaultEnabled: false,
2178
- category: "Infra Commands",
2179
- params: {
2180
- allowPatterns: {
2181
- type: "string[]",
2182
- description: "aws CLI command patterns to allow (e.g. 'aws s3 ls *', 'aws sts get-caller-identity')",
2183
- default: []
2184
- }
1058
+ },
1059
+ {
1060
+ name: "require-ci-green-before-stop",
1061
+ displayTitle: "Stopped with failing CI",
1062
+ impact: "Failing CI blocks deploy.",
1063
+ description: "Require CI checks to pass on the current HEAD commit before Claude stops (ignores stale runs on prior commits)",
1064
+ match: { events: ["Stop"] },
1065
+ defaultEnabled: false,
1066
+ category: "Workflow"
1067
+ }
1068
+ ];
1069
+ });
1070
+
1071
+ // src/hooks/policy-helpers.ts
1072
+ function allow(reason) {
1073
+ return reason ? { decision: "allow", reason } : { decision: "allow" };
1074
+ }
1075
+ function deny(reason) {
1076
+ return { decision: "deny", reason };
1077
+ }
1078
+ function instruct(reason) {
1079
+ return { decision: "instruct", reason };
1080
+ }
1081
+
1082
+ // src/hooks/policy-registry.ts
1083
+ function normalizePolicyName(name) {
1084
+ return name.includes("/") ? name : `${DEFAULT_POLICY_NAMESPACE}/${name}`;
1085
+ }
1086
+ function getIndexCache() {
1087
+ return globalThis[INDEX_CACHE_KEY];
1088
+ }
1089
+ function setIndexCache(cache) {
1090
+ globalThis[INDEX_CACHE_KEY] = cache;
1091
+ }
1092
+ function getRegistry() {
1093
+ const g = globalThis;
1094
+ if (!g[REGISTRY_KEY]) {
1095
+ g[REGISTRY_KEY] = [];
1096
+ }
1097
+ return g[REGISTRY_KEY];
1098
+ }
1099
+ function registerPolicy(name, description, fn, match, priority = 0, params) {
1100
+ const canonical = normalizePolicyName(name);
1101
+ const registry = getRegistry();
1102
+ const idx = registry.findIndex((p) => p.name === canonical);
1103
+ const entry = {
1104
+ name: canonical,
1105
+ description,
1106
+ fn,
1107
+ match,
1108
+ priority,
1109
+ ...params ? { params } : {}
1110
+ };
1111
+ if (idx >= 0) {
1112
+ registry[idx] = entry;
1113
+ } else {
1114
+ registry.push(entry);
1115
+ }
1116
+ setIndexCache(null);
1117
+ }
1118
+ function getPoliciesForEvent(eventType, toolName) {
1119
+ let cache = getIndexCache();
1120
+ if (!cache) {
1121
+ cache = new Map;
1122
+ setIndexCache(cache);
1123
+ }
1124
+ const key = `${eventType}:${toolName ?? ""}`;
1125
+ const cached = cache.get(key);
1126
+ if (cached)
1127
+ return cached;
1128
+ const result = getRegistry().filter((p) => {
1129
+ if (p.match.events && p.match.events.length > 0) {
1130
+ if (!p.match.events.includes(eventType))
1131
+ return false;
1132
+ }
1133
+ if (p.match.toolNames && p.match.toolNames.length > 0) {
1134
+ if (!toolName || !p.match.toolNames.includes(toolName))
1135
+ return false;
1136
+ }
1137
+ return true;
1138
+ }).sort((a, b) => b.priority - a.priority);
1139
+ cache.set(key, result);
1140
+ return result;
1141
+ }
1142
+ function clearPolicies() {
1143
+ const g = globalThis;
1144
+ g[REGISTRY_KEY] = [];
1145
+ setIndexCache(null);
1146
+ }
1147
+ var REGISTRY_KEY = "__FAILPROOFAI_POLICY_REGISTRY__", INDEX_CACHE_KEY = "__FAILPROOFAI_POLICY_INDEX_CACHE__", DEFAULT_POLICY_NAMESPACE = "failproofai";
1148
+
1149
+ // src/hooks/builtin-policies.ts
1150
+ import { resolve as resolve3, join as join2 } from "node:path";
1151
+ import { statSync as statSync3 } from "node:fs";
1152
+ import { readFile, writeFile } from "node:fs/promises";
1153
+ import { execSync, execFileSync } from "node:child_process";
1154
+ import { homedir as homedir3 } from "node:os";
1155
+ function isAgentInternalPath(resolved2) {
1156
+ const normResolved = resolved2.replaceAll("\\", "/");
1157
+ for (const dir of [".claude", ".codex", ".copilot", ".cursor", ".opencode", ".pi", ".gemini"]) {
1158
+ const root = join2(homedir3(), dir).replaceAll("\\", "/");
1159
+ if (normResolved === root || normResolved.startsWith(root + "/"))
1160
+ return true;
1161
+ }
1162
+ for (const sub of [join2(".config", "opencode"), join2(".local", "share", "opencode")]) {
1163
+ const root = join2(homedir3(), sub).replaceAll("\\", "/");
1164
+ if (normResolved === root || normResolved.startsWith(root + "/"))
1165
+ return true;
1166
+ }
1167
+ return false;
1168
+ }
1169
+ function isAgentSettingsFile(resolved2) {
1170
+ if (/[\\/]\.claude[\\/]settings(?:\.[^/\\]+)?\.json$/.test(resolved2))
1171
+ return true;
1172
+ if (/[\\/]\.codex[\\/]hooks\.json$/.test(resolved2))
1173
+ return true;
1174
+ if (/[\\/]\.copilot[\\/]hooks[\\/][^/\\]+\.json$/.test(resolved2))
1175
+ return true;
1176
+ if (/[\\/]\.github[\\/]hooks[\\/][^/\\]+\.json$/.test(resolved2))
1177
+ return true;
1178
+ if (/[\\/]\.cursor[\\/]hooks\.json$/.test(resolved2))
1179
+ return true;
1180
+ if (/[\\/]\.opencode[\\/]opencode\.jsonc?$/.test(resolved2))
1181
+ return true;
1182
+ if (/[\\/]\.opencode[\\/]plugins[\\/][^/\\]+\.(?:mjs|js|ts)$/.test(resolved2))
1183
+ return true;
1184
+ if (/[\\/]\.config[\\/]opencode[\\/]opencode\.jsonc?$/.test(resolved2))
1185
+ return true;
1186
+ if (/[\\/]\.config[\\/]opencode[\\/]config\.json$/.test(resolved2))
1187
+ return true;
1188
+ if (/[\\/]\.config[\\/]opencode[\\/]plugins[\\/][^/\\]+\.(?:mjs|js|ts)$/.test(resolved2))
1189
+ return true;
1190
+ if (/[\\/]\.pi[\\/](?:agent[\\/])?settings\.json$/.test(resolved2))
1191
+ return true;
1192
+ if (/[\\/]\.pi[\\/](?:agent[\\/])?extensions[\\/]/.test(resolved2))
1193
+ return true;
1194
+ if (/[\\/]\.gemini[\\/]settings\.json$/.test(resolved2))
1195
+ return true;
1196
+ if (/[\\/]\.gemini[\\/]config[\\/]hooks\.json$/.test(resolved2))
1197
+ return true;
1198
+ return false;
1199
+ }
1200
+ function getCommand(ctx) {
1201
+ return ctx.toolInput?.command ?? "";
1202
+ }
1203
+ function getFilePath(ctx) {
1204
+ return ctx.toolInput?.file_path ?? "";
1205
+ }
1206
+ function parseArgvTokens(cmd) {
1207
+ return cmd.trim().split(/\s+/).map((t) => t.replace(/^['"]|['"]$/g, ""));
1208
+ }
1209
+ function classifySelfInvocation(raw) {
1210
+ const command = raw.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, "$$$1");
1211
+ let found = null;
1212
+ const selfVars = new Set;
1213
+ for (const [, name, value] of command.matchAll(/(?:^|[\s;&|])([A-Za-z_][A-Za-z0-9_]*)=("[^"]*"|'[^']*'|[^\s;&|]+)/g)) {
1214
+ const bare = value.replace(/^["']|["']$/g, "");
1215
+ if (SELF_BINARY_TOKEN_RE.test(bare) || SELF_ENTRY_PATH_RE.test(bare))
1216
+ selfVars.add(name);
1217
+ }
1218
+ const isSelfVarRef = (token) => {
1219
+ const m = /^\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?$/.exec(token);
1220
+ return m ? selfVars.has(m[1]) : false;
1221
+ };
1222
+ for (const segment of command.split(SEGMENT_SEPARATORS)) {
1223
+ const tokens = segment.split(/\s+/).filter(Boolean);
1224
+ let i = 0;
1225
+ while (i < tokens.length) {
1226
+ const token = tokens[i];
1227
+ const isSkippable = ENV_ASSIGNMENT_RE.test(token) || token.startsWith("-") || token.startsWith(">") || token.startsWith("<") || RUNNER_OPERAND_RE.test(token) || COMMAND_PREFIX_TOKENS.has(token.slice(token.lastIndexOf("/") + 1));
1228
+ if (!isSkippable)
1229
+ break;
1230
+ i++;
1231
+ }
1232
+ if (i >= tokens.length)
1233
+ continue;
1234
+ if (!SELF_BINARY_TOKEN_RE.test(tokens[i]) && !SELF_ENTRY_PATH_RE.test(tokens[i]) && !isSelfVarRef(tokens[i])) {
1235
+ continue;
1236
+ }
1237
+ const args = tokens.slice(i + 1);
1238
+ const configAt = args.findIndex((a) => CONFIG_SUBCOMMAND_RE.test(a));
1239
+ if (configAt !== -1 && args.slice(configAt + 1).some((a) => PAUSE_FLAG_RE.test(a))) {
1240
+ return "pause";
1241
+ }
1242
+ found = "cli";
1243
+ }
1244
+ return found;
1245
+ }
1246
+ function decodeAnsiC(body) {
1247
+ return body.replace(/\\(x[0-9A-Fa-f]{1,2}|u[0-9A-Fa-f]{1,4}|U[0-9A-Fa-f]{1,8}|[0-7]{1,3}|.)/g, (_, seq) => {
1248
+ try {
1249
+ if (seq[0] === "x")
1250
+ return String.fromCharCode(parseInt(seq.slice(1), 16));
1251
+ if (seq[0] === "u" || seq[0] === "U")
1252
+ return String.fromCodePoint(parseInt(seq.slice(1), 16));
1253
+ if (/^[0-7]+$/.test(seq))
1254
+ return String.fromCharCode(parseInt(seq, 8));
1255
+ } catch {
1256
+ return seq;
1257
+ }
1258
+ const named = {
1259
+ a: "\x07",
1260
+ b: "\b",
1261
+ e: "\x1B",
1262
+ f: "\f",
1263
+ n: `
1264
+ `,
1265
+ r: "\r",
1266
+ t: "\t",
1267
+ v: "\v",
1268
+ "\\": "\\",
1269
+ "'": "'",
1270
+ '"': '"',
1271
+ "?": "?"
1272
+ };
1273
+ return named[seq] ?? seq;
1274
+ });
1275
+ }
1276
+ function stripShellQuoting(command) {
1277
+ const joined = command.replace(/\\\r?\n/g, "");
1278
+ const ansiDecoded = joined.replace(/\$'((?:[^'\\]|\\.)*)'/g, (_, body) => decodeAnsiC(body));
1279
+ return ansiDecoded.replace(/\\(.)/g, "$1").replace(/['"]/g, "");
1280
+ }
1281
+ function statGitHeadMtimeMs(cwd) {
1282
+ try {
1283
+ return statSync3(join2(cwd, ".git", "HEAD")).mtimeMs;
1284
+ } catch {
1285
+ return null;
1286
+ }
1287
+ }
1288
+ function getCurrentBranch(cwd) {
1289
+ try {
1290
+ const headMtimeMs = statGitHeadMtimeMs(cwd);
1291
+ const cached = gitBranchCache.get(cwd);
1292
+ if (cached && headMtimeMs !== null && cached.headMtimeMs === headMtimeMs) {
1293
+ return cached.branch || null;
1294
+ }
1295
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
1296
+ cwd,
1297
+ encoding: "utf8",
1298
+ stdio: ["pipe", "pipe", "pipe"],
1299
+ timeout: 3000
1300
+ }).trim();
1301
+ if (headMtimeMs !== null) {
1302
+ if (gitBranchCache.size >= GIT_BRANCH_CACHE_MAX_ENTRIES)
1303
+ gitBranchCache.clear();
1304
+ gitBranchCache.set(cwd, { branch, headMtimeMs });
1305
+ }
1306
+ return branch || null;
1307
+ } catch {
1308
+ return null;
1309
+ }
1310
+ }
1311
+ function getHeadSha(cwd) {
1312
+ try {
1313
+ const sha = execSync("git rev-parse HEAD", {
1314
+ cwd,
1315
+ encoding: "utf8",
1316
+ stdio: ["pipe", "pipe", "pipe"],
1317
+ timeout: 3000
1318
+ }).trim();
1319
+ return sha || null;
1320
+ } catch {
1321
+ return null;
1322
+ }
1323
+ }
1324
+ function getThirdPartyCheckRuns(cwd, sha) {
1325
+ try {
1326
+ const json = execFileSync("gh", [
1327
+ "api",
1328
+ `repos/{owner}/{repo}/commits/${sha}/check-runs`,
1329
+ "--jq",
1330
+ '.check_runs | map(select(.app.slug != "github-actions")) | map({name: .name, status: .status, conclusion: (.conclusion // "")})'
1331
+ ], {
1332
+ cwd,
1333
+ encoding: "utf8",
1334
+ stdio: ["pipe", "pipe", "pipe"],
1335
+ timeout: 15000
1336
+ }).trim();
1337
+ if (!json || json === "[]")
1338
+ return [];
1339
+ return JSON.parse(json);
1340
+ } catch {
1341
+ return [];
1342
+ }
1343
+ }
1344
+ function getCommitStatuses(cwd, sha) {
1345
+ try {
1346
+ const json = execFileSync("gh", [
1347
+ "api",
1348
+ `repos/{owner}/{repo}/commits/${sha}/statuses`,
1349
+ "--jq",
1350
+ "map({name: .context, state: .state}) | unique_by(.name)"
1351
+ ], {
1352
+ cwd,
1353
+ encoding: "utf8",
1354
+ stdio: ["pipe", "pipe", "pipe"],
1355
+ timeout: 15000
1356
+ }).trim();
1357
+ if (!json || json === "[]")
1358
+ return [];
1359
+ const statuses = JSON.parse(json);
1360
+ return statuses.map((s) => ({
1361
+ name: s.name,
1362
+ status: s.state === "pending" ? "in_progress" : "completed",
1363
+ conclusion: s.state === "pending" ? "" : s.state === "success" ? "success" : "failure"
1364
+ }));
1365
+ } catch {
1366
+ return [];
1367
+ }
1368
+ }
1369
+ function matchesAllowedPattern(cmd, pattern) {
1370
+ const cmdTokens = parseArgvTokens(cmd);
1371
+ const patTokens = parseArgvTokens(pattern);
1372
+ if (cmdTokens.length < patTokens.length)
1373
+ return false;
1374
+ if (cmdTokens.some((tok) => SHELL_OPERATORS.has(tok)))
1375
+ return false;
1376
+ if (cmdTokens.some((tok) => SHELL_METACHAR_RE.test(tok)))
1377
+ return false;
1378
+ return patTokens.every((tok, i) => tok === "*" || tok === cmdTokens[i]);
1379
+ }
1380
+ function sanitizeJwt(ctx) {
1381
+ const output = JSON.stringify(ctx.payload);
1382
+ if (JWT_RE.test(output)) {
1383
+ return {
1384
+ decision: "deny",
1385
+ reason: "JWT token detected in tool output",
1386
+ message: "[REDACTED: JWT token removed by failproofai]"
1387
+ };
1388
+ }
1389
+ return allow();
1390
+ }
1391
+ function sanitizeApiKeys(ctx) {
1392
+ const output = JSON.stringify(ctx.payload);
1393
+ for (const [pattern, label] of API_KEY_PATTERNS) {
1394
+ if (pattern.test(output)) {
1395
+ return {
1396
+ decision: "deny",
1397
+ reason: `${label} detected in tool output`,
1398
+ message: `[REDACTED: ${label} removed by failproofai]`
1399
+ };
1400
+ }
1401
+ }
1402
+ const additional = ctx.params?.additionalPatterns ?? [];
1403
+ for (const { regex, label } of additional) {
1404
+ try {
1405
+ if (new RegExp(regex).test(output)) {
1406
+ return {
1407
+ decision: "deny",
1408
+ reason: `${label} detected in tool output`,
1409
+ message: `[REDACTED: ${label} removed by failproofai]`
1410
+ };
1411
+ }
1412
+ } catch {
1413
+ hookLogWarn(`additionalPatterns: invalid regex "${regex}", skipping`);
1414
+ }
1415
+ }
1416
+ return allow();
1417
+ }
1418
+ function sanitizeConnectionStrings(ctx) {
1419
+ const output = JSON.stringify(ctx.payload);
1420
+ if (CONNECTION_STRING_RE.test(output)) {
1421
+ return {
1422
+ decision: "deny",
1423
+ reason: "Database connection string with credentials detected in tool output",
1424
+ message: "[REDACTED: connection string removed by failproofai]"
1425
+ };
1426
+ }
1427
+ return allow();
1428
+ }
1429
+ function sanitizePrivateKeyContent(ctx) {
1430
+ const output = JSON.stringify(ctx.payload);
1431
+ if (PRIVATE_KEY_RE.test(output)) {
1432
+ return {
1433
+ decision: "deny",
1434
+ reason: "Private key content detected in tool output",
1435
+ message: "[REDACTED: private key content removed by failproofai]"
1436
+ };
1437
+ }
1438
+ return allow();
1439
+ }
1440
+ function sanitizeBearerTokens(ctx) {
1441
+ const output = JSON.stringify(ctx.payload);
1442
+ if (BEARER_TOKEN_RE.test(output)) {
1443
+ return {
1444
+ decision: "deny",
1445
+ reason: "Bearer token detected in tool output",
1446
+ message: "[REDACTED: Bearer token removed by failproofai]"
1447
+ };
1448
+ }
1449
+ return allow();
1450
+ }
1451
+ function warnDestructiveSql(ctx) {
1452
+ if (ctx.toolName !== "Bash")
1453
+ return allow();
1454
+ const cmd = getCommand(ctx);
1455
+ if (!SQL_TOOL_RE.test(cmd))
1456
+ return allow();
1457
+ if (DESTRUCTIVE_SQL_RE.test(cmd)) {
1458
+ return instruct("STOP: This command contains destructive SQL (DROP/TRUNCATE/DELETE). Confirm with the user before executing.");
1459
+ }
1460
+ if (DELETE_NO_WHERE_RE.test(cmd) && !SQL_WHERE_RE.test(cmd)) {
1461
+ return instruct("STOP: This command contains destructive SQL (DROP/TRUNCATE/DELETE). Confirm with the user before executing.");
1462
+ }
1463
+ return allow();
1464
+ }
1465
+ function warnLargeFileWrite(ctx) {
1466
+ if (ctx.toolName !== "Write")
1467
+ return allow();
1468
+ const content = ctx.toolInput?.content;
1469
+ if (typeof content !== "string")
1470
+ return allow();
1471
+ const thresholdKb = ctx.params?.thresholdKb ?? 1024;
1472
+ const thresholdBytes = thresholdKb * 1024;
1473
+ if (content.length > thresholdBytes) {
1474
+ return instruct(`STOP: You are writing a file larger than ${thresholdKb}KB (${Math.round(content.length / 1024)}KB). This is unusually large. Confirm this is intentional before proceeding.`);
1475
+ }
1476
+ return allow();
1477
+ }
1478
+ function warnPackagePublish(ctx) {
1479
+ if (ctx.toolName !== "Bash")
1480
+ return allow();
1481
+ const cmd = getCommand(ctx);
1482
+ if (PUBLISH_CMD_RE.test(cmd)) {
1483
+ return instruct("STOP: This command publishes a package to a public registry. Confirm with the user that this is intentional.");
1484
+ }
1485
+ return allow();
1486
+ }
1487
+ function protectEnvVars(ctx) {
1488
+ if (ctx.toolName !== "Bash")
1489
+ return allow();
1490
+ const cmd = getCommand(ctx);
1491
+ if (ENV_PRINTENV_RE.test(cmd)) {
1492
+ return deny("Command reads environment variables");
1493
+ }
1494
+ if (ECHO_ENV_RE.test(cmd)) {
1495
+ return deny("Command echoes environment variable");
1496
+ }
1497
+ if (EXPORT_RE.test(cmd)) {
1498
+ return deny("Command exports environment variable");
1499
+ }
1500
+ if (PS_ENV_VAR_RE.test(cmd)) {
1501
+ return deny("Command reads environment variable via PowerShell");
1502
+ }
1503
+ if (PS_CHILDITEM_ENV_RE.test(cmd)) {
1504
+ return deny("Command reads environment variables via PowerShell");
1505
+ }
1506
+ if (DOTNET_GETENV_RE.test(cmd)) {
1507
+ return deny("Command reads environment variable via .NET");
1508
+ }
1509
+ if (CMD_ECHO_ENV_RE.test(cmd)) {
1510
+ return deny("Command echoes environment variable via cmd");
1511
+ }
1512
+ return allow();
1513
+ }
1514
+ function blockEnvFiles(ctx) {
1515
+ const cmd = getCommand(ctx);
1516
+ const filePath = getFilePath(ctx);
1517
+ if (filePath && ENV_FILE_PATH_RE.test(filePath)) {
1518
+ return deny("Access to .env file blocked");
1519
+ }
1520
+ if (ctx.toolName === "Bash" && ENV_CMD_RE.test(cmd)) {
1521
+ return deny("Command references .env file");
1522
+ }
1523
+ return allow();
1524
+ }
1525
+ function quoteAwareSegments(command) {
1526
+ const segments = [];
1527
+ let tokens = [];
1528
+ let token = "";
1529
+ let quote = null;
1530
+ const endToken = () => {
1531
+ if (token)
1532
+ tokens.push(token);
1533
+ token = "";
1534
+ };
1535
+ const endSegment = () => {
1536
+ endToken();
1537
+ if (tokens.length)
1538
+ segments.push(tokens);
1539
+ tokens = [];
1540
+ };
1541
+ for (let i = 0;i < command.length; i++) {
1542
+ const c = command[i];
1543
+ if (quote) {
1544
+ if (c === "\\" && quote === '"' && i + 1 < command.length) {
1545
+ token += command[++i];
1546
+ continue;
2185
1547
  }
2186
- },
2187
- {
2188
- name: "block-gcloud",
2189
- displayTitle: "Tried to run a Google Cloud command",
2190
- impact: "gcloud can spend money or break prod — gated.",
2191
- description: "Block gcloud (Google Cloud) CLI commands",
2192
- fn: blockGcloud,
2193
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
2194
- defaultEnabled: false,
2195
- category: "Infra Commands",
2196
- params: {
2197
- allowPatterns: {
2198
- type: "string[]",
2199
- description: "gcloud command patterns to allow (e.g. 'gcloud auth list', 'gcloud config list')",
2200
- default: []
2201
- }
1548
+ if (c === quote) {
1549
+ quote = null;
1550
+ continue;
2202
1551
  }
2203
- },
2204
- {
2205
- name: "block-az-cli",
2206
- displayTitle: "Tried to run an Azure CLI command",
2207
- impact: "az can spend money or break prod — gated.",
2208
- description: "Block az (Azure) CLI commands",
2209
- fn: blockAzCli,
2210
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
2211
- defaultEnabled: false,
2212
- category: "Infra Commands",
2213
- params: {
2214
- allowPatterns: {
2215
- type: "string[]",
2216
- description: "az CLI command patterns to allow (e.g. 'az account show', 'az group list')",
2217
- default: []
2218
- }
1552
+ token += c;
1553
+ continue;
1554
+ }
1555
+ if (c === "\\" && i + 1 < command.length) {
1556
+ token += command[++i];
1557
+ continue;
1558
+ }
1559
+ if (c === '"' || c === "'") {
1560
+ quote = c;
1561
+ continue;
1562
+ }
1563
+ if (/\s/.test(c)) {
1564
+ endToken();
1565
+ continue;
1566
+ }
1567
+ if (/[;&|\n\r(){}`]/.test(c)) {
1568
+ endSegment();
1569
+ continue;
1570
+ }
1571
+ token += c;
1572
+ }
1573
+ endSegment();
1574
+ return segments;
1575
+ }
1576
+ function namesElevation(command, depth = 0) {
1577
+ for (const tokens of quoteAwareSegments(command)) {
1578
+ let i = 0;
1579
+ while (i < tokens.length) {
1580
+ const token = tokens[i];
1581
+ const base = token.slice(token.lastIndexOf("/") + 1);
1582
+ if (base === "sudo" || base === "doas")
1583
+ return true;
1584
+ const isSkippable = ENV_ASSIGNMENT_RE.test(token) || token.startsWith("-") || token.startsWith(">") || token.startsWith("<") || RUNNER_OPERAND_RE.test(token) || COMMAND_PREFIX_TOKENS.has(base);
1585
+ if (!isSkippable)
1586
+ break;
1587
+ if (EVAL_FLAG_RE.test(token) && depth < 3 && i + 1 < tokens.length) {
1588
+ if (namesElevation(tokens[i + 1], depth + 1))
1589
+ return true;
2219
1590
  }
2220
- },
2221
- {
2222
- name: "block-helm",
2223
- displayTitle: "Tried to run a Helm command",
2224
- impact: "Helm releases mutate cluster state — gated.",
2225
- description: "Block helm commands",
2226
- fn: blockHelm,
2227
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
2228
- defaultEnabled: false,
2229
- category: "Infra Commands",
2230
- params: {
2231
- allowPatterns: {
2232
- type: "string[]",
2233
- description: "helm command patterns to allow (e.g. 'helm list', 'helm status *')",
2234
- default: []
2235
- }
1591
+ i++;
1592
+ }
1593
+ }
1594
+ return false;
1595
+ }
1596
+ function blockSudo(ctx) {
1597
+ if (ctx.toolName !== "Bash")
1598
+ return allow();
1599
+ const cmd = getCommand(ctx).trimStart();
1600
+ if (namesElevation(cmd)) {
1601
+ const allowPatterns = ctx.params?.allowPatterns ?? [];
1602
+ if (allowPatterns.some((p) => matchesAllowedPattern(cmd, p)))
1603
+ return allow();
1604
+ return deny("sudo commands are blocked");
1605
+ }
1606
+ if (PS_ELEVATION_RE.test(cmd)) {
1607
+ return deny("Elevated process launch is blocked");
1608
+ }
1609
+ if (RUNAS_RE.test(cmd)) {
1610
+ return deny("runas elevation is blocked");
1611
+ }
1612
+ return allow();
1613
+ }
1614
+ function blockCurlPipeSh(ctx) {
1615
+ if (ctx.toolName !== "Bash")
1616
+ return allow();
1617
+ const cmd = getCommand(ctx);
1618
+ if (CURL_PIPE_SH_RE.test(cmd)) {
1619
+ return deny("Piping downloads to shell is blocked");
1620
+ }
1621
+ if (PS_WEB_PIPE_RE.test(cmd)) {
1622
+ return deny("Piping downloads to Invoke-Expression is blocked");
1623
+ }
1624
+ return allow();
1625
+ }
1626
+ function extractGitPushArgs(cmd) {
1627
+ return cmd.split(/&&|\|\||[|;\n]/).map((s) => s.trim()).filter((s) => /^git\s+push\s/.test(s)).map((s) => s.replace(/^git\s+push\s+/, ""));
1628
+ }
1629
+ function blockPushMaster(ctx) {
1630
+ if (ctx.toolName !== "Bash")
1631
+ return allow();
1632
+ const protectedBranches = ctx.params?.protectedBranches ?? ["main", "master"];
1633
+ if (protectedBranches.length === 0)
1634
+ return allow();
1635
+ const args = extractGitPushArgs(getCommand(ctx));
1636
+ const branchPattern = new RegExp(`\\b(?:${protectedBranches.map((b) => b.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b`);
1637
+ if (args.some((a) => branchPattern.test(a))) {
1638
+ return deny(`Pushing to ${protectedBranches.join("/")} is blocked`);
1639
+ }
1640
+ return allow();
1641
+ }
1642
+ function expandHomePrefix(path) {
1643
+ const m = path.match(/^(?:~|\$HOME|\$\{HOME\})(?=$|\/)/);
1644
+ return m ? homedir3() + path.slice(m[0].length) : path;
1645
+ }
1646
+ function stripTrailingGlob(path) {
1647
+ return path.replace(/\/\*$/, "").replace(/\/+$/, "");
1648
+ }
1649
+ function isCatastrophicTarget(token) {
1650
+ const raw = token.replace(/^['"]|['"]$/g, "");
1651
+ if (raw === "")
1652
+ return false;
1653
+ const homePrefix = raw.match(HOME_PREFIX_RE);
1654
+ if (!homePrefix && /^[$`]/.test(raw))
1655
+ return true;
1656
+ const belowRoot = homePrefix ? raw.slice(homePrefix[0].length) : raw.startsWith("/") ? raw : null;
1657
+ if (belowRoot === null)
1658
+ return false;
1659
+ const segments = stripTrailingGlob(belowRoot).split("/").filter(Boolean);
1660
+ if (!homePrefix && SCRATCH_ROOTS.some((r) => `/${segments.join("/")}`.startsWith(`${r}/`)))
1661
+ return false;
1662
+ return segments.length <= CATASTROPHIC_DEPTH;
1663
+ }
1664
+ function recursiveDeletionTargets(seg) {
1665
+ const tokens = parseArgvTokens(seg);
1666
+ const findIdx = tokens.findIndex((t) => FIND_CMD_RE.test(t));
1667
+ if (findIdx >= 0) {
1668
+ const expr = tokens.slice(findIdx + 1);
1669
+ const execIdx = expr.findIndex((t) => FIND_EXEC_RE.test(t));
1670
+ const deletes = expr.includes("-delete") || execIdx >= 0 && RM_CMD_RE.test(expr[execIdx + 1] ?? "");
1671
+ if (deletes) {
1672
+ let start = 0;
1673
+ while (start < expr.length && FIND_GLOBAL_OPT_RE.test(expr[start])) {
1674
+ start += expr[start] === "-D" ? 2 : 1;
2236
1675
  }
2237
- },
2238
- {
2239
- name: "block-gh-pipeline",
2240
- displayTitle: "Tried to run a privileged GitHub CLI pipeline command",
2241
- impact: "Catches `gh workflow run`, `gh pr merge`, `gh secret set`, etc.",
2242
- description: "Block gh CLI pipeline-trigger subcommands (workflow run, run rerun/cancel, pr merge, release create/delete, cache delete, secret set/delete)",
2243
- fn: blockGhPipeline,
2244
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
2245
- defaultEnabled: false,
2246
- category: "Infra Commands",
2247
- params: {
2248
- allowPatterns: {
2249
- type: "string[]",
2250
- description: "gh pipeline command patterns to allow (e.g. specific scripted invocations); read-only gh subcommands like 'gh pr view' and 'gh run list' are not matched by this policy",
2251
- default: []
2252
- }
1676
+ const rest = expr.slice(start);
1677
+ const end = rest.findIndex((t) => FIND_EXPR_START_RE.test(t));
1678
+ return end < 0 ? rest : rest.slice(0, end);
1679
+ }
1680
+ }
1681
+ const rmIdx = tokens.findIndex((t) => RM_CMD_RE.test(t));
1682
+ if (rmIdx >= 0) {
1683
+ const args = tokens.slice(rmIdx + 1);
1684
+ const shortFlags = args.filter((t) => /^-[^-]/.test(t)).join("");
1685
+ const longFlags = args.filter((t) => /^--/.test(t));
1686
+ const recursive = /r/i.test(shortFlags) || longFlags.some((f) => /^--recursive$/i.test(f));
1687
+ const force = /f/.test(shortFlags) || longFlags.some((f) => /^--force$/i.test(f));
1688
+ if (recursive && force)
1689
+ return args.filter((t) => !t.startsWith("-"));
1690
+ }
1691
+ return null;
1692
+ }
1693
+ function shellSegments(cmd) {
1694
+ return cmd.split(/&&|\|\||[|;\n]/).map((s) => s.trim()).filter((s) => s !== "");
1695
+ }
1696
+ function deletionTargetIsAllowed(cmd, allowPaths) {
1697
+ if (allowPaths.length === 0)
1698
+ return false;
1699
+ const normalizedAllowPaths = allowPaths.map((p) => stripTrailingGlob(expandHomePrefix(p)) || "/");
1700
+ let sawRecursiveDelete = false;
1701
+ for (const seg of shellSegments(cmd)) {
1702
+ const targets = recursiveDeletionTargets(seg);
1703
+ if (targets === null)
1704
+ continue;
1705
+ sawRecursiveDelete = true;
1706
+ for (const target of targets) {
1707
+ const normalized = stripTrailingGlob(expandHomePrefix(target)) || "/";
1708
+ const covered = normalizedAllowPaths.some((np) => normalized === np || normalized.startsWith(np + "/"));
1709
+ if (!covered) {
1710
+ const segCovered = allowPaths.some((p) => {
1711
+ const escaped = p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1712
+ return new RegExp(`${escaped}(?:[/"'\\s/*]|$)`).test(seg);
1713
+ });
1714
+ if (!segCovered)
1715
+ return false;
2253
1716
  }
2254
- },
2255
- {
2256
- name: "block-secrets-write",
2257
- displayTitle: "Tried to write a secret-key file",
2258
- impact: "Stops the agent from creating `.pem`, `id_rsa`, `credentials.json`, etc.",
2259
- description: "Block writing secret key files",
2260
- fn: blockSecretsWrite,
2261
- match: { events: ["PreToolUse"], toolNames: ["Write"] },
2262
- defaultEnabled: false,
2263
- category: "Dangerous Commands",
2264
- params: {
2265
- additionalPatterns: {
2266
- type: "string[]",
2267
- description: "Additional filename patterns (substrings) to block",
2268
- default: []
2269
- }
1717
+ }
1718
+ }
1719
+ return sawRecursiveDelete;
1720
+ }
1721
+ function blockRmRf(ctx) {
1722
+ if (ctx.toolName !== "Bash")
1723
+ return allow();
1724
+ const cmd = getCommand(ctx);
1725
+ const hasCatastrophicTarget = shellSegments(cmd).some((seg) => {
1726
+ const targets = recursiveDeletionTargets(seg);
1727
+ return targets !== null && targets.some(isCatastrophicTarget);
1728
+ });
1729
+ if (hasCatastrophicTarget) {
1730
+ const allowPaths = ctx.params?.allowPaths ?? [];
1731
+ if (deletionTargetIsAllowed(cmd, allowPaths))
1732
+ return allow();
1733
+ return deny("Catastrophic deletion blocked");
1734
+ }
1735
+ if (/Remove-Item\s+.*-Recurse.*-Force.*(?:[A-Z]:\\(?:\s|$)|\\\*)/i.test(cmd)) {
1736
+ return deny("Catastrophic deletion blocked");
1737
+ }
1738
+ if (/(?:rd|rmdir)\s+\/s\s+\/q\s+[A-Z]:\\/i.test(cmd)) {
1739
+ return deny("Catastrophic deletion blocked");
1740
+ }
1741
+ return allow();
1742
+ }
1743
+ function blockForcePush(ctx) {
1744
+ if (ctx.toolName !== "Bash")
1745
+ return allow();
1746
+ for (const segment of extractGitPushArgs(getCommand(ctx))) {
1747
+ let sawEndOfOptions = false;
1748
+ for (const token of segment.split(/\s+/)) {
1749
+ if (token === "--") {
1750
+ sawEndOfOptions = true;
1751
+ continue;
2270
1752
  }
2271
- },
2272
- {
2273
- name: "block-push-master",
2274
- displayTitle: "Tried to push directly to main/master",
2275
- impact: "Direct pushes to a protected branch bypass review.",
2276
- description: "Block pushing to main/master",
2277
- fn: blockPushMaster,
2278
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
2279
- defaultEnabled: true,
2280
- category: "Git",
2281
- params: {
2282
- protectedBranches: {
2283
- type: "string[]",
2284
- description: "Branch names to protect from direct pushes",
2285
- default: ["main", "master"]
2286
- }
1753
+ if (sawEndOfOptions)
1754
+ continue;
1755
+ if (isForcePushFlag(token)) {
1756
+ return deny("Force-pushing is blocked");
2287
1757
  }
2288
- },
2289
- {
2290
- name: "block-force-push",
2291
- displayTitle: "Tried to force-push",
2292
- impact: "Force-pushes rewrite history and can clobber teammates' work.",
2293
- description: "Prevent force-pushing to any branch",
2294
- fn: blockForcePush,
2295
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
2296
- defaultEnabled: false,
2297
- category: "Git"
2298
- },
2299
- {
2300
- name: "block-work-on-main",
2301
- displayTitle: "Tried to commit or merge on main/master",
2302
- impact: "Work should land via PR — direct commits skip review.",
2303
- description: "Block git commits and merges on main/master branch",
2304
- fn: blockWorkOnMain,
2305
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
2306
- defaultEnabled: false,
2307
- category: "Git",
2308
- params: {
2309
- protectedBranches: {
2310
- type: "string[]",
2311
- description: "Branch names where commits/merges are blocked",
2312
- default: ["main", "master"]
2313
- }
1758
+ }
1759
+ }
1760
+ return allow();
1761
+ }
1762
+ function isForcePushFlag(token) {
1763
+ if (token === "--force")
1764
+ return true;
1765
+ if (SAFE_FORCE_PREFIXES.some((prefix) => token.startsWith(prefix)))
1766
+ return false;
1767
+ if (token.startsWith("--force"))
1768
+ return true;
1769
+ return SHORT_FLAG_BUNDLE_RE.test(token);
1770
+ }
1771
+ function blockSecretsWrite(ctx) {
1772
+ if (ctx.toolName !== "Write")
1773
+ return allow();
1774
+ const filePath = getFilePath(ctx);
1775
+ if (SECRET_FILE_RE.test(filePath) || SECRET_FILE_ID_RSA_RE.test(filePath) || SECRET_FILE_CREDENTIALS_RE.test(filePath)) {
1776
+ return deny("Writing secret key files is blocked");
1777
+ }
1778
+ const additionalPatterns = ctx.params?.additionalPatterns ?? [];
1779
+ for (const pattern of additionalPatterns) {
1780
+ if (filePath.includes(pattern)) {
1781
+ return deny(`Writing blocked file pattern: ${pattern}`);
1782
+ }
1783
+ }
1784
+ return allow();
1785
+ }
1786
+ function extractAbsolutePaths(command) {
1787
+ const paths = [];
1788
+ const pathRe = /(?<![a-zA-Z0-9_.\-~\\*?:=])(?:~\/[^\s;|&"'()\[\]{}]*|~(?=\s|$|[;|&"'()\[\]{}])|\/[^\s;|&"'()\[\]{}]*)/g;
1789
+ function addPaths(s) {
1790
+ pathRe.lastIndex = 0;
1791
+ let m;
1792
+ while ((m = pathRe.exec(s)) !== null) {
1793
+ let p = m[0];
1794
+ if (p === "~")
1795
+ p = homedir3();
1796
+ else if (p.startsWith("~/"))
1797
+ p = join2(homedir3(), p.slice(2));
1798
+ paths.push(p);
1799
+ }
1800
+ }
1801
+ let firstBarePipe = command.length;
1802
+ let inDouble = false, inSingle = false;
1803
+ for (let i = 0;i < command.length; i++) {
1804
+ const c = command[i];
1805
+ if (c === '"' && !inSingle)
1806
+ inDouble = !inDouble;
1807
+ else if (c === "'" && !inDouble)
1808
+ inSingle = !inSingle;
1809
+ else if (c === "|" && !inDouble && !inSingle) {
1810
+ firstBarePipe = i;
1811
+ break;
1812
+ }
1813
+ }
1814
+ const firstSegment = command.slice(0, firstBarePipe);
1815
+ const quotedRe = /"([^"]*)"|'([^']*)'/g;
1816
+ let qm;
1817
+ while ((qm = quotedRe.exec(firstSegment)) !== null) {
1818
+ const content = qm[1] ?? qm[2] ?? "";
1819
+ if (/[*?\[\]^$+()\\]/.test(content))
1820
+ continue;
1821
+ addPaths(content);
1822
+ }
1823
+ const stripped = command.replace(/"[^"]*"/g, (m) => " ".repeat(m.length)).replace(/'[^']*'/g, (m) => " ".repeat(m.length));
1824
+ addPaths(stripped);
1825
+ return paths;
1826
+ }
1827
+ function blockReadOutsideCwd(ctx) {
1828
+ const cwd = process.env.CLAUDE_PROJECT_DIR || ctx.session?.cwd;
1829
+ if (!cwd)
1830
+ return allow();
1831
+ const allowPaths = ctx.params?.allowPaths ?? [];
1832
+ if (ctx.toolName === "Bash") {
1833
+ const cmd = getCommand(ctx);
1834
+ if (!READ_LIKE_CMDS.test(cmd))
1835
+ return allow();
1836
+ const paths = extractAbsolutePaths(cmd);
1837
+ const cwdWithSep2 = cwd.endsWith("/") ? cwd : cwd + "/";
1838
+ for (const p of paths) {
1839
+ const resolved3 = resolve3(cwd, p);
1840
+ if (isClaudeSettingsFile(resolved3)) {
1841
+ return deny(`Reading agent settings file blocked: ${resolved3}`);
2314
1842
  }
2315
- },
2316
- {
2317
- name: "warn-git-amend",
2318
- displayTitle: "Used git commit --amend",
2319
- impact: "Amending after a push rewrites history that others may have pulled.",
2320
- description: "Warns before amending git commits, which rewrites history",
2321
- fn: warnGitAmend,
2322
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
2323
- defaultEnabled: false,
2324
- category: "Git"
2325
- },
2326
- {
2327
- name: "warn-git-stash-drop",
2328
- displayTitle: "Tried to drop or clear git stash",
2329
- impact: "Stash deletions are permanent and silent.",
2330
- description: "Warns before permanently deleting stashed changes",
2331
- fn: warnGitStashDrop,
2332
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
2333
- defaultEnabled: false,
2334
- category: "Git"
2335
- },
2336
- {
2337
- name: "warn-all-files-staged",
2338
- displayTitle: "Staged all files with git add -A / .",
2339
- impact: "Wide stages routinely catch generated files or secrets you didn't intend to commit.",
2340
- description: "Warns before staging all working tree files with git add -A / . / --all",
2341
- fn: warnAllFilesStaged,
2342
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
2343
- defaultEnabled: false,
2344
- category: "Git"
2345
- },
2346
- {
2347
- name: "warn-destructive-sql",
2348
- displayTitle: "Ran destructive SQL (DROP / TRUNCATE / DELETE without WHERE)",
2349
- impact: "Easy way to wipe a table by accident.",
2350
- description: "Warn before executing destructive SQL (DROP/TRUNCATE/DELETE without WHERE) via database clients",
2351
- fn: warnDestructiveSql,
2352
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
2353
- defaultEnabled: false,
2354
- category: "Database"
2355
- },
2356
- {
2357
- name: "warn-schema-alteration",
2358
- displayTitle: "Altered a database schema column",
2359
- impact: "ALTER TABLE operations can lock tables and break readers.",
2360
- description: "Warns before SQL schema changes (ALTER TABLE with column or rename operations)",
2361
- fn: warnSchemaAlteration,
2362
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
2363
- defaultEnabled: false,
2364
- category: "Database"
2365
- },
2366
- {
2367
- name: "warn-package-publish",
2368
- displayTitle: "Tried to publish a package",
2369
- impact: "Publishes are irreversible — `npm publish` / `cargo publish` shouldn't happen without intent.",
2370
- description: "Warn before publishing packages to public registries (npm, PyPI, crates.io, RubyGems, etc.)",
2371
- fn: warnPackagePublish,
2372
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
2373
- defaultEnabled: false,
2374
- category: "Packages & System"
2375
- },
2376
- {
2377
- name: "warn-global-package-install",
2378
- displayTitle: "Installed a package globally",
2379
- impact: "`npm i -g`, `cargo install`, `pip --user` pollute your machine outside the project.",
2380
- description: "Warns before installing packages globally (npm -g, cargo install, etc.)",
2381
- fn: warnGlobalPackageInstall,
2382
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
2383
- defaultEnabled: false,
2384
- category: "Packages & System"
2385
- },
2386
- {
2387
- name: "prefer-package-manager",
2388
- displayTitle: "Used a non-preferred package manager",
2389
- impact: "Mixing package managers creates lockfile churn for your team.",
2390
- description: "Blocks non-preferred package managers and tells Claude to use an allowed one (e.g., uv instead of pip)",
2391
- fn: preferPackageManager,
2392
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
2393
- defaultEnabled: false,
2394
- category: "Packages & System",
2395
- params: {
2396
- allowed: {
2397
- type: "string[]",
2398
- description: "Allowed package manager names (e.g. ['uv', 'bun']). Any detected manager not in this list is blocked.",
2399
- default: []
2400
- },
2401
- blocked: {
2402
- type: "string[]",
2403
- description: "Additional manager names to block beyond the built-in list (e.g. ['pdm', 'pipx']).",
2404
- default: []
1843
+ if (isClaudeInternalPath(resolved3))
1844
+ continue;
1845
+ if (resolved3 === "/dev/null")
1846
+ continue;
1847
+ if (resolved3 !== cwd && !resolved3.startsWith(cwdWithSep2)) {
1848
+ if (allowPaths.some((ap) => resolved3 === ap || resolved3.startsWith(ap.endsWith("/") ? ap : ap + "/")))
1849
+ continue;
1850
+ return deny(`Bash read outside project directory blocked: ${resolved3}`);
1851
+ }
1852
+ }
1853
+ return allow();
1854
+ }
1855
+ const filePath = getFilePath(ctx);
1856
+ const searchPath = ctx.toolInput?.path ?? "";
1857
+ const target = filePath || searchPath;
1858
+ if (!target)
1859
+ return allow();
1860
+ const resolved2 = resolve3(cwd, target);
1861
+ if (isClaudeSettingsFile(resolved2)) {
1862
+ return deny(`Reading agent settings file blocked: ${resolved2}`);
1863
+ }
1864
+ if (isClaudeInternalPath(resolved2))
1865
+ return allow();
1866
+ if (resolved2 === "/dev/null")
1867
+ return allow();
1868
+ const cwdWithSep = cwd.endsWith("/") ? cwd : cwd + "/";
1869
+ if (resolved2 !== cwd && !resolved2.startsWith(cwdWithSep)) {
1870
+ if (allowPaths.some((ap) => resolved2 === ap || resolved2.startsWith(ap.endsWith("/") ? ap : ap + "/")))
1871
+ return allow();
1872
+ return deny(`Access outside project directory blocked: ${resolved2}`);
1873
+ }
1874
+ return allow();
1875
+ }
1876
+ function blockWorkOnMain(ctx) {
1877
+ if (ctx.toolName !== "Bash")
1878
+ return allow();
1879
+ const cmd = getCommand(ctx);
1880
+ const match = cmd.match(GIT_COMMIT_MERGE_RE);
1881
+ if (!match)
1882
+ return allow();
1883
+ const cwd = ctx.session?.cwd;
1884
+ if (!cwd)
1885
+ return allow();
1886
+ const branch = getCurrentBranch(cwd);
1887
+ if (!branch)
1888
+ return allow();
1889
+ const protectedBranches = ctx.params?.protectedBranches ?? ["main", "master"];
1890
+ if (protectedBranches.includes(branch)) {
1891
+ return deny(`Git ${match[1]} on ${branch} is blocked. Create a feature branch first.`);
1892
+ }
1893
+ return allow();
1894
+ }
1895
+ function blockFailproofaiCommands(ctx) {
1896
+ if (ctx.toolName !== "Bash")
1897
+ return allow();
1898
+ const cmd = getCommand(ctx);
1899
+ const unescaped = stripShellQuoting(cmd);
1900
+ const kind = classifySelfInvocation(cmd) ?? classifySelfInvocation(unescaped);
1901
+ if (kind === "pause") {
1902
+ return deny("Pausing failproofai enforcement is a human action, not an agent one. " + "If a policy is blocking legitimate work, say so and let the operator decide.");
1903
+ }
1904
+ if (kind === "cli") {
1905
+ return deny("Running failproofai CLI commands is blocked");
1906
+ }
1907
+ if (FAILPROOFAI_UNINSTALL_RE.test(cmd) || FAILPROOFAI_UNINSTALL_RE.test(unescaped)) {
1908
+ return deny("Uninstalling failproofai is blocked");
1909
+ }
1910
+ if (FAILPROOFAI_STATE_WRITE_RE.test(cmd) || FAILPROOFAI_STATE_WRITE_RE.test(unescaped)) {
1911
+ return deny("Deleting or moving failproofai's own state would switch enforcement off. " + "If a policy is blocking legitimate work, say so and let the operator decide.");
1912
+ }
1913
+ return allow();
1914
+ }
1915
+ function blockInfraCli(ctx, re, denyMsg) {
1916
+ if (ctx.toolName !== "Bash")
1917
+ return allow();
1918
+ const cmd = getCommand(ctx);
1919
+ if (!re.test(cmd))
1920
+ return allow();
1921
+ const allowPatterns = ctx.params?.allowPatterns ?? [];
1922
+ if (allowPatterns.some((p) => matchesAllowedPattern(cmd, p)))
1923
+ return allow();
1924
+ return deny(denyMsg);
1925
+ }
1926
+ function blockKubectl(ctx) {
1927
+ return blockInfraCli(ctx, KUBECTL_RE, "kubectl commands are blocked");
1928
+ }
1929
+ function blockTerraform(ctx) {
1930
+ return blockInfraCli(ctx, TERRAFORM_RE, "terraform/tofu commands are blocked");
1931
+ }
1932
+ function blockAwsCli(ctx) {
1933
+ return blockInfraCli(ctx, AWS_CLI_RE, "aws CLI commands are blocked");
1934
+ }
1935
+ function blockGcloud(ctx) {
1936
+ return blockInfraCli(ctx, GCLOUD_RE, "gcloud commands are blocked");
1937
+ }
1938
+ function blockAzCli(ctx) {
1939
+ return blockInfraCli(ctx, AZ_CLI_RE, "az (Azure) CLI commands are blocked");
1940
+ }
1941
+ function blockHelm(ctx) {
1942
+ return blockInfraCli(ctx, HELM_RE, "helm commands are blocked");
1943
+ }
1944
+ function blockGhPipeline(ctx) {
1945
+ return blockInfraCli(ctx, GH_PIPELINE_RE, "gh pipeline-trigger commands are blocked");
1946
+ }
1947
+ async function warnRepeatedToolCalls(ctx) {
1948
+ const THRESHOLD = 3;
1949
+ const transcriptPath = ctx.session?.transcriptPath;
1950
+ if (!transcriptPath || !ctx.toolName || !ctx.toolInput)
1951
+ return allow();
1952
+ const trackerPath = `${transcriptPath}.tool-calls.json`;
1953
+ const fingerprint = JSON.stringify({ tool: ctx.toolName, input: ctx.toolInput });
1954
+ let counts = {};
1955
+ try {
1956
+ const raw = await readFile(trackerPath, "utf8");
1957
+ counts = JSON.parse(raw);
1958
+ } catch {}
1959
+ const prevCount = counts[fingerprint] ?? 0;
1960
+ if (prevCount >= THRESHOLD) {
1961
+ return instruct(`STOP: You have already called ${ctx.toolName} ${prevCount} times with identical parameters. This is wasteful and unproductive. Do NOT repeat this call — use a different approach or ask the user for clarification.`);
1962
+ }
1963
+ counts[fingerprint] = prevCount + 1;
1964
+ try {
1965
+ const serialized = JSON.stringify(counts);
1966
+ if (serialized.length <= TOOL_CALL_TRACKER_MAX_BYTES) {
1967
+ await writeFile(trackerPath, serialized, "utf8");
1968
+ }
1969
+ } catch {}
1970
+ return allow();
1971
+ }
1972
+ function warnGitAmend(ctx) {
1973
+ if (ctx.toolName !== "Bash")
1974
+ return allow();
1975
+ const cmd = getCommand(ctx);
1976
+ if (GIT_AMEND_RE.test(cmd)) {
1977
+ return instruct("STOP: This command amends the last commit, which rewrites git history. If this commit has already been pushed to a shared branch, this will cause divergence for other contributors. Confirm with the user before executing.");
1978
+ }
1979
+ return allow();
1980
+ }
1981
+ function warnGitStashDrop(ctx) {
1982
+ if (ctx.toolName !== "Bash")
1983
+ return allow();
1984
+ const cmd = getCommand(ctx);
1985
+ if (GIT_STASH_DROP_RE.test(cmd)) {
1986
+ return instruct("STOP: This command permanently deletes stashed changes (git stash drop/clear). Stash entries cannot be recovered after deletion. Confirm with the user before executing.");
1987
+ }
1988
+ return allow();
1989
+ }
1990
+ function warnAllFilesStaged(ctx) {
1991
+ if (ctx.toolName !== "Bash")
1992
+ return allow();
1993
+ const cmd = getCommand(ctx);
1994
+ if (GIT_ADD_ALL_RE.test(cmd)) {
1995
+ return instruct("STOP: This command stages all files in the working tree (git add -A / --all / .). This may inadvertently include build artifacts, generated files, or sensitive files not covered by .gitignore. Confirm with the user before executing.");
1996
+ }
1997
+ return allow();
1998
+ }
1999
+ function warnSchemaAlteration(ctx) {
2000
+ if (ctx.toolName !== "Bash")
2001
+ return allow();
2002
+ const cmd = getCommand(ctx);
2003
+ if (!SQL_TOOL_RE.test(cmd))
2004
+ return allow();
2005
+ if (SCHEMA_ALTER_RE.test(cmd)) {
2006
+ return instruct("STOP: This command contains a schema-altering SQL statement (ALTER TABLE with column or rename operation). Schema changes on production databases are irreversible or disruptive. Confirm with the user before executing.");
2007
+ }
2008
+ return allow();
2009
+ }
2010
+ function warnGlobalPackageInstall(ctx) {
2011
+ if (ctx.toolName !== "Bash")
2012
+ return allow();
2013
+ const cmd = getCommand(ctx);
2014
+ const isGlobal = NPM_GLOBAL_RE.test(cmd) || YARN_GLOBAL_RE.test(cmd) || PNPM_GLOBAL_RE.test(cmd) || BUN_GLOBAL_RE.test(cmd) || CARGO_INSTALL_RE.test(cmd) || PIP_SYSTEM_RE.test(cmd);
2015
+ if (isGlobal) {
2016
+ return instruct("STOP: This command installs a package globally, which modifies the system-wide environment outside the project. This can conflict with other projects or system tools. Confirm with the user before executing.");
2017
+ }
2018
+ return allow();
2019
+ }
2020
+ function preferPackageManager(ctx) {
2021
+ if (ctx.toolName !== "Bash")
2022
+ return allow();
2023
+ const cmd = getCommand(ctx);
2024
+ if (!cmd)
2025
+ return allow();
2026
+ const allowed = ctx.params?.allowed ?? [];
2027
+ if (allowed.length === 0)
2028
+ return allow();
2029
+ const allowedSet = new Set(allowed.map((a) => a.toLowerCase()));
2030
+ const blocked = ctx.params?.blocked ?? [];
2031
+ const allowedList = allowed.join(", ");
2032
+ const segments = cmd.split(SEGMENT_SPLIT_RE);
2033
+ for (const segment of segments) {
2034
+ const trimmed = segment.trim();
2035
+ if (!trimmed)
2036
+ continue;
2037
+ let segmentAllowed = false;
2038
+ for (const manager of allowedSet) {
2039
+ const patterns = PKG_MANAGER_DETECTORS[manager];
2040
+ if (!patterns)
2041
+ continue;
2042
+ for (const pattern of patterns) {
2043
+ if (pattern.test(trimmed)) {
2044
+ segmentAllowed = true;
2045
+ break;
2405
2046
  }
2406
2047
  }
2407
- },
2408
- {
2409
- name: "warn-large-file-write",
2410
- displayTitle: "Wrote a file larger than the configured threshold",
2411
- impact: "Catches accidentally large file writes (logs, binaries, model dumps).",
2412
- description: "Warn before writing files larger than 1MB (configurable via thresholdKb param)",
2413
- fn: warnLargeFileWrite,
2414
- match: { events: ["PreToolUse"], toolNames: ["Write"] },
2415
- defaultEnabled: false,
2416
- category: "Packages & System",
2417
- params: {
2418
- thresholdKb: {
2419
- type: "number",
2420
- description: "File size threshold in KB above which a warning is issued",
2421
- default: 1024
2048
+ if (segmentAllowed)
2049
+ break;
2050
+ }
2051
+ if (segmentAllowed)
2052
+ continue;
2053
+ for (const [manager, patterns] of Object.entries(PKG_MANAGER_DETECTORS)) {
2054
+ if (allowedSet.has(manager))
2055
+ continue;
2056
+ for (const pattern of patterns) {
2057
+ if (pattern.test(trimmed)) {
2058
+ return deny(`"${manager}" is not an allowed package manager. ` + `Allowed package managers for this project: ${allowedList}. ` + `Rewrite this command using an allowed package manager.`);
2422
2059
  }
2423
2060
  }
2424
- },
2425
- {
2426
- name: "warn-background-process",
2427
- displayTitle: "Started a long-lived background process",
2428
- impact: "Catches `nohup` / `&` / `screen` / `tmux` / `disown` patterns that the agent often forgets to clean up.",
2429
- description: "Warns before starting detached or background processes",
2430
- fn: warnBackgroundProcess,
2431
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
2432
- defaultEnabled: false,
2433
- category: "Packages & System"
2434
- },
2435
- {
2436
- name: "warn-repeated-tool-calls",
2437
- displayTitle: "Called the same tool 3+ times with identical arguments",
2438
- impact: "Usually a sign of a stuck loop burning tokens.",
2439
- description: "Warn when the same tool is called 3+ times with identical parameters",
2440
- fn: warnRepeatedToolCalls,
2441
- match: { events: ["PreToolUse"] },
2442
- defaultEnabled: false,
2443
- category: "AI Behavior"
2444
- },
2445
- {
2446
- name: "require-commit-before-stop",
2447
- displayTitle: "Stopped with uncommitted changes",
2448
- impact: "Work not in a commit is invisible to teammates and easy to lose.",
2449
- description: "Require all changes to be committed before Claude stops",
2450
- fn: requireCommitBeforeStop,
2451
- match: { events: ["Stop"] },
2452
- defaultEnabled: false,
2453
- category: "Workflow"
2454
- },
2455
- {
2456
- name: "require-push-before-stop",
2457
- displayTitle: "Stopped with unpushed commits",
2458
- impact: "Local-only commits won't trigger CI or be reviewable.",
2459
- description: "Require all commits to be pushed to remote before Claude stops",
2460
- fn: requirePushBeforeStop,
2461
- match: { events: ["Stop"] },
2462
- defaultEnabled: false,
2463
- category: "Workflow",
2464
- params: {
2465
- remote: {
2466
- type: "string",
2467
- description: "Remote name to push to (default: origin)",
2468
- default: "origin"
2469
- },
2470
- baseBranch: {
2471
- type: "string",
2472
- description: "Base branch to compare against (default: main)",
2473
- default: "main"
2474
- }
2061
+ }
2062
+ for (const name of blocked) {
2063
+ const lower = name.toLowerCase();
2064
+ if (allowedSet.has(lower))
2065
+ continue;
2066
+ const re = new RegExp(`\\b${lower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
2067
+ if (re.test(trimmed)) {
2068
+ return deny(`"${lower}" is not an allowed package manager. ` + `Allowed package managers for this project: ${allowedList}. ` + `Rewrite this command using an allowed package manager.`);
2475
2069
  }
2476
- },
2477
- {
2478
- name: "require-pr-before-stop",
2479
- displayTitle: "Stopped without a PR for the branch",
2480
- impact: "Branches without PRs don't get reviewed.",
2481
- description: "Require a pull request to exist for the current branch before Claude stops",
2482
- fn: requirePrBeforeStop,
2483
- match: { events: ["Stop"] },
2484
- defaultEnabled: false,
2485
- category: "Workflow",
2486
- params: {
2487
- baseBranch: {
2488
- type: "string",
2489
- description: "Base branch to compare against (default: main)",
2490
- default: "main"
2491
- }
2070
+ }
2071
+ }
2072
+ return allow();
2073
+ }
2074
+ function warnBackgroundProcess(ctx) {
2075
+ if (ctx.toolName !== "Bash")
2076
+ return allow();
2077
+ const cmd = getCommand(ctx);
2078
+ const isBackground = NOHUP_RE.test(cmd) || SCREEN_DETACH_RE.test(cmd) || TMUX_DETACH_RE.test(cmd) || DISOWN_RE.test(cmd) || BACKGROUND_AMPERSAND_RE.test(cmd);
2079
+ if (isBackground) {
2080
+ return instruct("STOP: This command starts a background or detached process (nohup, screen -d, tmux -d, or trailing &). Background processes persist after Claude's session and may be difficult to track or stop. Confirm with the user before executing.");
2081
+ }
2082
+ return allow();
2083
+ }
2084
+ function isPlanMode(ctx) {
2085
+ return ctx.session?.permissionMode === "plan";
2086
+ }
2087
+ function requireCommitBeforeStop(ctx) {
2088
+ if (isPlanMode(ctx))
2089
+ return allow("Plan mode — no changes made, skipping commit check.");
2090
+ const cwd = ctx.session?.cwd;
2091
+ if (!cwd)
2092
+ return allow("No working directory available, skipping commit check.");
2093
+ try {
2094
+ const status = execSync("git status --porcelain", {
2095
+ cwd,
2096
+ encoding: "utf8",
2097
+ stdio: ["pipe", "pipe", "pipe"],
2098
+ timeout: 5000
2099
+ }).trim();
2100
+ if (status.length > 0) {
2101
+ return deny("You have uncommitted changes in the working directory. Commit all changes now.");
2102
+ }
2103
+ return allow("All changes are committed.");
2104
+ } catch {
2105
+ return allow("Not a git repository, skipping commit check.");
2106
+ }
2107
+ }
2108
+ function requirePushBeforeStop(ctx) {
2109
+ if (isPlanMode(ctx))
2110
+ return allow("Plan mode — no changes made, skipping push check.");
2111
+ const cwd = ctx.session?.cwd;
2112
+ if (!cwd)
2113
+ return allow("No working directory available, skipping push check.");
2114
+ try {
2115
+ const remotes = execSync("git remote", {
2116
+ cwd,
2117
+ encoding: "utf8",
2118
+ stdio: ["pipe", "pipe", "pipe"],
2119
+ timeout: 3000
2120
+ }).trim();
2121
+ if (!remotes)
2122
+ return allow("No git remote configured, skipping push check.");
2123
+ const remote = ctx.params?.remote ?? "origin";
2124
+ const branch = getCurrentBranch(cwd);
2125
+ if (!branch || branch === "HEAD")
2126
+ return allow("Detached HEAD, skipping push check.");
2127
+ const baseBranch = ctx.params?.baseBranch ?? "main";
2128
+ if (branch === baseBranch) {
2129
+ return allow(`On base branch "${baseBranch}", skipping push check.`);
2130
+ }
2131
+ try {
2132
+ const ahead = execFileSync("git", ["log", `${remote}/${baseBranch}..HEAD`, "--oneline"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
2133
+ if (!ahead) {
2134
+ return allow(`No commits ahead of ${remote}/${baseBranch}, skipping push check.`);
2135
+ }
2136
+ const diff = execFileSync("git", ["diff", "--stat", `${remote}/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
2137
+ if (!diff) {
2138
+ return allow(`No file changes compared to ${remote}/${baseBranch}, skipping push check.`);
2139
+ }
2140
+ } catch {}
2141
+ let hasTracking = false;
2142
+ try {
2143
+ execFileSync("git", ["rev-parse", "--verify", `${remote}/${branch}`], {
2144
+ cwd,
2145
+ encoding: "utf8",
2146
+ stdio: ["pipe", "pipe", "pipe"],
2147
+ timeout: 3000
2148
+ });
2149
+ hasTracking = true;
2150
+ } catch {}
2151
+ if (!hasTracking) {
2152
+ return deny(`Branch "${branch}" has not been pushed to remote "${remote}". ` + `Run now: git push -u ${remote} ${branch}`);
2153
+ }
2154
+ const unpushed = execFileSync("git", ["log", `${remote}/${branch}..HEAD`, "--oneline"], {
2155
+ cwd,
2156
+ encoding: "utf8",
2157
+ stdio: ["pipe", "pipe", "pipe"],
2158
+ timeout: 5000
2159
+ }).trim();
2160
+ if (unpushed.length > 0) {
2161
+ const commitCount = unpushed.split(`
2162
+ `).length;
2163
+ return deny(`You have ${commitCount} unpushed commit${commitCount > 1 ? "s" : ""} on branch "${branch}". ` + `Run now: git push`);
2164
+ }
2165
+ return allow(`All commits pushed to "${remote}".`);
2166
+ } catch {
2167
+ return allow("Could not check push status, skipping.");
2168
+ }
2169
+ }
2170
+ function requirePrBeforeStop(ctx) {
2171
+ if (isPlanMode(ctx))
2172
+ return allow("Plan mode — no changes made, skipping PR check.");
2173
+ const cwd = ctx.session?.cwd;
2174
+ if (!cwd)
2175
+ return allow("No working directory available, skipping PR check.");
2176
+ try {
2177
+ try {
2178
+ execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
2179
+ } catch {
2180
+ return allow("GitHub CLI (gh) not installed, skipping PR check.");
2181
+ }
2182
+ const branch = getCurrentBranch(cwd);
2183
+ if (!branch || branch === "HEAD")
2184
+ return allow("Detached HEAD, skipping PR check.");
2185
+ const baseBranch = ctx.params?.baseBranch ?? "main";
2186
+ if (branch === baseBranch) {
2187
+ return allow(`On base branch "${baseBranch}", skipping PR check.`);
2188
+ }
2189
+ try {
2190
+ const ahead = execFileSync("git", ["log", `origin/${baseBranch}..HEAD`, "--oneline"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
2191
+ if (!ahead) {
2192
+ return allow(`No commits ahead of origin/${baseBranch}, skipping PR check.`);
2492
2193
  }
2493
- },
2494
- {
2495
- name: "require-no-conflicts-before-stop",
2496
- displayTitle: "Stopped with a branch that conflicts with main",
2497
- impact: "Conflicting branches can't merge — surface them early.",
2498
- description: "Require the current branch to merge cleanly with the base branch before Claude stops",
2499
- fn: requireNoConflictsBeforeStop,
2500
- match: { events: ["Stop"] },
2501
- defaultEnabled: false,
2502
- category: "Workflow",
2503
- params: {
2504
- baseBranch: {
2505
- type: "string",
2506
- description: "Base branch to check for conflicts against (default: main)",
2507
- default: "main"
2508
- }
2194
+ const diff = execFileSync("git", ["diff", "--stat", `origin/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
2195
+ if (!diff) {
2196
+ return allow(`No file changes compared to origin/${baseBranch}, skipping PR check.`);
2509
2197
  }
2510
- },
2511
- {
2512
- name: "require-ci-green-before-stop",
2513
- displayTitle: "Stopped with failing CI",
2514
- impact: "Failing CI blocks deploy.",
2515
- description: "Require CI checks to pass on the current HEAD commit before Claude stops (ignores stale runs on prior commits)",
2516
- fn: requireCiGreenBeforeStop,
2517
- match: { events: ["Stop"] },
2518
- defaultEnabled: false,
2519
- category: "Workflow"
2198
+ } catch {}
2199
+ let prJson;
2200
+ try {
2201
+ prJson = execSync("gh pr view --json number,url,state", {
2202
+ cwd,
2203
+ encoding: "utf8",
2204
+ stdio: ["pipe", "pipe", "pipe"],
2205
+ timeout: 15000
2206
+ }).trim();
2207
+ } catch {
2208
+ return deny(`No pull request found for branch "${branch}". ` + `Run now: gh pr create`);
2209
+ }
2210
+ const pr = JSON.parse(prJson);
2211
+ if (pr.state === "OPEN") {
2212
+ return allow(`PR #${pr.number} exists: ${pr.url}`);
2213
+ }
2214
+ if (pr.state === "MERGED") {
2215
+ return allow(`PR #${pr.number} was merged: ${pr.url}. ` + `Switch off this branch (e.g. 'git checkout ${baseBranch} && git pull') before stopping again.`);
2216
+ }
2217
+ return deny(`Pull request for branch "${branch}" is ${pr.state.toLowerCase()}. Run now: gh pr create`);
2218
+ } catch {
2219
+ return allow("Could not check PR status, skipping.");
2220
+ }
2221
+ }
2222
+ function requireNoConflictsBeforeStop(ctx) {
2223
+ if (isPlanMode(ctx))
2224
+ return allow("Plan mode — no changes made, skipping conflict check.");
2225
+ const cwd = ctx.session?.cwd;
2226
+ if (!cwd)
2227
+ return allow("No working directory available, skipping conflict check.");
2228
+ const branch = getCurrentBranch(cwd);
2229
+ if (!branch || branch === "HEAD")
2230
+ return allow("Detached HEAD, skipping conflict check.");
2231
+ const baseBranch = ctx.params?.baseBranch ?? "main";
2232
+ if (branch === baseBranch) {
2233
+ return allow(`On base branch "${baseBranch}", skipping conflict check.`);
2234
+ }
2235
+ try {
2236
+ execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
2237
+ } catch {
2238
+ return allow("gh CLI not installed, skipping conflict check.");
2239
+ }
2240
+ let prJson;
2241
+ try {
2242
+ prJson = execSync("gh pr view --json mergeable,number,url,state", {
2243
+ cwd,
2244
+ encoding: "utf8",
2245
+ stdio: ["pipe", "pipe", "pipe"],
2246
+ timeout: 15000
2247
+ }).trim();
2248
+ } catch {
2249
+ return allow("No pull request found for branch, skipping conflict check.");
2250
+ }
2251
+ let pr;
2252
+ try {
2253
+ pr = JSON.parse(prJson);
2254
+ } catch {
2255
+ return allow("Could not parse gh pr view output, skipping conflict check.");
2256
+ }
2257
+ if (pr.state !== "OPEN") {
2258
+ return allow(`PR #${pr.number} is ${pr.state.toLowerCase()}; skipping conflict check.`);
2259
+ }
2260
+ try {
2261
+ execFileSync("git", ["rev-parse", "--verify", `origin/${baseBranch}`], {
2262
+ cwd,
2263
+ encoding: "utf8",
2264
+ stdio: ["pipe", "pipe", "pipe"],
2265
+ timeout: 3000
2266
+ });
2267
+ const ahead = execFileSync("git", ["log", `origin/${baseBranch}..HEAD`, "--oneline"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
2268
+ if (ahead) {
2269
+ execFileSync("git", ["merge-tree", "--write-tree", "--name-only", `origin/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 1e4 });
2270
+ }
2271
+ } catch (err) {
2272
+ const e = err;
2273
+ if (e.status === 1) {
2274
+ const out = (typeof e.stdout === "string" ? e.stdout : e.stdout?.toString("utf8") ?? "").trim();
2275
+ const lines = out.split(`
2276
+ `);
2277
+ const files = [];
2278
+ for (let i = 1;i < lines.length; i++) {
2279
+ const line = lines[i];
2280
+ if (line === "")
2281
+ break;
2282
+ files.push(line);
2283
+ }
2284
+ const fileList = files.length ? files.join(", ") : "one or more files";
2285
+ return deny(`Branch "${branch}" has merge conflicts with ${baseBranch} in: ${fileList}. ` + `Rebase or merge origin/${baseBranch} now and resolve the conflicts.`);
2286
+ }
2287
+ }
2288
+ if (pr.mergeable === "CONFLICTING") {
2289
+ return deny(`PR #${pr.number} has merge conflicts per GitHub (${pr.url}). ` + `Rebase or merge origin/${baseBranch} now and resolve the conflicts.`);
2290
+ }
2291
+ if (pr.mergeable === "UNKNOWN") {
2292
+ return deny(`GitHub is still computing mergeability for PR #${pr.number} (${pr.url}). ` + `Wait ~10 seconds, then re-check with \`gh pr view --json mergeable\` before attempting to stop again.`);
2293
+ }
2294
+ return allow(`PR #${pr.number} merges cleanly per GitHub.`);
2295
+ }
2296
+ function requireCiGreenBeforeStop(ctx) {
2297
+ if (isPlanMode(ctx))
2298
+ return allow("Plan mode — no changes made, skipping CI check.");
2299
+ const cwd = ctx.session?.cwd;
2300
+ if (!cwd)
2301
+ return allow("No working directory available, skipping CI check.");
2302
+ try {
2303
+ try {
2304
+ execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
2305
+ } catch {
2306
+ return allow("GitHub CLI (gh) not installed, skipping CI check.");
2307
+ }
2308
+ const branch = getCurrentBranch(cwd);
2309
+ if (!branch || branch === "HEAD")
2310
+ return allow("Detached HEAD, skipping CI check.");
2311
+ const sha = getHeadSha(cwd);
2312
+ let workflowRuns = [];
2313
+ try {
2314
+ const runsJson = execFileSync("gh", ["run", "list", "--branch", branch, "--limit", "20", "--json", "status,conclusion,name,headSha"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 }).trim();
2315
+ if (runsJson && runsJson !== "[]") {
2316
+ const allWorkflowRuns = JSON.parse(runsJson);
2317
+ const headRuns = sha ? allWorkflowRuns.filter((r) => r.headSha === sha) : allWorkflowRuns;
2318
+ const seen = new Set;
2319
+ workflowRuns = headRuns.filter((r) => {
2320
+ if (seen.has(r.name))
2321
+ return false;
2322
+ seen.add(r.name);
2323
+ return true;
2324
+ });
2325
+ }
2326
+ } catch {}
2327
+ let thirdPartyChecks = [];
2328
+ let commitStatuses = [];
2329
+ if (sha) {
2330
+ thirdPartyChecks = getThirdPartyCheckRuns(cwd, sha);
2331
+ commitStatuses = getCommitStatuses(cwd, sha);
2332
+ }
2333
+ const allChecks = [...workflowRuns, ...thirdPartyChecks, ...commitStatuses];
2334
+ if (allChecks.length === 0)
2335
+ return allow(`No CI runs found for branch "${branch}".`);
2336
+ const failing = allChecks.filter((r) => r.status === "completed" && r.conclusion !== "success" && r.conclusion !== "skipped" && r.conclusion !== "cancelled" && r.conclusion !== "neutral");
2337
+ if (failing.length > 0) {
2338
+ const names = failing.map((r) => `"${r.name}"`).join(", ");
2339
+ return deny(`CI checks are failing on branch "${branch}": ${names}. Fix the failing checks now.`);
2340
+ }
2341
+ const pending = allChecks.filter((r) => r.status === "in_progress" || r.status === "queued" || r.status === "waiting");
2342
+ if (pending.length > 0) {
2343
+ const names = pending.map((r) => `"${r.name}"`).join(", ");
2344
+ return deny(`CI checks are still running on branch "${branch}": ${names}. Wait for all checks to complete, then verify they pass.`);
2345
+ }
2346
+ return allow(`All CI checks passed on branch "${branch}".`);
2347
+ } catch {
2348
+ return allow("Could not check CI status, skipping.");
2349
+ }
2350
+ }
2351
+ function assertCatalogBijection() {
2352
+ const implNames = new Set(Object.keys(POLICY_IMPLEMENTATIONS));
2353
+ const missing = POLICY_CATALOG.filter((e) => !implNames.has(e.name)).map((e) => e.name);
2354
+ if (missing.length > 0) {
2355
+ throw new Error(`failproofai: builtin policies missing an implementation: ${missing.join(", ")}`);
2356
+ }
2357
+ const catalogNames = new Set(POLICY_CATALOG.map((e) => e.name));
2358
+ const orphaned = [...implNames].filter((n) => !catalogNames.has(n));
2359
+ if (orphaned.length > 0) {
2360
+ throw new Error(`failproofai: policy implementations with no catalog entry: ${orphaned.join(", ")}`);
2361
+ }
2362
+ }
2363
+ function registerBuiltinPolicies(enabledNames) {
2364
+ const enabledSet = new Set(enabledNames.map(normalizePolicyName));
2365
+ for (const policy of BUILTIN_POLICIES) {
2366
+ if (policy.alwaysOn || enabledSet.has(normalizePolicyName(policy.name))) {
2367
+ registerPolicy(policy.name, policy.description, policy.fn, policy.match, 0, policy.params);
2520
2368
  }
2369
+ }
2370
+ }
2371
+ var isClaudeInternalPath, isClaudeSettingsFile, SHELL_OPERATORS, SHELL_METACHAR_RE, JWT_RE, API_KEY_PATTERNS, CONNECTION_STRING_RE, PRIVATE_KEY_RE, BEARER_TOKEN_RE, SECRET_PATTERNS, 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, PS_ELEVATION_RE, RUNAS_RE, CURL_PIPE_SH_RE, SEGMENT_SEPARATORS, COMMAND_PREFIX_TOKENS, SELF_BINARY_TOKEN_RE, SELF_ENTRY_PATH_RE, CONFIG_SUBCOMMAND_RE, PAUSE_FLAG_RE, ENV_ASSIGNMENT_RE, RUNNER_OPERAND_RE, PS_WEB_PIPE_RE, SHORT_FLAG_BUNDLE_RE, SAFE_FORCE_PREFIXES, SECRET_FILE_RE, SECRET_FILE_ID_RSA_RE, SECRET_FILE_CREDENTIALS_RE, GIT_COMMIT_MERGE_RE, FAILPROOFAI_STATE_WRITE_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, GIT_BRANCH_CACHE_MAX_ENTRIES = 500, EVAL_FLAG_RE, HOME_PREFIX_RE, RM_CMD_RE, FIND_CMD_RE, FIND_EXEC_RE, FIND_GLOBAL_OPT_RE, FIND_EXPR_START_RE, SCRATCH_ROOTS, CATASTROPHIC_DEPTH = 2, READ_LIKE_CMDS, TOOL_CALL_TRACKER_MAX_BYTES = 65536, SEGMENT_SPLIT_RE, POLICY_IMPLEMENTATIONS, BUILTIN_POLICIES;
2372
+ var init_builtin_policies = __esm(() => {
2373
+ init_policy_catalog();
2374
+ init_hook_logger();
2375
+ isClaudeInternalPath = isAgentInternalPath;
2376
+ isClaudeSettingsFile = isAgentSettingsFile;
2377
+ SHELL_OPERATORS = new Set(["&&", "||", "|", ";"]);
2378
+ SHELL_METACHAR_RE = /[;&<>`$()\\]/;
2379
+ JWT_RE = /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/;
2380
+ API_KEY_PATTERNS = [
2381
+ [/sk-ant-[A-Za-z0-9\-_]{20,}/, "Anthropic API key"],
2382
+ [/sk-proj-[A-Za-z0-9\-_]{20,}/, "OpenAI project API key"],
2383
+ [/sk-[A-Za-z0-9]{20,}/, "OpenAI API key"],
2384
+ [/ghp_[A-Za-z0-9]{36}/, "GitHub personal access token"],
2385
+ [/github_pat_[A-Za-z0-9_]{82}/, "GitHub fine-grained token"],
2386
+ [/AKIA[A-Z0-9]{16}/, "AWS access key ID"],
2387
+ [/sk_live_[A-Za-z0-9]{24,}/, "Stripe live secret key"],
2388
+ [/sk_test_[A-Za-z0-9]{24,}/, "Stripe test secret key"],
2389
+ [/AIza[0-9A-Za-z\-_]{35}/, "Google API key"]
2390
+ ];
2391
+ CONNECTION_STRING_RE = /(?:postgresql|postgres|mysql|mongodb(?:\+srv)?|redis|amqps?|smtps?):\/\/[^@\s]+@/;
2392
+ PRIVATE_KEY_RE = /-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----/;
2393
+ BEARER_TOKEN_RE = /Authorization:\s*Bearer\s+[A-Za-z0-9\-._~+/]{20,}/i;
2394
+ SECRET_PATTERNS = [
2395
+ [PRIVATE_KEY_RE, "private key"],
2396
+ [JWT_RE, "JWT"],
2397
+ [BEARER_TOKEN_RE, "bearer token"],
2398
+ [CONNECTION_STRING_RE, "database credentials"],
2399
+ ...API_KEY_PATTERNS
2521
2400
  ];
2401
+ SQL_TOOL_RE = /\b(?:psql|mysql|sqlite3|pgcli|clickhouse-client)\b/;
2402
+ DESTRUCTIVE_SQL_RE = /\b(?:DROP\s+(?:TABLE|DATABASE|SCHEMA)|TRUNCATE\b)/i;
2403
+ DELETE_NO_WHERE_RE = /\bDELETE\s+FROM\b/i;
2404
+ SQL_WHERE_RE = /\bWHERE\b/i;
2405
+ SCHEMA_ALTER_RE = /\bALTER\s+TABLE\b[\s\S]*\b(?:DROP\s+COLUMN|ADD\s+COLUMN|RENAME\s+(?:COLUMN|TO)|MODIFY\s+COLUMN)\b/i;
2406
+ PUBLISH_CMD_RE = /(?:npm\s+publish|bun\s+publish|pnpm\s+publish|yarn\s+npm\s+publish|twine\s+upload|poetry\s+publish|cargo\s+publish|gem\s+push)\b/;
2407
+ ENV_PRINTENV_RE = /(?:^|\s|;|&&|\|\|)(?:env|printenv)(?:\s|$|;|&&|\|)/;
2408
+ ECHO_ENV_RE = /echo\s+.*\$\{?[A-Za-z_]/;
2409
+ EXPORT_RE = /(?:^|\s|;|&&|\|\|)export\s+\w+/;
2410
+ PS_ENV_VAR_RE = /\$env:[A-Za-z_]/i;
2411
+ PS_CHILDITEM_ENV_RE = /(?:Get-ChildItem|dir|gci|ls)\s+Env:/i;
2412
+ DOTNET_GETENV_RE = /\[Environment\]::GetEnvironment/i;
2413
+ CMD_ECHO_ENV_RE = /echo\s+%[A-Za-z_]/i;
2414
+ ENV_FILE_PATH_RE = /(?:^|[\\/])\.env(?:\.|$)/;
2415
+ ENV_CMD_RE = /\.env(?:\b|\s|$|\.)/;
2416
+ PS_ELEVATION_RE = /Start-Process\s+.*-Verb\s+RunAs/i;
2417
+ RUNAS_RE = /(?:^|;|&&|\|\|)\s*runas\s/i;
2418
+ CURL_PIPE_SH_RE = /(?:curl|wget)\s.*\|\s*(?:sh|bash|zsh|dash|ksh|csh|tcsh|fish|ash)\b/;
2419
+ SEGMENT_SEPARATORS = /[;&|\n\r(){}`]+/;
2420
+ COMMAND_PREFIX_TOKENS = new Set([
2421
+ "npx",
2422
+ "bunx",
2423
+ "pnpx",
2424
+ "npm",
2425
+ "pnpm",
2426
+ "yarn",
2427
+ "dlx",
2428
+ "exec",
2429
+ "run",
2430
+ "eval",
2431
+ "node",
2432
+ "bun",
2433
+ "deno",
2434
+ "env",
2435
+ "command",
2436
+ "builtin",
2437
+ "nohup",
2438
+ "setsid",
2439
+ "time",
2440
+ "timeout",
2441
+ "nice",
2442
+ "stdbuf",
2443
+ "xargs",
2444
+ "sudo",
2445
+ "doas",
2446
+ "sh",
2447
+ "bash",
2448
+ "zsh",
2449
+ "dash",
2450
+ "ksh",
2451
+ "fish",
2452
+ "ash"
2453
+ ]);
2454
+ SELF_BINARY_TOKEN_RE = /(?:^|\/)failproofai[^/]*$/;
2455
+ SELF_ENTRY_PATH_RE = /failproofai\/(?:dist\/(?:cli|index)\.mjs|bin\/failproofai\.mjs)$/;
2456
+ CONFIG_SUBCOMMAND_RE = /^(?:config|configure|setup)$/;
2457
+ PAUSE_FLAG_RE = /^--pause(?:=|$)/;
2458
+ ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
2459
+ RUNNER_OPERAND_RE = /^\d+[a-z]*$/i;
2460
+ PS_WEB_PIPE_RE = /(?:Invoke-WebRequest|iwr|Invoke-RestMethod|irm)\s+.*\|\s*(?:Invoke-Expression|iex)/i;
2461
+ SHORT_FLAG_BUNDLE_RE = /^-[a-zA-Z]*f[a-zA-Z]*$/;
2462
+ SAFE_FORCE_PREFIXES = ["--force-with-lease", "--force-if-includes"];
2463
+ SECRET_FILE_RE = /\.(?:pem|key)$/;
2464
+ SECRET_FILE_ID_RSA_RE = /id_rsa/;
2465
+ SECRET_FILE_CREDENTIALS_RE = /credentials/;
2466
+ GIT_COMMIT_MERGE_RE = /git\s+(commit|merge|rebase|cherry-pick)\b/;
2467
+ FAILPROOFAI_STATE_WRITE_RE = /\b(?:rm|unlink|shred|mv|truncate)\b[^;&|]*\.failproofai(?:\/|\b)/;
2468
+ FAILPROOFAI_UNINSTALL_RE = /(?:npm\s+(?:uninstall|remove|un|r)\s.*failproofai|bun\s+remove\s.*failproofai|yarn\s+global\s+remove\s+failproofai|pnpm\s+(?:remove|uninstall|un)\s.*failproofai)/;
2469
+ GIT_AMEND_RE = /\bgit\s+commit\b.*--amend\b/;
2470
+ GIT_STASH_DROP_RE = /\bgit\s+stash\s+(?:drop|clear)\b/;
2471
+ GIT_ADD_ALL_RE = /\bgit\s+add\s+(?:-A\b|--all\b|\.(?:\s|$|;|&&|\|\|))/;
2472
+ NPM_GLOBAL_RE = /\bnpm\s+(?:install|i)\b(?=.*(?:\s-g\b|--global\b))/;
2473
+ YARN_GLOBAL_RE = /\byarn\s+global\s+add\b/;
2474
+ PNPM_GLOBAL_RE = /\bpnpm\s+(?:add|install|i)\b(?=.*(?:\s-g\b|--global\b))/;
2475
+ BUN_GLOBAL_RE = /\bbun\s+(?:install|add)\b(?=.*(?:\s-g\b|--global\b))/;
2476
+ CARGO_INSTALL_RE = /\bcargo\s+install\b/;
2477
+ PIP_SYSTEM_RE = /\bpip(?:3)?\s+install\b(?=.*(?:--user\b|--break-system-packages\b))/;
2478
+ PKG_MANAGER_DETECTORS = {
2479
+ pip: [/\bpip\b/, /\bpip3\b/, /\bpython3?\s+-m\s+pip\b/],
2480
+ npm: [/\bnpm\b/, /\bnpx\b/],
2481
+ yarn: [/\byarn\b/],
2482
+ pnpm: [/\bpnpm\b/, /\bpnpx\b/],
2483
+ bun: [/\bbun\b/, /\bbunx\b/],
2484
+ uv: [/\buv\b/],
2485
+ poetry: [/\bpoetry\b/],
2486
+ pipenv: [/\bpipenv\b/],
2487
+ conda: [/\bconda\b/],
2488
+ cargo: [/\bcargo\b/]
2489
+ };
2490
+ NOHUP_RE = /\bnohup\s+\S/;
2491
+ SCREEN_DETACH_RE = /\bscreen\s+-[A-Za-z]*d[A-Za-z]*\b/;
2492
+ TMUX_DETACH_RE = /\btmux\s+(?:new-session|new)\b[^|&;]*-d\b/;
2493
+ DISOWN_RE = /\bdisown\b/;
2494
+ BACKGROUND_AMPERSAND_RE = /(?<![&|])\s?&\s*(?:$|#|;)/;
2495
+ KUBECTL_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*kubectl(?:\s|$)/;
2496
+ TERRAFORM_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*(?:terraform|tofu)(?:\s|$)/;
2497
+ AWS_CLI_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*aws(?:\s|$)/;
2498
+ GCLOUD_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*gcloud(?:\s|$)/;
2499
+ AZ_CLI_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*az(?:\s|$)/;
2500
+ HELM_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*helm(?:\s|$)/;
2501
+ GH_PIPELINE_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*gh\s+(?:workflow\s+(?:run|enable|disable)|run\s+(?:rerun|cancel)|pr\s+merge|release\s+(?:create|delete)|cache\s+delete|secret\s+(?:set|delete))\b/;
2502
+ gitBranchCache = new Map;
2503
+ EVAL_FLAG_RE = /^-{1,2}c$/;
2504
+ HOME_PREFIX_RE = /^(?:~[A-Za-z0-9_.-]*|\$HOME|\$\{HOME\})(?=$|\/)/;
2505
+ RM_CMD_RE = /^(?:\/\S*\/)?rm$/;
2506
+ FIND_CMD_RE = /^(?:\/\S*\/)?find$/;
2507
+ FIND_EXEC_RE = /^-(?:exec|execdir|ok|okdir)$/;
2508
+ FIND_GLOBAL_OPT_RE = /^-(?:[HLP]|D|O\d*)$/;
2509
+ FIND_EXPR_START_RE = /^(?:-|\\?[(!])/;
2510
+ SCRATCH_ROOTS = ["/tmp", "/var/tmp"];
2511
+ READ_LIKE_CMDS = /(?:^|;|&&|\|\||\|)\s*(?:ls|find|cat|head|tail|less|more|wc|file|stat|tree|du)\s/;
2512
+ SEGMENT_SPLIT_RE = /\s*(?:&&|\|\||\||;)\s*/;
2513
+ POLICY_IMPLEMENTATIONS = {
2514
+ "sanitize-jwt": sanitizeJwt,
2515
+ "sanitize-api-keys": sanitizeApiKeys,
2516
+ "sanitize-connection-strings": sanitizeConnectionStrings,
2517
+ "sanitize-private-key-content": sanitizePrivateKeyContent,
2518
+ "sanitize-bearer-tokens": sanitizeBearerTokens,
2519
+ "protect-env-vars": protectEnvVars,
2520
+ "block-env-files": blockEnvFiles,
2521
+ "block-read-outside-cwd": blockReadOutsideCwd,
2522
+ "block-sudo": blockSudo,
2523
+ "block-curl-pipe-sh": blockCurlPipeSh,
2524
+ "block-rm-rf": blockRmRf,
2525
+ "block-failproofai-commands": blockFailproofaiCommands,
2526
+ "block-kubectl": blockKubectl,
2527
+ "block-terraform": blockTerraform,
2528
+ "block-aws-cli": blockAwsCli,
2529
+ "block-gcloud": blockGcloud,
2530
+ "block-az-cli": blockAzCli,
2531
+ "block-helm": blockHelm,
2532
+ "block-gh-pipeline": blockGhPipeline,
2533
+ "block-secrets-write": blockSecretsWrite,
2534
+ "block-push-master": blockPushMaster,
2535
+ "block-force-push": blockForcePush,
2536
+ "block-work-on-main": blockWorkOnMain,
2537
+ "warn-git-amend": warnGitAmend,
2538
+ "warn-git-stash-drop": warnGitStashDrop,
2539
+ "warn-all-files-staged": warnAllFilesStaged,
2540
+ "warn-destructive-sql": warnDestructiveSql,
2541
+ "warn-schema-alteration": warnSchemaAlteration,
2542
+ "warn-package-publish": warnPackagePublish,
2543
+ "warn-global-package-install": warnGlobalPackageInstall,
2544
+ "prefer-package-manager": preferPackageManager,
2545
+ "warn-large-file-write": warnLargeFileWrite,
2546
+ "warn-background-process": warnBackgroundProcess,
2547
+ "warn-repeated-tool-calls": warnRepeatedToolCalls,
2548
+ "require-commit-before-stop": requireCommitBeforeStop,
2549
+ "require-push-before-stop": requirePushBeforeStop,
2550
+ "require-pr-before-stop": requirePrBeforeStop,
2551
+ "require-no-conflicts-before-stop": requireNoConflictsBeforeStop,
2552
+ "require-ci-green-before-stop": requireCiGreenBeforeStop
2553
+ };
2554
+ assertCatalogBijection();
2555
+ BUILTIN_POLICIES = POLICY_CATALOG.map((entry) => ({
2556
+ ...entry,
2557
+ fn: POLICY_IMPLEMENTATIONS[entry.name]
2558
+ }));
2522
2559
  });
2523
2560
 
2524
2561
  // package.json
2525
- var version = "1.0.1";
2562
+ var version = "1.0.2-beta.1";
2526
2563
  var init_package = () => {};
2527
2564
 
2528
2565
  // src/posthog-key.ts
@@ -2864,17 +2901,17 @@ async function evaluatePolicies(eventType, payload, session, config) {
2864
2901
  const instructEntries = [];
2865
2902
  const allowEntries = [];
2866
2903
  for (const policy of policies) {
2867
- const schema = POLICY_PARAMS_MAP.get(policy.name);
2904
+ const schema = policy.params;
2905
+ const userParams = getConfigParamsFor(config, policy.name) ?? {};
2868
2906
  let ctx;
2869
2907
  if (schema) {
2870
- const userParams = getConfigParamsFor(config, policy.name) ?? {};
2871
2908
  const resolvedParams = {};
2872
2909
  for (const [key, spec] of Object.entries(schema)) {
2873
2910
  resolvedParams[key] = key in userParams ? userParams[key] : spec.default;
2874
2911
  }
2875
2912
  ctx = { ...baseCtx, params: resolvedParams };
2876
2913
  } else {
2877
- ctx = { ...baseCtx, params: {} };
2914
+ ctx = { ...baseCtx, params: userParams };
2878
2915
  }
2879
2916
  let result;
2880
2917
  try {
@@ -2882,8 +2919,8 @@ async function evaluatePolicies(eventType, payload, session, config) {
2882
2919
  } catch (err) {
2883
2920
  const msg = err instanceof Error ? err.message : String(err);
2884
2921
  hookLogWarn(`policy "${policy.name}" threw: ${msg}`);
2885
- const isCustom = policy.name.startsWith("custom/") || policy.name.startsWith(".failproofai-");
2886
- if (!isCustom) {
2922
+ const isBuiltin = policy.name.startsWith(`${DEFAULT_POLICY_NAMESPACE}/`);
2923
+ if (isBuiltin) {
2887
2924
  trackHookEvent(getInstanceId(), "policy_evaluation_error", {
2888
2925
  policy_name: policy.name,
2889
2926
  event_type: eventType,
@@ -3560,13 +3597,10 @@ You MUST complete the above action(s) NOW. Do NOT ask the user for confirmation
3560
3597
  }
3561
3598
  return { exitCode: 0, stdout: "", stderr: "", policyName: null, reason: null, decision: "allow" };
3562
3599
  }
3563
- var POLICY_PARAMS_MAP;
3564
3600
  var init_policy_evaluator = __esm(() => {
3565
- init_builtin_policies();
3566
3601
  init_hook_logger();
3567
3602
  init_hook_telemetry();
3568
3603
  init_telemetry_id();
3569
- POLICY_PARAMS_MAP = new Map(BUILTIN_POLICIES.filter((p) => p.params).map((p) => [normalizePolicyName(p.name), p.params]));
3570
3604
  });
3571
3605
 
3572
3606
  // src/hooks/custom-hooks-registry.ts
@@ -3836,7 +3870,7 @@ async function loadSingleFile(absPath, opts) {
3836
3870
  if (cached?.fingerprint === fingerprint) {
3837
3871
  for (const hook of cached.hooks)
3838
3872
  customPolicies.add({ ...hook });
3839
- return;
3873
+ return null;
3840
3874
  }
3841
3875
  const entryTmp = absPath + tmpSuffix;
3842
3876
  const fileUrl = pathToFileURL2(entryTmp).href;
@@ -3848,6 +3882,7 @@ async function loadSingleFile(absPath, opts) {
3848
3882
  fingerprint,
3849
3883
  hooks: getCustomHooks().slice(hooksBefore).map((hook) => ({ ...hook }))
3850
3884
  });
3885
+ return null;
3851
3886
  } catch (err) {
3852
3887
  const msg = err instanceof Error ? err.message : String(err);
3853
3888
  const errorType = /Cannot find module|MODULE_NOT_FOUND|ENOENT/i.test(msg) ? "module_not_found" : /SyntaxError|Unexpected token/i.test(msg) ? "syntax_error" : /timed out loading module/i.test(msg) ? "load_timeout" : "runtime_error";
@@ -3860,6 +3895,7 @@ async function loadSingleFile(absPath, opts) {
3860
3895
  if (opts?.strict)
3861
3896
  throw new Error(`Failed to load custom hooks from ${absPath}: ${msg}`);
3862
3897
  hookLogError(`failed to load custom hooks from ${absPath}: ${msg}`);
3898
+ return { type: errorType, reason: msg };
3863
3899
  } finally {
3864
3900
  g[LOADING_KEY] = false;
3865
3901
  await cleanupTmpFiles(tmpFiles);
@@ -3887,9 +3923,24 @@ function warnSkippedPolicyFiles(dir, scope) {
3887
3923
  const suggest = (n) => n.replace(/\.(js|mjs|ts)$/, "-policies.$1");
3888
3924
  hookLogWarn(`${scope} policies: ${skipped.length} file(s) in ${dir} are NOT loaded — ` + `the convention only loads names ending in "policies.js|mjs|ts". ` + skipped.map((n) => `${n} → rename to ${suggest(n)}`).join("; "));
3889
3925
  }
3926
+ function reconcilePackManifest(pack, loaded) {
3927
+ if (!pack || pack.policies.length === 0)
3928
+ return;
3929
+ const declared = new Set(pack.policies.map((p) => p.name));
3930
+ const registered = new Set(loaded.map((h) => h.name));
3931
+ const missing = [...declared].filter((n) => !registered.has(n));
3932
+ const extra = [...registered].filter((n) => !declared.has(n));
3933
+ if (missing.length > 0) {
3934
+ hookLogWarn(`pack ${pack.id}@${pack.version} declares ${missing.join(", ")} but its artifact does not ` + `register ${missing.length === 1 ? "it" : "them"} — listed as protection that never runs`);
3935
+ }
3936
+ if (extra.length > 0) {
3937
+ hookLogWarn(`pack ${pack.id}@${pack.version} registers undeclared ${extra.join(", ")} — ` + `${extra.length === 1 ? "it enforces" : "they enforce"} but will not appear in listings`);
3938
+ }
3939
+ }
3890
3940
  async function loadAllCustomHooks(customPoliciesPaths, opts) {
3891
3941
  clearCustomHooks();
3892
3942
  const conventionSources = [];
3943
+ const packFailures = new Map;
3893
3944
  const projectRoot = findProjectConfigDir(opts?.sessionCwd ?? process.cwd());
3894
3945
  const conventionEnabled = opts?.customPoliciesEnabled !== false;
3895
3946
  const loadedPaths = new Set;
@@ -3906,28 +3957,50 @@ async function loadAllCustomHooks(customPoliciesPaths, opts) {
3906
3957
  cloudManagedByPath.set(key, policy);
3907
3958
  }
3908
3959
  }
3960
+ const packByPath = new Map;
3961
+ for (const pack of opts?.packs ?? []) {
3962
+ const key = resolve5(pack.path);
3963
+ const existing = packByPath.get(key);
3964
+ if (!existing) {
3965
+ packByPath.set(key, pack);
3966
+ continue;
3967
+ }
3968
+ hookLogWarn(`packs ${existing.id} and ${pack.id} have identical source, so they share one artifact ` + `and load as one pack; enforcing it if either asks to enforce`);
3969
+ if (existing.effect !== "enforce" && pack.effect === "enforce") {
3970
+ packByPath.set(key, pack);
3971
+ }
3972
+ }
3909
3973
  for (const customPoliciesPath of typeof customPoliciesPaths === "string" ? [customPoliciesPaths] : customPoliciesPaths ?? []) {
3910
3974
  const absPath = resolve5(projectRoot, customPoliciesPath);
3975
+ const cloudManaged = cloudManagedByPath.get(absPath);
3976
+ const pack = packByPath.get(absPath);
3911
3977
  if (existsSync4(absPath)) {
3912
3978
  if (!loadedPaths.has(absPath)) {
3913
3979
  loadedPaths.add(absPath);
3914
3980
  const hooksBefore = getCustomHooks().length;
3915
- await loadSingleFile(absPath, {
3916
- verifyEntrySha: cloudManagedByPath.get(absPath)?.sha256
3981
+ const failure = await loadSingleFile(absPath, {
3982
+ verifyEntrySha: cloudManaged?.sha256 ?? pack?.sha256
3917
3983
  });
3984
+ if (failure && pack)
3985
+ packFailures.set(pack.id, failure);
3918
3986
  for (const hook of getCustomHooks().slice(hooksBefore)) {
3919
- const cloudManaged = cloudManagedByPath.get(absPath);
3920
3987
  const tagged = hook;
3921
3988
  if (cloudManaged) {
3922
3989
  tagged.__cloudManaged = cloudManaged;
3923
3990
  tagged.__policyId = `cloud:${cloudManaged.id}@${cloudManaged.version}:${hook.name}`;
3991
+ } else if (pack) {
3992
+ tagged.__pack = pack;
3993
+ tagged.__policyId = `pack:${pack.id}@${pack.version}:${hook.name}`;
3924
3994
  } else {
3925
3995
  tagged.__policyId = customPolicyId(absPath, hook.name);
3926
3996
  }
3927
3997
  }
3998
+ reconcilePackManifest(pack, getCustomHooks().slice(hooksBefore));
3928
3999
  }
3929
4000
  } else {
3930
4001
  hookLogWarn(`custom policy path not found: ${absPath}`);
4002
+ if (pack)
4003
+ packFailures.set(pack.id, { type: "path_missing", reason: `path missing: ${absPath}` });
3931
4004
  }
3932
4005
  }
3933
4006
  const hooksBeforeConvention = getCustomHooks().length;
@@ -3999,7 +4072,7 @@ async function loadAllCustomHooks(customPoliciesPaths, opts) {
3999
4072
  hook.__conventionScope = hookNameToScope.get(hook.name) ?? "project";
4000
4073
  }
4001
4074
  }
4002
- return { hooks: allHooks, conventionSources };
4075
+ return { hooks: allHooks, conventionSources, packFailures };
4003
4076
  }
4004
4077
  var LOADING_KEY = "__FAILPROOFAI_LOADING_HOOKS__", CONVENTION_FILE_RE, loadSequence = 0, policyModuleCache, POLICY_MODULE_CACHE_MAX_ENTRIES = 500, LOADABLE_EXT_RE;
4005
4078
  var init_custom_hooks_loader = __esm(() => {
@@ -4285,12 +4358,12 @@ function formatDate(date) {
4285
4358
  // lib/codex-projects.ts
4286
4359
  var exports_codex_projects = {};
4287
4360
  __export(exports_codex_projects, {
4288
- getCodexSessionsForCwd: () => getCodexSessionsForCwd,
4289
- getCodexSessionsByEncodedName: () => getCodexSessionsByEncodedName,
4290
- getCodexProjects: () => getCodexProjects,
4291
- getCachedCodexSessionsForCwd: () => getCachedCodexSessionsForCwd,
4361
+ getCachedCodexProjects: () => getCachedCodexProjects,
4292
4362
  getCachedCodexSessionsByEncodedName: () => getCachedCodexSessionsByEncodedName,
4293
- getCachedCodexProjects: () => getCachedCodexProjects
4363
+ getCachedCodexSessionsForCwd: () => getCachedCodexSessionsForCwd,
4364
+ getCodexProjects: () => getCodexProjects,
4365
+ getCodexSessionsByEncodedName: () => getCodexSessionsByEncodedName,
4366
+ getCodexSessionsForCwd: () => getCodexSessionsForCwd
4294
4367
  });
4295
4368
  import { open, readdir } from "node:fs/promises";
4296
4369
  import { homedir as homedir5 } from "node:os";
@@ -4484,12 +4557,12 @@ var init_codex_projects = __esm(() => {
4484
4557
  // lib/copilot-projects.ts
4485
4558
  var exports_copilot_projects = {};
4486
4559
  __export(exports_copilot_projects, {
4487
- getCopilotSessionsForCwd: () => getCopilotSessionsForCwd,
4488
- getCopilotSessionsByEncodedName: () => getCopilotSessionsByEncodedName,
4489
- getCopilotProjects: () => getCopilotProjects,
4490
- getCachedCopilotSessionsForCwd: () => getCachedCopilotSessionsForCwd,
4560
+ getCachedCopilotProjects: () => getCachedCopilotProjects,
4491
4561
  getCachedCopilotSessionsByEncodedName: () => getCachedCopilotSessionsByEncodedName,
4492
- getCachedCopilotProjects: () => getCachedCopilotProjects
4562
+ getCachedCopilotSessionsForCwd: () => getCachedCopilotSessionsForCwd,
4563
+ getCopilotProjects: () => getCopilotProjects,
4564
+ getCopilotSessionsByEncodedName: () => getCopilotSessionsByEncodedName,
4565
+ getCopilotSessionsForCwd: () => getCopilotSessionsForCwd
4493
4566
  });
4494
4567
  import { readdir as readdir2, readFile as readFile4, stat } from "node:fs/promises";
4495
4568
  import { homedir as homedir6 } from "node:os";
@@ -4632,12 +4705,12 @@ var init_copilot_projects = __esm(() => {
4632
4705
  // lib/cursor-projects.ts
4633
4706
  var exports_cursor_projects = {};
4634
4707
  __export(exports_cursor_projects, {
4635
- getCursorSessionsForCwd: () => getCursorSessionsForCwd,
4636
- getCursorSessionsByEncodedName: () => getCursorSessionsByEncodedName,
4637
- getCursorProjects: () => getCursorProjects,
4638
- getCachedCursorSessionsForCwd: () => getCachedCursorSessionsForCwd,
4708
+ getCachedCursorProjects: () => getCachedCursorProjects,
4639
4709
  getCachedCursorSessionsByEncodedName: () => getCachedCursorSessionsByEncodedName,
4640
- getCachedCursorProjects: () => getCachedCursorProjects
4710
+ getCachedCursorSessionsForCwd: () => getCachedCursorSessionsForCwd,
4711
+ getCursorProjects: () => getCursorProjects,
4712
+ getCursorSessionsByEncodedName: () => getCursorSessionsByEncodedName,
4713
+ getCursorSessionsForCwd: () => getCursorSessionsForCwd
4641
4714
  });
4642
4715
  import { readdir as readdir3, readFile as readFile5, stat as stat2 } from "node:fs/promises";
4643
4716
  import { homedir as homedir7 } from "node:os";
@@ -4856,12 +4929,12 @@ var init_cursor_projects = __esm(() => {
4856
4929
  // lib/opencode-projects.ts
4857
4930
  var exports_opencode_projects = {};
4858
4931
  __export(exports_opencode_projects, {
4859
- getOpenCodeSessionsForCwd: () => getOpenCodeSessionsForCwd,
4860
- getOpenCodeSessionsByEncodedName: () => getOpenCodeSessionsByEncodedName,
4861
- getOpenCodeProjects: () => getOpenCodeProjects,
4862
- getCachedOpenCodeSessionsForCwd: () => getCachedOpenCodeSessionsForCwd,
4932
+ getCachedOpenCodeProjects: () => getCachedOpenCodeProjects,
4863
4933
  getCachedOpenCodeSessionsByEncodedName: () => getCachedOpenCodeSessionsByEncodedName,
4864
- getCachedOpenCodeProjects: () => getCachedOpenCodeProjects
4934
+ getCachedOpenCodeSessionsForCwd: () => getCachedOpenCodeSessionsForCwd,
4935
+ getOpenCodeProjects: () => getOpenCodeProjects,
4936
+ getOpenCodeSessionsByEncodedName: () => getOpenCodeSessionsByEncodedName,
4937
+ getOpenCodeSessionsForCwd: () => getOpenCodeSessionsForCwd
4865
4938
  });
4866
4939
  import { execFileSync as execFileSync2 } from "node:child_process";
4867
4940
  function runOpenCodeDb(sql) {
@@ -5005,12 +5078,12 @@ var init_opencode_projects = __esm(() => {
5005
5078
  // lib/pi-projects.ts
5006
5079
  var exports_pi_projects = {};
5007
5080
  __export(exports_pi_projects, {
5008
- getPiSessionsForCwd: () => getPiSessionsForCwd,
5009
- getPiSessionsByEncodedName: () => getPiSessionsByEncodedName,
5010
- getPiProjects: () => getPiProjects,
5011
- getCachedPiSessionsForCwd: () => getCachedPiSessionsForCwd,
5081
+ getCachedPiProjects: () => getCachedPiProjects,
5012
5082
  getCachedPiSessionsByEncodedName: () => getCachedPiSessionsByEncodedName,
5013
- getCachedPiProjects: () => getCachedPiProjects
5083
+ getCachedPiSessionsForCwd: () => getCachedPiSessionsForCwd,
5084
+ getPiProjects: () => getPiProjects,
5085
+ getPiSessionsByEncodedName: () => getPiSessionsByEncodedName,
5086
+ getPiSessionsForCwd: () => getPiSessionsForCwd
5014
5087
  });
5015
5088
  import { readdir as readdir4, readFile as readFile6, stat as stat3 } from "node:fs/promises";
5016
5089
  import { homedir as homedir8 } from "node:os";
@@ -5496,12 +5569,12 @@ var init_hermes_sessions = __esm(() => {
5496
5569
  // lib/hermes-projects.ts
5497
5570
  var exports_hermes_projects = {};
5498
5571
  __export(exports_hermes_projects, {
5499
- getHermesSessionsByEncodedName: () => getHermesSessionsByEncodedName,
5500
- getHermesSessions: () => getHermesSessions,
5501
- getHermesProjects: () => getHermesProjects,
5502
- getCachedHermesSessionsByEncodedName: () => getCachedHermesSessionsByEncodedName,
5572
+ getCachedHermesProjects: () => getCachedHermesProjects,
5503
5573
  getCachedHermesSessions: () => getCachedHermesSessions,
5504
- getCachedHermesProjects: () => getCachedHermesProjects
5574
+ getCachedHermesSessionsByEncodedName: () => getCachedHermesSessionsByEncodedName,
5575
+ getHermesProjects: () => getHermesProjects,
5576
+ getHermesSessions: () => getHermesSessions,
5577
+ getHermesSessionsByEncodedName: () => getHermesSessionsByEncodedName
5505
5578
  });
5506
5579
  async function getHermesSessions() {
5507
5580
  const out = [];
@@ -5814,16 +5887,16 @@ var init_openclaw_sessions = __esm(() => {
5814
5887
  // lib/openclaw-projects.ts
5815
5888
  var exports_openclaw_projects = {};
5816
5889
  __export(exports_openclaw_projects, {
5817
- parseOpenClawProjectName: () => parseOpenClawProjectName,
5818
- openClawProjectPath: () => openClawProjectPath,
5819
- openClawProjectNameCandidates: () => openClawProjectNameCandidates,
5820
- openClawProjectName: () => openClawProjectName,
5821
- getOpenClawSessionsByEncodedName: () => getOpenClawSessionsByEncodedName,
5822
- getOpenClawSessions: () => getOpenClawSessions,
5823
- getOpenClawProjects: () => getOpenClawProjects,
5824
- getCachedOpenClawSessionsByEncodedName: () => getCachedOpenClawSessionsByEncodedName,
5890
+ getCachedOpenClawProjects: () => getCachedOpenClawProjects,
5825
5891
  getCachedOpenClawSessions: () => getCachedOpenClawSessions,
5826
- getCachedOpenClawProjects: () => getCachedOpenClawProjects
5892
+ getCachedOpenClawSessionsByEncodedName: () => getCachedOpenClawSessionsByEncodedName,
5893
+ getOpenClawProjects: () => getOpenClawProjects,
5894
+ getOpenClawSessions: () => getOpenClawSessions,
5895
+ getOpenClawSessionsByEncodedName: () => getOpenClawSessionsByEncodedName,
5896
+ openClawProjectName: () => openClawProjectName,
5897
+ openClawProjectNameCandidates: () => openClawProjectNameCandidates,
5898
+ openClawProjectPath: () => openClawProjectPath,
5899
+ parseOpenClawProjectName: () => parseOpenClawProjectName
5827
5900
  });
5828
5901
  import { readFileSync as readFileSync5 } from "node:fs";
5829
5902
  import { join as join13 } from "node:path";
@@ -6203,10 +6276,10 @@ var init_factory_sessions = __esm(() => {
6203
6276
  // lib/factory-projects.ts
6204
6277
  var exports_factory_projects = {};
6205
6278
  __export(exports_factory_projects, {
6206
- getFactorySessionsByEncodedName: () => getFactorySessionsByEncodedName,
6207
- getFactoryProjects: () => getFactoryProjects,
6279
+ getCachedFactoryProjects: () => getCachedFactoryProjects,
6208
6280
  getCachedFactorySessionsByEncodedName: () => getCachedFactorySessionsByEncodedName,
6209
- getCachedFactoryProjects: () => getCachedFactoryProjects
6281
+ getFactoryProjects: () => getFactoryProjects,
6282
+ getFactorySessionsByEncodedName: () => getFactorySessionsByEncodedName
6210
6283
  });
6211
6284
  async function getFactoryProjects() {
6212
6285
  let transcripts;
@@ -6493,12 +6566,12 @@ var init_devin_sessions = __esm(() => {
6493
6566
  // lib/devin-projects.ts
6494
6567
  var exports_devin_projects = {};
6495
6568
  __export(exports_devin_projects, {
6496
- getDevinSessionsByEncodedName: () => getDevinSessionsByEncodedName,
6497
- getDevinSessions: () => getDevinSessions,
6498
- getDevinProjects: () => getDevinProjects,
6499
- getCachedDevinSessionsByEncodedName: () => getCachedDevinSessionsByEncodedName,
6569
+ getCachedDevinProjects: () => getCachedDevinProjects,
6500
6570
  getCachedDevinSessions: () => getCachedDevinSessions,
6501
- getCachedDevinProjects: () => getCachedDevinProjects
6571
+ getCachedDevinSessionsByEncodedName: () => getCachedDevinSessionsByEncodedName,
6572
+ getDevinProjects: () => getDevinProjects,
6573
+ getDevinSessions: () => getDevinSessions,
6574
+ getDevinSessionsByEncodedName: () => getDevinSessionsByEncodedName
6502
6575
  });
6503
6576
  async function getDevinSessions() {
6504
6577
  const db = await openSqliteReadonly(devinDbPath());
@@ -6761,12 +6834,12 @@ var init_antigravity_sessions = __esm(() => {
6761
6834
  // lib/antigravity-projects.ts
6762
6835
  var exports_antigravity_projects = {};
6763
6836
  __export(exports_antigravity_projects, {
6764
- getCachedAntigravitySessionsByEncodedName: () => getCachedAntigravitySessionsByEncodedName,
6765
- getCachedAntigravityProjects: () => getCachedAntigravityProjects,
6766
- getAntigravitySessionsByEncodedName: () => getAntigravitySessionsByEncodedName,
6767
- getAntigravityProjects: () => getAntigravityProjects,
6837
+ antigravitySummariesDbPath: () => antigravitySummariesDbPath,
6768
6838
  getAntigravityIndex: () => getAntigravityIndex,
6769
- antigravitySummariesDbPath: () => antigravitySummariesDbPath
6839
+ getAntigravityProjects: () => getAntigravityProjects,
6840
+ getAntigravitySessionsByEncodedName: () => getAntigravitySessionsByEncodedName,
6841
+ getCachedAntigravityProjects: () => getCachedAntigravityProjects,
6842
+ getCachedAntigravitySessionsByEncodedName: () => getCachedAntigravitySessionsByEncodedName
6770
6843
  });
6771
6844
  import { join as join17 } from "node:path";
6772
6845
  function antigravitySummariesDbPath() {
@@ -7100,12 +7173,12 @@ var init_goose_sessions = __esm(() => {
7100
7173
  // lib/goose-projects.ts
7101
7174
  var exports_goose_projects = {};
7102
7175
  __export(exports_goose_projects, {
7103
- getGooseSessionsByEncodedName: () => getGooseSessionsByEncodedName,
7104
- getGooseSessions: () => getGooseSessions,
7105
- getGooseProjects: () => getGooseProjects,
7106
- getCachedGooseSessionsByEncodedName: () => getCachedGooseSessionsByEncodedName,
7176
+ getCachedGooseProjects: () => getCachedGooseProjects,
7107
7177
  getCachedGooseSessions: () => getCachedGooseSessions,
7108
- getCachedGooseProjects: () => getCachedGooseProjects
7178
+ getCachedGooseSessionsByEncodedName: () => getCachedGooseSessionsByEncodedName,
7179
+ getGooseProjects: () => getGooseProjects,
7180
+ getGooseSessions: () => getGooseSessions,
7181
+ getGooseSessionsByEncodedName: () => getGooseSessionsByEncodedName
7109
7182
  });
7110
7183
  async function getGooseSessions() {
7111
7184
  const db = await openSqliteReadonly(gooseDbPath());
@@ -8806,21 +8879,299 @@ var init_cloud_managed_policies = __esm(() => {
8806
8879
  POLICY_ID_RE = /^[A-Za-z0-9._-]{1,128}$/;
8807
8880
  });
8808
8881
 
8882
+ // src/hooks/pack-manifest.ts
8883
+ import { createHash as createHash4 } from "node:crypto";
8884
+ import { existsSync as existsSync12, readFileSync as readFileSync12 } from "node:fs";
8885
+ import { resolve as resolve12 } from "node:path";
8886
+ function packsRoot() {
8887
+ return process.env.FAILPROOFAI_PACK_DIR ?? packsDir();
8888
+ }
8889
+ function parsePackIdentity(value) {
8890
+ if (typeof value.id !== "string" || !PACK_ID_RE.test(value.id)) {
8891
+ throw new Error(`unsafe pack id ${JSON.stringify(value.id)}`);
8892
+ }
8893
+ if (typeof value.version !== "string" || !PACK_VERSION_RE.test(value.version)) {
8894
+ throw new Error(`invalid version for pack ${value.id}`);
8895
+ }
8896
+ if (value.effect !== undefined && value.effect !== "enforce" && value.effect !== "observe") {
8897
+ throw new Error(`unknown effect ${JSON.stringify(value.effect)} for pack ${value.id}`);
8898
+ }
8899
+ return {
8900
+ id: value.id,
8901
+ version: value.version,
8902
+ effect: value.effect ?? "enforce"
8903
+ };
8904
+ }
8905
+ function hasInstalledPacks() {
8906
+ try {
8907
+ const raw = JSON.parse(readFileSync12(installedFilePath(), "utf8"));
8908
+ return Array.isArray(raw.packs) && raw.packs.length > 0;
8909
+ } catch {
8910
+ return false;
8911
+ }
8912
+ }
8913
+ function installedFilePath() {
8914
+ return process.env.FAILPROOFAI_PACK_DIR ? resolve12(process.env.FAILPROOFAI_PACK_DIR, "installed.json") : packsInstalledFile();
8915
+ }
8916
+ function parsePackPolicy(packId, value, index) {
8917
+ const where = `${packId} policy #${index}`;
8918
+ if (!value || typeof value !== "object")
8919
+ throw new Error(`${where} is not an object`);
8920
+ const raw = value;
8921
+ const name = raw.name;
8922
+ if (typeof name !== "string" || !PACK_POLICY_NAME_RE.test(name)) {
8923
+ throw new Error(`${where} has an unsafe name ${JSON.stringify(name)}`);
8924
+ }
8925
+ if ("alwaysOn" in raw) {
8926
+ throw new Error(`${where} declares alwaysOn, which packs may not set`);
8927
+ }
8928
+ if (typeof raw.description !== "string" || raw.description.length === 0) {
8929
+ throw new Error(`${where} is missing a description`);
8930
+ }
8931
+ if (typeof raw.category !== "string" || raw.category.length === 0) {
8932
+ throw new Error(`${where} is missing a category`);
8933
+ }
8934
+ if (typeof raw.defaultEnabled !== "boolean") {
8935
+ throw new Error(`${where} is missing defaultEnabled`);
8936
+ }
8937
+ if (!raw.match || typeof raw.match !== "object") {
8938
+ throw new Error(`${where} is missing a match`);
8939
+ }
8940
+ return raw;
8941
+ }
8942
+ function parsePack(root, value) {
8943
+ if (!value || typeof value !== "object")
8944
+ throw new Error("pack entry is not an object");
8945
+ const raw = value;
8946
+ const identity = parsePackIdentity(raw);
8947
+ if (typeof raw.source !== "string" || raw.source.length === 0) {
8948
+ throw new Error(`pack ${raw.id} has no source`);
8949
+ }
8950
+ if (typeof raw.sha256 !== "string" || !SHA256_RE2.test(raw.sha256)) {
8951
+ throw new Error(`invalid SHA-256 for pack ${raw.id}`);
8952
+ }
8953
+ const effect = identity.effect;
8954
+ const path2 = resolveManagedPath(root, raw.entry);
8955
+ const actual = createHash4("sha256").update(readFileSync12(path2)).digest("hex");
8956
+ if (actual !== raw.sha256) {
8957
+ throw new Error(`pack ${raw.id} failed integrity verification: expected ${raw.sha256}, got ${actual}`);
8958
+ }
8959
+ if (!Array.isArray(raw.policies))
8960
+ throw new Error(`pack ${raw.id} policies is not an array`);
8961
+ const policies = raw.policies.map((p, i) => parsePackPolicy(raw.id, p, i));
8962
+ const names = new Set;
8963
+ for (const p of policies) {
8964
+ if (names.has(p.name))
8965
+ throw new Error(`pack ${raw.id} declares ${p.name} twice`);
8966
+ names.add(p.name);
8967
+ }
8968
+ let enabled = null;
8969
+ if (raw.enabled !== undefined) {
8970
+ if (!Array.isArray(raw.enabled) || raw.enabled.some((n) => typeof n !== "string")) {
8971
+ throw new Error(`pack ${raw.id} enabled is not an array of names`);
8972
+ }
8973
+ enabled = raw.enabled.filter((n) => names.has(n));
8974
+ }
8975
+ return {
8976
+ id: identity.id,
8977
+ version: identity.version,
8978
+ source: raw.source,
8979
+ path: path2,
8980
+ sha256: raw.sha256,
8981
+ effect,
8982
+ policies,
8983
+ enabled
8984
+ };
8985
+ }
8986
+ function readInstalledPacks() {
8987
+ const manifestPath = installedFilePath();
8988
+ if (!existsSync12(manifestPath))
8989
+ return { packs: [], errors: [] };
8990
+ let raw;
8991
+ try {
8992
+ raw = JSON.parse(readFileSync12(manifestPath, "utf8"));
8993
+ } catch (err) {
8994
+ return {
8995
+ packs: [],
8996
+ errors: [{ id: null, reason: `unreadable pack manifest: ${errText(err)}` }]
8997
+ };
8998
+ }
8999
+ if (!raw || typeof raw !== "object") {
9000
+ return { packs: [], errors: [{ id: null, reason: "pack manifest is not an object" }] };
9001
+ }
9002
+ const manifest = raw;
9003
+ if (!ACCEPTED_PACK_SCHEMA_VERSIONS.includes(manifest.schemaVersion)) {
9004
+ return {
9005
+ packs: [],
9006
+ errors: [
9007
+ {
9008
+ id: null,
9009
+ reason: `unsupported pack manifest schema ${String(manifest.schemaVersion)} ` + `(supported: ${ACCEPTED_PACK_SCHEMA_VERSIONS.join(", ")})`
9010
+ }
9011
+ ]
9012
+ };
9013
+ }
9014
+ if (!Array.isArray(manifest.packs)) {
9015
+ return { packs: [], errors: [{ id: null, reason: "pack manifest packs is not an array" }] };
9016
+ }
9017
+ const root = packsRoot();
9018
+ const packs = [];
9019
+ const errors = [];
9020
+ const seen = new Set;
9021
+ for (const entry of manifest.packs) {
9022
+ const declaredId = entry && typeof entry === "object" && typeof entry.id === "string" ? entry.id : null;
9023
+ try {
9024
+ const pack = parsePack(root, entry);
9025
+ if (seen.has(pack.id))
9026
+ throw new Error(`duplicate pack id ${pack.id}`);
9027
+ seen.add(pack.id);
9028
+ packs.push(pack);
9029
+ } catch (err) {
9030
+ const rec = entry;
9031
+ const declaredEffect = rec && (rec.effect === "observe" || rec.effect === "enforce") ? rec.effect : "enforce";
9032
+ errors.push({
9033
+ id: declaredId,
9034
+ reason: errText(err),
9035
+ effect: declaredEffect,
9036
+ ...Array.isArray(rec?.policies) ? { declared: safeDeclared(rec.policies) } : {}
9037
+ });
9038
+ }
9039
+ }
9040
+ return { packs, errors };
9041
+ }
9042
+ function safeDeclared(raw) {
9043
+ const out = [];
9044
+ for (const value of raw) {
9045
+ if (!value || typeof value !== "object")
9046
+ continue;
9047
+ const p = value;
9048
+ if (typeof p.name !== "string" || !p.match || typeof p.match !== "object")
9049
+ continue;
9050
+ out.push(p);
9051
+ }
9052
+ return out;
9053
+ }
9054
+ function errText(err) {
9055
+ return err instanceof Error ? err.message : String(err);
9056
+ }
9057
+ var ACCEPTED_PACK_SCHEMA_VERSIONS, SHA256_RE2, PACK_ID_RE, PACK_VERSION_RE, PACK_POLICY_NAME_RE;
9058
+ var init_pack_manifest = __esm(() => {
9059
+ init_fp_home();
9060
+ init_cloud_managed_policies();
9061
+ ACCEPTED_PACK_SCHEMA_VERSIONS = [1];
9062
+ SHA256_RE2 = /^[a-f0-9]{64}$/;
9063
+ PACK_ID_RE = /^[A-Za-z0-9._-]{1,64}\/[A-Za-z0-9._-]{1,64}$/;
9064
+ PACK_VERSION_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$/;
9065
+ PACK_POLICY_NAME_RE = /^[A-Za-z0-9._-]{1,128}$/;
9066
+ });
9067
+
9068
+ // src/hooks/pack-failclosed.ts
9069
+ function unionMatch(matchers) {
9070
+ const events = new Set;
9071
+ const toolNames = new Set;
9072
+ let anyEvent = false;
9073
+ let anyTool = false;
9074
+ for (const m of matchers) {
9075
+ if (!m.events || m.events.length === 0)
9076
+ anyEvent = true;
9077
+ else
9078
+ for (const e of m.events)
9079
+ events.add(e);
9080
+ if (!m.toolNames || m.toolNames.length === 0)
9081
+ anyTool = true;
9082
+ else
9083
+ for (const t of m.toolNames)
9084
+ toolNames.add(t);
9085
+ }
9086
+ const scoped = {};
9087
+ if (!anyEvent)
9088
+ scoped.events = [...events];
9089
+ if (!anyTool)
9090
+ scoped.toolNames = [...toolNames];
9091
+ return scoped;
9092
+ }
9093
+ function missingGuards(input) {
9094
+ const out = [];
9095
+ for (const err of input.errors) {
9096
+ if (err.effect === "observe")
9097
+ continue;
9098
+ const declared = err.declared ?? [];
9099
+ out.push({
9100
+ packId: err.id ?? "(unnamed pack)",
9101
+ packVersion: null,
9102
+ policies: declared.map((p) => p.name),
9103
+ match: declared.length > 0 ? unionMatch(declared.map((p) => p.match)) : unionMatch([{}]),
9104
+ reason: err.reason
9105
+ });
9106
+ }
9107
+ for (const pack of input.packs) {
9108
+ if (pack.effect === "observe")
9109
+ continue;
9110
+ const loadFailure = input.failed.get(pack.id);
9111
+ if (loadFailure && PERMANENT_LOAD_FAILURES.has(loadFailure.type)) {
9112
+ const taken2 = pack.enabled ?? pack.policies.map((p) => p.name);
9113
+ const unavailable = pack.policies.filter((p) => taken2.includes(p.name) && !input.disabled.has(`pack:${pack.id}@${pack.version}:${p.name}`));
9114
+ if (unavailable.length > 0) {
9115
+ out.push({
9116
+ packId: pack.id,
9117
+ packVersion: pack.version,
9118
+ policies: unavailable.map((p) => p.name),
9119
+ match: unionMatch(unavailable.map((p) => p.match)),
9120
+ reason: `artifact failed to load: ${loadFailure.reason}`
9121
+ });
9122
+ }
9123
+ continue;
9124
+ }
9125
+ const registered = input.registered.get(pack.id);
9126
+ if (!registered)
9127
+ continue;
9128
+ const taken = pack.enabled ?? pack.policies.map((p) => p.name);
9129
+ const missing = pack.policies.filter((p) => taken.includes(p.name) && !registered.has(p.name) && !input.disabled.has(`pack:${pack.id}@${pack.version}:${p.name}`));
9130
+ if (missing.length === 0)
9131
+ continue;
9132
+ out.push({
9133
+ packId: pack.id,
9134
+ packVersion: pack.version,
9135
+ policies: missing.map((p) => p.name),
9136
+ match: unionMatch(missing.map((p) => p.match)),
9137
+ reason: `declared ${missing.length} polic${missing.length === 1 ? "y" : "ies"} its artifact did not register`
9138
+ });
9139
+ }
9140
+ return out.filter((g) => !(g.match.events && g.match.events.length === 0));
9141
+ }
9142
+ function packFailureReason(guards) {
9143
+ const named = guards.map((g) => {
9144
+ const where = g.packVersion ? `${g.packId}@${g.packVersion}` : g.packId;
9145
+ const what = g.policies.length > 0 ? `: ${g.policies.slice(0, 4).join(", ")}` : "";
9146
+ return `${where} (${g.reason})${what}`;
9147
+ }).join("; ");
9148
+ return `Blocked because a policy pack this machine is configured to enforce is not running — ${named}. ` + `Enforcement you were told you had is missing, so this is refused rather than allowed silently. ` + `A human needs to run: failproofai policies, then failproofai policies add <source> (or failproofai config). ` + `The agent cannot run those itself.`;
9149
+ }
9150
+ var PERMANENT_LOAD_FAILURES;
9151
+ var init_pack_failclosed = __esm(() => {
9152
+ PERMANENT_LOAD_FAILURES = new Set([
9153
+ "module_not_found",
9154
+ "syntax_error",
9155
+ "runtime_error",
9156
+ "path_missing"
9157
+ ]);
9158
+ });
9159
+
8809
9160
  // lib/atomic-write.ts
8810
9161
  var init_atomic_write = () => {};
8811
9162
 
8812
9163
  // src/hooks/session-pause.ts
8813
- import { existsSync as existsSync12, mkdirSync as mkdirSync6, readdirSync as readdirSync11, readFileSync as readFileSync12, rmSync as rmSync2 } from "node:fs";
8814
- import { createHash as createHash4 } from "node:crypto";
8815
- import { join as join26, resolve as resolve12 } from "node:path";
9164
+ import { existsSync as existsSync13, mkdirSync as mkdirSync6, readdirSync as readdirSync11, readFileSync as readFileSync13, rmSync as rmSync2 } from "node:fs";
9165
+ import { createHash as createHash5 } from "node:crypto";
9166
+ import { join as join26, resolve as resolve13 } from "node:path";
8816
9167
  function pauseStateDir() {
8817
9168
  const override = process.env.FAILPROOFAI_STATE_DIR;
8818
9169
  if (override)
8819
- return resolve12(override, "sessions");
9170
+ return resolve13(override, "sessions");
8820
9171
  return sessionPauseDir();
8821
9172
  }
8822
9173
  function pauseFilePath(sessionId) {
8823
- const digest = createHash4("sha256").update(sessionId).digest("hex").slice(0, 32);
9174
+ const digest = createHash5("sha256").update(sessionId).digest("hex").slice(0, 32);
8824
9175
  return join26(pauseStateDir(), `${digest}.json`);
8825
9176
  }
8826
9177
  function parseStored(raw) {
@@ -8850,9 +9201,9 @@ function readActivePause(sessionId, now = Date.now()) {
8850
9201
  return null;
8851
9202
  try {
8852
9203
  const path2 = pauseFilePath(sessionId);
8853
- if (!existsSync12(path2))
9204
+ if (!existsSync13(path2))
8854
9205
  return null;
8855
- const stored = parseStored(JSON.parse(readFileSync12(path2, "utf8")));
9206
+ const stored = parseStored(JSON.parse(readFileSync13(path2, "utf8")));
8856
9207
  if (!stored)
8857
9208
  return null;
8858
9209
  if (stored.expiresAt <= now)
@@ -8891,9 +9242,9 @@ var init_daemon_service = __esm(() => {
8891
9242
  // src/hooks/fp-reset.ts
8892
9243
  import {
8893
9244
  copyFileSync,
8894
- existsSync as existsSync13,
9245
+ existsSync as existsSync14,
8895
9246
  mkdirSync as mkdirSync7,
8896
- readFileSync as readFileSync13,
9247
+ readFileSync as readFileSync14,
8897
9248
  readdirSync as readdirSync12,
8898
9249
  renameSync as renameSync3,
8899
9250
  rmSync as rmSync3,
@@ -8908,7 +9259,7 @@ function layoutWarningForHook() {
8908
9259
  if (state.kind === "future") {
8909
9260
  return `[failproofai] this directory was written by a newer version (layout ${state.found} vs ${LAYOUT_VERSION}) — policies are NOT being enforced. ` + `Upgrade failproofai.`;
8910
9261
  }
8911
- if (existsSync13(globalPolicyConfigFile()))
9262
+ if (existsSync14(globalPolicyConfigFile()))
8912
9263
  return null;
8913
9264
  return `[failproofai] setup predates this version — global policies are NOT being enforced. ` + `Run \`failproofai config\` to re-create it.`;
8914
9265
  }
@@ -8917,6 +9268,7 @@ var init_fp_reset = __esm(() => {
8917
9268
  init_fp_home();
8918
9269
  init_fp_config();
8919
9270
  init_daemon_service();
9271
+ init_hooks_config();
8920
9272
  });
8921
9273
 
8922
9274
  // src/hooks/handler.ts
@@ -9018,6 +9370,8 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
9018
9370
  let activePause = null;
9019
9371
  const policyAttribution = new Map;
9020
9372
  let cloudDeployment;
9373
+ const registeredByPack = new Map;
9374
+ let packErrors = [];
9021
9375
  const observedResults = [];
9022
9376
  if (opts?.forceDecision) {
9023
9377
  config = { enabledPolicies: [] };
@@ -9028,7 +9382,13 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
9028
9382
  config = readMergedHooksConfig(session.cwd);
9029
9383
  clearPolicies();
9030
9384
  activePause = readActivePause(session.sessionId);
9031
- registerBuiltinPolicies(activePause ? [] : config.enabledPolicies);
9385
+ const packsInstalledHere = hasInstalledPacks();
9386
+ const legacyNames = activePause || packsInstalledHere ? [] : config.enabledPolicies;
9387
+ registerBuiltinPolicies(activePause ? [] : legacyNames);
9388
+ if (legacyNames.length > 0) {
9389
+ hookLogWarn(`enforcing ${legacyNames.length} policies from this build because no pack is installed — ` + `run \`failproofai policies add core\` to move them into a real pack`);
9390
+ }
9391
+ const enabledBuiltinNames = new Set(legacyNames);
9032
9392
  let cloudManagedPolicies = [];
9033
9393
  try {
9034
9394
  cloudManagedPolicies = readActiveCloudManagedPolicies();
@@ -9041,15 +9401,28 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
9041
9401
  });
9042
9402
  }
9043
9403
  cloudDeployment = cloudManagedPolicies[0]?.deployment;
9404
+ let installedPacks = [];
9405
+ try {
9406
+ const packResult = readInstalledPacks();
9407
+ installedPacks = packResult.packs;
9408
+ packErrors = packResult.errors;
9409
+ for (const err of packResult.errors) {
9410
+ hookLogWarn(`pack ${err.id ?? "(unnamed)"} not loaded: ${err.reason}`);
9411
+ }
9412
+ } catch (err) {
9413
+ hookLogWarn(`pack manifest unreadable: ${err instanceof Error ? err.message : String(err)}`);
9414
+ }
9044
9415
  const configuredCustomPaths = config.customPoliciesPaths ?? config.customPoliciesPath;
9045
- const allExplicitPaths = cloudManagedPolicies.length === 0 ? configuredCustomPaths : [
9416
+ const allExplicitPaths = cloudManagedPolicies.length === 0 && installedPacks.length === 0 ? configuredCustomPaths : [
9046
9417
  ...typeof configuredCustomPaths === "string" ? [configuredCustomPaths] : configuredCustomPaths ?? [],
9047
- ...cloudManagedPolicies.map((policy) => policy.path)
9418
+ ...cloudManagedPolicies.map((policy) => policy.path),
9419
+ ...installedPacks.map((pack) => pack.path)
9048
9420
  ];
9049
9421
  const loadResult = await loadAllCustomHooks(allExplicitPaths, {
9050
9422
  sessionCwd: session.cwd,
9051
9423
  customPoliciesEnabled: config.customPoliciesEnabled,
9052
- ...cloudManagedPolicies.length > 0 ? { cloudManagedPolicies } : {}
9424
+ ...cloudManagedPolicies.length > 0 ? { cloudManagedPolicies } : {},
9425
+ ...installedPacks.length > 0 ? { packs: installedPacks } : {}
9053
9426
  });
9054
9427
  customHooksList = loadResult.hooks;
9055
9428
  const disabledCustomPolicies = new Set(config.disabledCustomPolicies ?? []);
@@ -9058,22 +9431,33 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
9058
9431
  const taggedHook = hook;
9059
9432
  const policyId = taggedHook.__policyId;
9060
9433
  const cloudManaged = taggedHook.__cloudManaged;
9434
+ const pack = taggedHook.__pack;
9061
9435
  if (!cloudManaged && policyId && disabledCustomPolicies.has(policyId))
9062
9436
  continue;
9063
9437
  if (!cloudManaged && activePause)
9064
9438
  continue;
9439
+ if (pack?.enabled && !pack.enabled.includes(hook.name))
9440
+ continue;
9441
+ if (pack && enabledBuiltinNames.has(hook.name))
9442
+ continue;
9443
+ if (pack) {
9444
+ const seen = registeredByPack.get(pack.id) ?? new Set;
9445
+ seen.add(hook.name);
9446
+ registeredByPack.set(pack.id, seen);
9447
+ }
9065
9448
  const hookName = hook.name;
9066
9449
  const conventionScope = hook.__conventionScope;
9067
9450
  const isConvention = !!conventionScope;
9068
- const prefix = cloudManaged ? `cloud/${cloudManaged.id}@${cloudManaged.version}` : isConvention ? `.failproofai-${conventionScope}` : "custom";
9069
- const observeOnly = cloudManaged?.effect === "observe";
9451
+ const prefix = cloudManaged ? `cloud/${cloudManaged.id}@${cloudManaged.version}` : pack ? `pack/${pack.id}@${pack.version}` : isConvention ? `.failproofai-${conventionScope}` : "custom";
9452
+ const observeOnly = cloudManaged?.effect === "observe" || pack?.effect === "observe";
9070
9453
  const fn = async (ctx) => {
9071
9454
  if (observeOnly) {
9072
9455
  const shadow = await runObserved(hook, ctx, hookName, eventType, cli);
9073
9456
  if (shadow.decision !== "allow") {
9457
+ const observer = cloudManaged ?? pack;
9074
9458
  observedResults.push({
9075
- policyId: cloudManaged.id,
9076
- version: cloudManaged.version,
9459
+ policyId: observer.id,
9460
+ version: observer.version,
9077
9461
  decision: shadow.decision,
9078
9462
  reason: shadow.reason ?? null
9079
9463
  });
@@ -9103,10 +9487,34 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
9103
9487
  };
9104
9488
  const registeredName = `${prefix}/${hookName}`;
9105
9489
  policyAttribution.set(registeredName, {
9106
- source: cloudManaged ? "cloud" : isConvention ? "convention" : "custom",
9107
- ...cloudManaged ? { cloudPolicyId: cloudManaged.id, cloudVersion: cloudManaged.version } : {}
9490
+ source: cloudManaged ? "cloud" : pack ? "pack" : isConvention ? "convention" : "custom",
9491
+ ...cloudManaged ? { cloudPolicyId: cloudManaged.id, cloudVersion: cloudManaged.version } : {},
9492
+ ...pack ? { packId: pack.id, packVersion: pack.version } : {}
9493
+ });
9494
+ registerPolicy(registeredName, hook.description ?? "", fn, hook.match ?? {}, -1, pack?.policies.find((p) => p.name === hook.name)?.params);
9495
+ }
9496
+ if (!activePause) {
9497
+ const guards = missingGuards({
9498
+ errors: packErrors,
9499
+ packs: installedPacks,
9500
+ registered: registeredByPack,
9501
+ failed: loadResult.packFailures,
9502
+ disabled: disabledCustomPolicies
9108
9503
  });
9109
- registerPolicy(registeredName, hook.description ?? "", fn, hook.match ?? {}, -1);
9504
+ if (guards.length > 0) {
9505
+ const reason = packFailureReason(guards);
9506
+ const match = guards.length === 1 ? guards[0].match : {
9507
+ events: guards.every((g) => g.match.events) ? [...new Set(guards.flatMap((g) => g.match.events ?? []))] : undefined
9508
+ };
9509
+ const name = "pack/failproofai-pack-unavailable";
9510
+ policyAttribution.set(name, {
9511
+ source: "pack",
9512
+ packId: guards[0].packId,
9513
+ ...guards[0].packVersion ? { packVersion: guards[0].packVersion } : {}
9514
+ });
9515
+ registerPolicy(name, "A policy pack this machine enforces could not be loaded", async () => canonicalEventType === "UserPromptSubmit" ? { decision: "instruct", reason } : { decision: "deny", reason }, match, 1);
9516
+ hookLogWarn(reason);
9517
+ }
9110
9518
  }
9111
9519
  if (customHooksList.length > 0) {
9112
9520
  trackHookEvent(getInstanceId(), "custom_hooks_loaded", {
@@ -9153,7 +9561,9 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
9153
9561
  return {
9154
9562
  policySource: attribution?.source ?? "builtin",
9155
9563
  ...attribution?.cloudPolicyId ? { cloudPolicyId: attribution.cloudPolicyId } : {},
9156
- ...attribution?.cloudVersion !== undefined ? { cloudVersion: attribution.cloudVersion } : {}
9564
+ ...attribution?.cloudVersion !== undefined ? { cloudVersion: attribution.cloudVersion } : {},
9565
+ ...attribution?.packId ? { packId: attribution.packId } : {},
9566
+ ...attribution?.packVersion ? { packVersion: attribution.packVersion } : {}
9157
9567
  };
9158
9568
  })() : {},
9159
9569
  ...cloudDeployment !== undefined ? { cloudDeployment } : {},
@@ -9212,6 +9622,8 @@ var init_handler = __esm(() => {
9212
9622
  init_telemetry_id();
9213
9623
  init_hook_logger();
9214
9624
  init_cloud_managed_policies();
9625
+ init_pack_manifest();
9626
+ init_pack_failclosed();
9215
9627
  init_session_pause();
9216
9628
  init_fp_reset();
9217
9629
  });
@@ -9222,7 +9634,7 @@ __export(exports_worker_server, {
9222
9634
  startWorkerServer: () => startWorkerServer
9223
9635
  });
9224
9636
  import { createConnection, createServer } from "node:net";
9225
- import { existsSync as existsSync14, unlinkSync as unlinkSync2 } from "node:fs";
9637
+ import { existsSync as existsSync15, unlinkSync as unlinkSync2 } from "node:fs";
9226
9638
  function isWorkerHookRequest(msg) {
9227
9639
  if (!msg || typeof msg !== "object")
9228
9640
  return false;
@@ -9355,14 +9767,14 @@ function requestExistingWorkerShutdown(socketPath) {
9355
9767
  function startWorkerServer(socketPath, shutdown) {
9356
9768
  const server = createServer((socket) => handleConnection(socket, shutdown ?? (() => server.close())));
9357
9769
  const listen = () => {
9358
- if (existsSync14(socketPath)) {
9770
+ if (existsSync15(socketPath)) {
9359
9771
  try {
9360
9772
  unlinkSync2(socketPath);
9361
9773
  } catch {}
9362
9774
  }
9363
9775
  server.listen(socketPath);
9364
9776
  };
9365
- if (!existsSync14(socketPath))
9777
+ if (!existsSync15(socketPath))
9366
9778
  listen();
9367
9779
  else
9368
9780
  requestExistingWorkerShutdown(socketPath).then(listen, (err) => server.emit("error", err));
@@ -9378,13 +9790,13 @@ var init_worker_server = __esm(() => {
9378
9790
 
9379
9791
  // bin/failproofai-worker.mjs
9380
9792
  import { realpathSync as realpathSync2 } from "node:fs";
9381
- import { dirname as dirname5, resolve as resolve13 } from "node:path";
9793
+ import { dirname as dirname5, resolve as resolve14 } from "node:path";
9382
9794
  import { fileURLToPath } from "node:url";
9383
9795
  if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
9384
- process.env.FAILPROOFAI_PACKAGE_ROOT = resolve13(dirname5(realpathSync2(fileURLToPath(import.meta.url))), "..");
9796
+ process.env.FAILPROOFAI_PACKAGE_ROOT = resolve14(dirname5(realpathSync2(fileURLToPath(import.meta.url))), "..");
9385
9797
  }
9386
9798
  if (!process.env.FAILPROOFAI_DIST_PATH) {
9387
- process.env.FAILPROOFAI_DIST_PATH = resolve13(dirname5(realpathSync2(fileURLToPath(import.meta.url))), "..", "dist");
9799
+ process.env.FAILPROOFAI_DIST_PATH = resolve14(dirname5(realpathSync2(fileURLToPath(import.meta.url))), "..", "dist");
9388
9800
  }
9389
9801
  var socketPath = process.env.FAILPROOFAI_WORKER_SOCKET;
9390
9802
  if (!socketPath) {