failproofai 1.0.2-beta.0 → 1.0.2-beta.2

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 (133) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +5 -5
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +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/build-manifest.json +2 -2
  15. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  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 +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
  32. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  33. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  35. package/.next/standalone/.next/server/app/index.html +1 -1
  36. package/.next/standalone/.next/server/app/index.rsc +14 -14
  37. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  38. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  39. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  40. package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
  41. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  42. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  44. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
  45. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  46. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  49. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  58. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
  62. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  63. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  65. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  70. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  71. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  72. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__07gm3zl._.js +14 -14
  73. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__090sxxw._.js +3 -0
  74. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  76. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  78. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  79. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  80. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/_0wb8osu._.js +23 -0
  82. package/.next/standalone/.next/server/chunks/ssr/{_12l4a_k._.js → _1oggjel._.js} +1 -1
  83. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  89. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  90. package/.next/standalone/.next/server/pages/404.html +1 -1
  91. package/.next/standalone/.next/server/pages/500.html +1 -1
  92. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  93. package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
  94. package/.next/standalone/.next/static/chunks/{3y54nw0i7dsjl.js → 0nnbtp0w4sxrr.js} +1 -1
  95. package/.next/standalone/.next/static/chunks/{0sa9v5mhc8861.js → 0ykd_go4oahan.js} +1 -1
  96. package/.next/standalone/.next/static/chunks/{0eqky-7n7dgdm.js → 140ip21rqmelt.js} +1 -1
  97. package/.next/standalone/.next/static/chunks/{2i2yt3hzjredm.js → 1da5a_a-3btas.js} +1 -1
  98. package/.next/standalone/.next/static/chunks/{1oml7c7env4-g.js → 1fac8krph-zgp.js} +1 -1
  99. package/.next/standalone/.next/static/chunks/{2hs7ycvsav7v2.js → 1nzy5jc_vf9e0.js} +1 -1
  100. package/.next/standalone/.next/static/chunks/{1sl2xmn9inx3y.js → 1yw71gn8vqco1.js} +1 -1
  101. package/.next/standalone/.next/static/chunks/{1nuqytbta44af.js → 29eh4k77otcw6.js} +1 -1
  102. package/.next/standalone/.next/static/chunks/{2aawsjld9ied_.js → 2axi5ehjbld9h.js} +1 -1
  103. package/.next/standalone/.next/static/chunks/{2gn7ut4svofm2.js → 2fzxl7n717pbc.js} +1 -1
  104. package/.next/standalone/.next/static/chunks/{35xg6retpe6rp.js → 3jjmdqqzem21x.js} +1 -1
  105. package/.next/standalone/.next/static/chunks/{turbopack-3kjgdgwci4tqf.js → turbopack-2vqpa-dts1gy4.js} +1 -1
  106. package/.next/standalone/app/actions/pack-actions.ts +21 -11
  107. package/.next/standalone/app/policies/hooks-client.tsx +3 -4
  108. package/.next/standalone/fp-cloud-cli/fp_cli/_version.py +1 -1
  109. package/.next/standalone/package.json +10 -11
  110. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  111. package/.next/standalone/server.js +1 -1
  112. package/bin/failproofai.mjs +455 -316
  113. package/dist/cli.mjs +2374 -1873
  114. package/dist/worker.mjs +3 -13
  115. package/package.json +10 -11
  116. package/src/hooks/configure-wizard.ts +67 -444
  117. package/src/hooks/fp-reset.ts +10 -19
  118. package/src/hooks/handler.ts +2 -2
  119. package/src/hooks/manager.ts +58 -24
  120. package/src/hooks/pack-cli.ts +488 -43
  121. package/src/hooks/pack-failclosed.ts +1 -1
  122. package/src/hooks/pack-manifest.ts +14 -6
  123. package/src/hooks/pack-store.ts +89 -128
  124. package/src/hooks/tui.ts +150 -48
  125. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0zig5sv._.js +0 -3
  126. package/.next/standalone/.next/server/chunks/ssr/_00e9tnf._.js +0 -23
  127. package/policy-pack/SHA256SUMS +0 -2
  128. package/policy-pack/failproofai-pack.json +0 -732
  129. package/policy-pack/failproofai-pack.mjs +0 -2017
  130. package/src/hooks/policy-presets.ts +0 -137
  131. /package/.next/standalone/.next/static/{nwbyWT06Ud0XdXOrVJyvS → A7czO1PZdTcNzLGa_H1sm}/_buildManifest.js +0 -0
  132. /package/.next/standalone/.next/static/{nwbyWT06Ud0XdXOrVJyvS → A7czO1PZdTcNzLGa_H1sm}/_clientMiddlewareManifest.js +0 -0
  133. /package/.next/standalone/.next/static/{nwbyWT06Ud0XdXOrVJyvS → A7czO1PZdTcNzLGa_H1sm}/_ssgManifest.js +0 -0
@@ -1,2017 +0,0 @@
1
- // policy-pack/.entry.generated.ts
2
- import { customPolicies } from "failproofai";
3
-
4
- // src/hooks/builtin-policies.ts
5
- import { resolve as resolve2, join as join2 } from "node:path";
6
- import { statSync as statSync2 } from "node:fs";
7
- import { readFile, writeFile } from "node:fs/promises";
8
- import { execSync, execFileSync } from "node:child_process";
9
- import { homedir as homedir2 } from "node:os";
10
-
11
- // src/hooks/policy-catalog.ts
12
- var POLICY_CATALOG = [
13
- {
14
- name: "sanitize-jwt",
15
- description: "Stop Claude from reading JWTs in tool responses",
16
- displayTitle: "Redacted JWT tokens from tool output",
17
- impact: "Stops the agent from echoing auth tokens it saw in command output.",
18
- match: { events: ["PostToolUse"] },
19
- defaultEnabled: true,
20
- category: "Sanitize"
21
- },
22
- {
23
- name: "sanitize-api-keys",
24
- description: "Stop Claude from reading API keys (OpenAI, Anthropic, GitHub, AWS, Stripe, Google) in tool responses",
25
- displayTitle: "Redacted API keys from tool output",
26
- impact: "Catches OpenAI / Anthropic / GitHub / AWS / Stripe / Google keys before the model sees them.",
27
- match: { events: ["PostToolUse"] },
28
- defaultEnabled: true,
29
- category: "Sanitize",
30
- params: {
31
- additionalPatterns: {
32
- type: "pattern[]",
33
- description: "Additional API key patterns to scrub, each with { regex, label }",
34
- default: []
35
- }
36
- }
37
- },
38
- {
39
- name: "sanitize-connection-strings",
40
- description: "Stop Claude from reading database connection strings with embedded credentials in tool responses",
41
- displayTitle: "Redacted database connection strings from tool output",
42
- impact: "Strips embedded DB credentials before they reach the model context.",
43
- match: { events: ["PostToolUse"] },
44
- defaultEnabled: true,
45
- category: "Sanitize"
46
- },
47
- {
48
- name: "sanitize-private-key-content",
49
- description: "Stop Claude from reading PEM private key content in tool responses",
50
- displayTitle: "Redacted PEM private keys from tool output",
51
- impact: "Prevents private key bodies from being echoed into chat context.",
52
- match: { events: ["PostToolUse"] },
53
- defaultEnabled: true,
54
- category: "Sanitize"
55
- },
56
- {
57
- name: "sanitize-bearer-tokens",
58
- displayTitle: "Redacted bearer tokens from tool output",
59
- impact: "Strips Authorization: Bearer values before they hit the model.",
60
- description: "Stop Claude from reading Authorization Bearer tokens in tool responses",
61
- match: { events: ["PostToolUse"] },
62
- defaultEnabled: true,
63
- category: "Sanitize"
64
- },
65
- {
66
- name: "protect-env-vars",
67
- displayTitle: "Tried to dump environment variables to chat",
68
- impact: "Env vars often contain secrets; blocking `env` / `printenv` keeps them out of the model context.",
69
- description: "Prevent commands that read environment variables",
70
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
71
- defaultEnabled: true,
72
- category: "Environment"
73
- },
74
- {
75
- name: "block-env-files",
76
- displayTitle: "Tried to read or write a .env file",
77
- impact: "`.env` files routinely contain API keys and DB credentials.",
78
- description: "Block reading/writing .env files",
79
- match: { events: ["PreToolUse"] },
80
- defaultEnabled: true,
81
- category: "Environment"
82
- },
83
- {
84
- name: "block-read-outside-cwd",
85
- displayTitle: "Tried to read files outside your project directory",
86
- impact: "Stops the agent from peeking at neighboring repos or your home directory.",
87
- description: "Block file reads outside the session working directory",
88
- match: { events: ["PreToolUse"], toolNames: ["Read", "Glob", "Grep", "Bash"] },
89
- defaultEnabled: false,
90
- category: "Environment",
91
- params: {
92
- allowPaths: {
93
- type: "string[]",
94
- description: "Absolute paths outside cwd that are allowed to be read",
95
- default: []
96
- }
97
- }
98
- },
99
- {
100
- name: "block-sudo",
101
- displayTitle: "Tried to run a command with sudo",
102
- impact: "Sudo gives the agent root — blocked unless explicitly allow-listed.",
103
- description: "Block sudo commands",
104
- match: { events: ["PreToolUse", "PermissionRequest"], toolNames: ["Bash"] },
105
- defaultEnabled: true,
106
- category: "Dangerous Commands",
107
- params: {
108
- allowPatterns: {
109
- type: "string[]",
110
- description: "Sudo command patterns to allow, matched token-by-token (e.g. 'sudo systemctl status')",
111
- default: []
112
- }
113
- }
114
- },
115
- {
116
- name: "block-curl-pipe-sh",
117
- displayTitle: "Tried to pipe a downloaded script straight to a shell",
118
- impact: "`curl ... | sh` runs unverified remote code on your machine.",
119
- description: "Block piping downloads to shell",
120
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
121
- defaultEnabled: true,
122
- category: "Dangerous Commands"
123
- },
124
- {
125
- name: "block-rm-rf",
126
- displayTitle: "Tried to recursively delete a system path",
127
- impact: "Catches catastrophic `rm -rf /` and Windows equivalents.",
128
- description: "Prevent catastrophic deletions",
129
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
130
- defaultEnabled: false,
131
- category: "Dangerous Commands",
132
- params: {
133
- allowPaths: {
134
- type: "string[]",
135
- description: "Paths that are allowed to be recursively deleted",
136
- default: []
137
- }
138
- }
139
- },
140
- {
141
- name: "block-failproofai-commands",
142
- displayTitle: "Tried to disable, pause or modify failproofai itself",
143
- impact: "An agent that can pause or remove enforcement can switch off every other policy.",
144
- description: "Block failproofai CLI commands, self-pause and uninstallation",
145
- match: { events: ["PreToolUse", "PermissionRequest"], toolNames: ["Bash"] },
146
- defaultEnabled: true,
147
- alwaysOn: true,
148
- category: "Dangerous Commands"
149
- },
150
- {
151
- name: "block-kubectl",
152
- displayTitle: "Tried to run a Kubernetes command",
153
- impact: "kubectl can change live cluster state — gated unless allow-listed.",
154
- description: "Block kubectl commands (Kubernetes cluster mutations)",
155
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
156
- defaultEnabled: false,
157
- category: "Infra Commands",
158
- params: {
159
- allowPatterns: {
160
- type: "string[]",
161
- description: "kubectl command patterns to allow, matched token-by-token (e.g. 'kubectl get *', 'kubectl describe *')",
162
- default: []
163
- }
164
- }
165
- },
166
- {
167
- name: "block-terraform",
168
- displayTitle: "Tried to run a Terraform/OpenTofu command",
169
- impact: "Terraform mutates real infrastructure — gated unless allow-listed.",
170
- description: "Block terraform and tofu (OpenTofu) commands",
171
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
172
- defaultEnabled: false,
173
- category: "Infra Commands",
174
- params: {
175
- allowPatterns: {
176
- type: "string[]",
177
- description: "terraform/tofu command patterns to allow (e.g. 'terraform plan', 'terraform validate')",
178
- default: []
179
- }
180
- }
181
- },
182
- {
183
- name: "block-aws-cli",
184
- displayTitle: "Tried to run an AWS CLI command",
185
- impact: "AWS CLI can spend money or break prod — gated.",
186
- description: "Block aws CLI commands",
187
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
188
- defaultEnabled: false,
189
- category: "Infra Commands",
190
- params: {
191
- allowPatterns: {
192
- type: "string[]",
193
- description: "aws CLI command patterns to allow (e.g. 'aws s3 ls *', 'aws sts get-caller-identity')",
194
- default: []
195
- }
196
- }
197
- },
198
- {
199
- name: "block-gcloud",
200
- displayTitle: "Tried to run a Google Cloud command",
201
- impact: "gcloud can spend money or break prod — gated.",
202
- description: "Block gcloud (Google Cloud) CLI commands",
203
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
204
- defaultEnabled: false,
205
- category: "Infra Commands",
206
- params: {
207
- allowPatterns: {
208
- type: "string[]",
209
- description: "gcloud command patterns to allow (e.g. 'gcloud auth list', 'gcloud config list')",
210
- default: []
211
- }
212
- }
213
- },
214
- {
215
- name: "block-az-cli",
216
- displayTitle: "Tried to run an Azure CLI command",
217
- impact: "az can spend money or break prod — gated.",
218
- description: "Block az (Azure) CLI commands",
219
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
220
- defaultEnabled: false,
221
- category: "Infra Commands",
222
- params: {
223
- allowPatterns: {
224
- type: "string[]",
225
- description: "az CLI command patterns to allow (e.g. 'az account show', 'az group list')",
226
- default: []
227
- }
228
- }
229
- },
230
- {
231
- name: "block-helm",
232
- displayTitle: "Tried to run a Helm command",
233
- impact: "Helm releases mutate cluster state — gated.",
234
- description: "Block helm commands",
235
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
236
- defaultEnabled: false,
237
- category: "Infra Commands",
238
- params: {
239
- allowPatterns: {
240
- type: "string[]",
241
- description: "helm command patterns to allow (e.g. 'helm list', 'helm status *')",
242
- default: []
243
- }
244
- }
245
- },
246
- {
247
- name: "block-gh-pipeline",
248
- displayTitle: "Tried to run a privileged GitHub CLI pipeline command",
249
- impact: "Catches `gh workflow run`, `gh pr merge`, `gh secret set`, etc.",
250
- description: "Block gh CLI pipeline-trigger subcommands (workflow run, run rerun/cancel, pr merge, release create/delete, cache delete, secret set/delete)",
251
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
252
- defaultEnabled: false,
253
- category: "Infra Commands",
254
- params: {
255
- allowPatterns: {
256
- type: "string[]",
257
- 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",
258
- default: []
259
- }
260
- }
261
- },
262
- {
263
- name: "block-secrets-write",
264
- displayTitle: "Tried to write a secret-key file",
265
- impact: "Stops the agent from creating `.pem`, `id_rsa`, `credentials.json`, etc.",
266
- description: "Block writing secret key files",
267
- match: { events: ["PreToolUse"], toolNames: ["Write"] },
268
- defaultEnabled: false,
269
- category: "Dangerous Commands",
270
- params: {
271
- additionalPatterns: {
272
- type: "string[]",
273
- description: "Additional filename patterns (substrings) to block",
274
- default: []
275
- }
276
- }
277
- },
278
- {
279
- name: "block-push-master",
280
- displayTitle: "Tried to push directly to main/master",
281
- impact: "Direct pushes to a protected branch bypass review.",
282
- description: "Block pushing to main/master",
283
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
284
- defaultEnabled: true,
285
- category: "Git",
286
- params: {
287
- protectedBranches: {
288
- type: "string[]",
289
- description: "Branch names to protect from direct pushes",
290
- default: ["main", "master"]
291
- }
292
- }
293
- },
294
- {
295
- name: "block-force-push",
296
- displayTitle: "Tried to force-push",
297
- impact: "Force-pushes rewrite history and can clobber teammates' work.",
298
- description: "Prevent force-pushing to any branch",
299
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
300
- defaultEnabled: false,
301
- category: "Git"
302
- },
303
- {
304
- name: "block-work-on-main",
305
- displayTitle: "Tried to commit or merge on main/master",
306
- impact: "Work should land via PR — direct commits skip review.",
307
- description: "Block git commits and merges on main/master branch",
308
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
309
- defaultEnabled: false,
310
- category: "Git",
311
- params: {
312
- protectedBranches: {
313
- type: "string[]",
314
- description: "Branch names where commits/merges are blocked",
315
- default: ["main", "master"]
316
- }
317
- }
318
- },
319
- {
320
- name: "warn-git-amend",
321
- displayTitle: "Used git commit --amend",
322
- impact: "Amending after a push rewrites history that others may have pulled.",
323
- description: "Warns before amending git commits, which rewrites history",
324
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
325
- defaultEnabled: false,
326
- category: "Git"
327
- },
328
- {
329
- name: "warn-git-stash-drop",
330
- displayTitle: "Tried to drop or clear git stash",
331
- impact: "Stash deletions are permanent and silent.",
332
- description: "Warns before permanently deleting stashed changes",
333
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
334
- defaultEnabled: false,
335
- category: "Git"
336
- },
337
- {
338
- name: "warn-all-files-staged",
339
- displayTitle: "Staged all files with git add -A / .",
340
- impact: "Wide stages routinely catch generated files or secrets you didn't intend to commit.",
341
- description: "Warns before staging all working tree files with git add -A / . / --all",
342
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
343
- defaultEnabled: false,
344
- category: "Git"
345
- },
346
- {
347
- name: "warn-destructive-sql",
348
- displayTitle: "Ran destructive SQL (DROP / TRUNCATE / DELETE without WHERE)",
349
- impact: "Easy way to wipe a table by accident.",
350
- description: "Warn before executing destructive SQL (DROP/TRUNCATE/DELETE without WHERE) via database clients",
351
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
352
- defaultEnabled: false,
353
- category: "Database"
354
- },
355
- {
356
- name: "warn-schema-alteration",
357
- displayTitle: "Altered a database schema column",
358
- impact: "ALTER TABLE operations can lock tables and break readers.",
359
- description: "Warns before SQL schema changes (ALTER TABLE with column or rename operations)",
360
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
361
- defaultEnabled: false,
362
- category: "Database"
363
- },
364
- {
365
- name: "warn-package-publish",
366
- displayTitle: "Tried to publish a package",
367
- impact: "Publishes are irreversible — `npm publish` / `cargo publish` shouldn't happen without intent.",
368
- description: "Warn before publishing packages to public registries (npm, PyPI, crates.io, RubyGems, etc.)",
369
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
370
- defaultEnabled: false,
371
- category: "Packages & System"
372
- },
373
- {
374
- name: "warn-global-package-install",
375
- displayTitle: "Installed a package globally",
376
- impact: "`npm i -g`, `cargo install`, `pip --user` pollute your machine outside the project.",
377
- description: "Warns before installing packages globally (npm -g, cargo install, etc.)",
378
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
379
- defaultEnabled: false,
380
- category: "Packages & System"
381
- },
382
- {
383
- name: "prefer-package-manager",
384
- displayTitle: "Used a non-preferred package manager",
385
- impact: "Mixing package managers creates lockfile churn for your team.",
386
- description: "Blocks non-preferred package managers and tells Claude to use an allowed one (e.g., uv instead of pip)",
387
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
388
- defaultEnabled: false,
389
- category: "Packages & System",
390
- params: {
391
- allowed: {
392
- type: "string[]",
393
- description: "Allowed package manager names (e.g. ['uv', 'bun']). Any detected manager not in this list is blocked.",
394
- default: []
395
- },
396
- blocked: {
397
- type: "string[]",
398
- description: "Additional manager names to block beyond the built-in list (e.g. ['pdm', 'pipx']).",
399
- default: []
400
- }
401
- }
402
- },
403
- {
404
- name: "warn-large-file-write",
405
- displayTitle: "Wrote a file larger than the configured threshold",
406
- impact: "Catches accidentally large file writes (logs, binaries, model dumps).",
407
- description: "Warn before writing files larger than 1MB (configurable via thresholdKb param)",
408
- match: { events: ["PreToolUse"], toolNames: ["Write"] },
409
- defaultEnabled: false,
410
- category: "Packages & System",
411
- params: {
412
- thresholdKb: {
413
- type: "number",
414
- description: "File size threshold in KB above which a warning is issued",
415
- default: 1024
416
- }
417
- }
418
- },
419
- {
420
- name: "warn-background-process",
421
- displayTitle: "Started a long-lived background process",
422
- impact: "Catches `nohup` / `&` / `screen` / `tmux` / `disown` patterns that the agent often forgets to clean up.",
423
- description: "Warns before starting detached or background processes",
424
- match: { events: ["PreToolUse"], toolNames: ["Bash"] },
425
- defaultEnabled: false,
426
- category: "Packages & System"
427
- },
428
- {
429
- name: "warn-repeated-tool-calls",
430
- displayTitle: "Called the same tool 3+ times with identical arguments",
431
- impact: "Usually a sign of a stuck loop burning tokens.",
432
- description: "Warn when the same tool is called 3+ times with identical parameters",
433
- match: { events: ["PreToolUse"] },
434
- defaultEnabled: false,
435
- category: "AI Behavior"
436
- },
437
- {
438
- name: "require-commit-before-stop",
439
- displayTitle: "Stopped with uncommitted changes",
440
- impact: "Work not in a commit is invisible to teammates and easy to lose.",
441
- description: "Require all changes to be committed before Claude stops",
442
- match: { events: ["Stop"] },
443
- defaultEnabled: false,
444
- category: "Workflow"
445
- },
446
- {
447
- name: "require-push-before-stop",
448
- displayTitle: "Stopped with unpushed commits",
449
- impact: "Local-only commits won't trigger CI or be reviewable.",
450
- description: "Require all commits to be pushed to remote before Claude stops",
451
- match: { events: ["Stop"] },
452
- defaultEnabled: false,
453
- category: "Workflow",
454
- params: {
455
- remote: {
456
- type: "string",
457
- description: "Remote name to push to (default: origin)",
458
- default: "origin"
459
- },
460
- baseBranch: {
461
- type: "string",
462
- description: "Base branch to compare against (default: main)",
463
- default: "main"
464
- }
465
- }
466
- },
467
- {
468
- name: "require-pr-before-stop",
469
- displayTitle: "Stopped without a PR for the branch",
470
- impact: "Branches without PRs don't get reviewed.",
471
- description: "Require a pull request to exist for the current branch before Claude stops",
472
- match: { events: ["Stop"] },
473
- defaultEnabled: false,
474
- category: "Workflow",
475
- params: {
476
- baseBranch: {
477
- type: "string",
478
- description: "Base branch to compare against (default: main)",
479
- default: "main"
480
- }
481
- }
482
- },
483
- {
484
- name: "require-no-conflicts-before-stop",
485
- displayTitle: "Stopped with a branch that conflicts with main",
486
- impact: "Conflicting branches can't merge — surface them early.",
487
- description: "Require the current branch to merge cleanly with the base branch before Claude stops",
488
- match: { events: ["Stop"] },
489
- defaultEnabled: false,
490
- category: "Workflow",
491
- params: {
492
- baseBranch: {
493
- type: "string",
494
- description: "Base branch to check for conflicts against (default: main)",
495
- default: "main"
496
- }
497
- }
498
- },
499
- {
500
- name: "require-ci-green-before-stop",
501
- displayTitle: "Stopped with failing CI",
502
- impact: "Failing CI blocks deploy.",
503
- description: "Require CI checks to pass on the current HEAD commit before Claude stops (ignores stale runs on prior commits)",
504
- match: { events: ["Stop"] },
505
- defaultEnabled: false,
506
- category: "Workflow"
507
- }
508
- ];
509
-
510
- // src/hooks/policy-helpers.ts
511
- function allow(reason) {
512
- return reason ? { decision: "allow", reason } : { decision: "allow" };
513
- }
514
- function deny(reason) {
515
- return { decision: "deny", reason };
516
- }
517
- function instruct(reason) {
518
- return { decision: "instruct", reason };
519
- }
520
-
521
- // src/hooks/hook-logger.ts
522
- import {
523
- appendFileSync,
524
- renameSync,
525
- mkdirSync,
526
- existsSync,
527
- statSync
528
- } from "node:fs";
529
- import { join } from "node:path";
530
-
531
- // src/hooks/fp-home.ts
532
- import { homedir } from "node:os";
533
- import { resolve } from "node:path";
534
- function failproofaiHome(home) {
535
- if (home)
536
- return resolve(home, ".failproofai");
537
- return process.env.FAILPROOFAI_HOME || resolve(homedir(), ".failproofai");
538
- }
539
- var at = (...parts) => resolve(failproofaiHome(), ...parts);
540
- var atHome = (home, ...parts) => home ? resolve(failproofaiHome(home), ...parts) : at(...parts);
541
- var logsDir = (home) => atHome(home, "logs");
542
-
543
- // src/hooks/hook-logger.ts
544
- var LEVEL_ORDER = { info: 0, warn: 1, error: 2 };
545
- var MAX_FILE_SIZE = 512 * 1024;
546
- var LOG_FILENAME = "hooks.log";
547
- var DEFAULT_LOG_DIR = logsDir();
548
- var resolved = false;
549
- var currentLevel = "warn";
550
- var fileLoggingEnabled = false;
551
- var logDir = DEFAULT_LOG_DIR;
552
- function ensureResolved() {
553
- if (resolved)
554
- return;
555
- resolved = true;
556
- const rawLevel = (process.env.FAILPROOFAI_LOG_LEVEL ?? "").toLowerCase();
557
- if (rawLevel === "info" || rawLevel === "warn" || rawLevel === "error") {
558
- currentLevel = rawLevel;
559
- }
560
- const rawFile = (process.env.FAILPROOFAI_HOOK_LOG_FILE ?? "").trim();
561
- if (rawFile) {
562
- fileLoggingEnabled = true;
563
- if (rawFile !== "1" && rawFile !== "true") {
564
- logDir = rawFile;
565
- }
566
- }
567
- }
568
- function shouldEmit(level) {
569
- ensureResolved();
570
- return LEVEL_ORDER[level] >= LEVEL_ORDER[currentLevel];
571
- }
572
- function emitStderr(label, msg) {
573
- process.stderr.write(`[failproofai:hook] ${label} ${msg}
574
- `);
575
- }
576
- function ensureLogDir() {
577
- if (!existsSync(logDir)) {
578
- mkdirSync(logDir, { recursive: true });
579
- }
580
- }
581
- function rotateIfNeeded(filePath) {
582
- try {
583
- const stats = statSync(filePath);
584
- if (stats.size >= MAX_FILE_SIZE) {
585
- const archiveName = `hooks-${Date.now()}.log`;
586
- renameSync(filePath, join(logDir, archiveName));
587
- }
588
- } catch {}
589
- }
590
- function appendToFile(label, msg) {
591
- if (!fileLoggingEnabled)
592
- return;
593
- try {
594
- ensureLogDir();
595
- const filePath = join(logDir, LOG_FILENAME);
596
- rotateIfNeeded(filePath);
597
- const timestamp = new Date().toISOString();
598
- const line = `[${timestamp}] ${label} ${msg}
599
- `;
600
- appendFileSync(filePath, line, "utf-8");
601
- } catch {}
602
- }
603
- function hookLogWarn(msg) {
604
- if (!shouldEmit("warn"))
605
- return;
606
- emitStderr("WARN", msg);
607
- appendToFile("WARN", msg);
608
- }
609
-
610
- // src/hooks/builtin-policies.ts
611
- function isAgentInternalPath(resolved2) {
612
- const normResolved = resolved2.replaceAll("\\", "/");
613
- for (const dir of [".claude", ".codex", ".copilot", ".cursor", ".opencode", ".pi", ".gemini"]) {
614
- const root = join2(homedir2(), dir).replaceAll("\\", "/");
615
- if (normResolved === root || normResolved.startsWith(root + "/"))
616
- return true;
617
- }
618
- for (const sub of [join2(".config", "opencode"), join2(".local", "share", "opencode")]) {
619
- const root = join2(homedir2(), sub).replaceAll("\\", "/");
620
- if (normResolved === root || normResolved.startsWith(root + "/"))
621
- return true;
622
- }
623
- return false;
624
- }
625
- function isAgentSettingsFile(resolved2) {
626
- if (/[\\/]\.claude[\\/]settings(?:\.[^/\\]+)?\.json$/.test(resolved2))
627
- return true;
628
- if (/[\\/]\.codex[\\/]hooks\.json$/.test(resolved2))
629
- return true;
630
- if (/[\\/]\.copilot[\\/]hooks[\\/][^/\\]+\.json$/.test(resolved2))
631
- return true;
632
- if (/[\\/]\.github[\\/]hooks[\\/][^/\\]+\.json$/.test(resolved2))
633
- return true;
634
- if (/[\\/]\.cursor[\\/]hooks\.json$/.test(resolved2))
635
- return true;
636
- if (/[\\/]\.opencode[\\/]opencode\.jsonc?$/.test(resolved2))
637
- return true;
638
- if (/[\\/]\.opencode[\\/]plugins[\\/][^/\\]+\.(?:mjs|js|ts)$/.test(resolved2))
639
- return true;
640
- if (/[\\/]\.config[\\/]opencode[\\/]opencode\.jsonc?$/.test(resolved2))
641
- return true;
642
- if (/[\\/]\.config[\\/]opencode[\\/]config\.json$/.test(resolved2))
643
- return true;
644
- if (/[\\/]\.config[\\/]opencode[\\/]plugins[\\/][^/\\]+\.(?:mjs|js|ts)$/.test(resolved2))
645
- return true;
646
- if (/[\\/]\.pi[\\/](?:agent[\\/])?settings\.json$/.test(resolved2))
647
- return true;
648
- if (/[\\/]\.pi[\\/](?:agent[\\/])?extensions[\\/]/.test(resolved2))
649
- return true;
650
- if (/[\\/]\.gemini[\\/]settings\.json$/.test(resolved2))
651
- return true;
652
- if (/[\\/]\.gemini[\\/]config[\\/]hooks\.json$/.test(resolved2))
653
- return true;
654
- return false;
655
- }
656
- var isClaudeInternalPath = isAgentInternalPath;
657
- var isClaudeSettingsFile = isAgentSettingsFile;
658
- function getCommand(ctx) {
659
- return ctx.toolInput?.command ?? "";
660
- }
661
- function getFilePath(ctx) {
662
- return ctx.toolInput?.file_path ?? "";
663
- }
664
- function parseArgvTokens(cmd) {
665
- return cmd.trim().split(/\s+/).map((t) => t.replace(/^['"]|['"]$/g, ""));
666
- }
667
- var SHELL_OPERATORS = new Set(["&&", "||", "|", ";"]);
668
- var SHELL_METACHAR_RE = /[;&<>`$()\\]/;
669
- var JWT_RE = /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/;
670
- var API_KEY_PATTERNS = [
671
- [/sk-ant-[A-Za-z0-9\-_]{20,}/, "Anthropic API key"],
672
- [/sk-proj-[A-Za-z0-9\-_]{20,}/, "OpenAI project API key"],
673
- [/sk-[A-Za-z0-9]{20,}/, "OpenAI API key"],
674
- [/ghp_[A-Za-z0-9]{36}/, "GitHub personal access token"],
675
- [/github_pat_[A-Za-z0-9_]{82}/, "GitHub fine-grained token"],
676
- [/AKIA[A-Z0-9]{16}/, "AWS access key ID"],
677
- [/sk_live_[A-Za-z0-9]{24,}/, "Stripe live secret key"],
678
- [/sk_test_[A-Za-z0-9]{24,}/, "Stripe test secret key"],
679
- [/AIza[0-9A-Za-z\-_]{35}/, "Google API key"]
680
- ];
681
- var CONNECTION_STRING_RE = /(?:postgresql|postgres|mysql|mongodb(?:\+srv)?|redis|amqps?|smtps?):\/\/[^@\s]+@/;
682
- var PRIVATE_KEY_RE = /-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----/;
683
- var BEARER_TOKEN_RE = /Authorization:\s*Bearer\s+[A-Za-z0-9\-._~+/]{20,}/i;
684
- var SECRET_PATTERNS = [
685
- [PRIVATE_KEY_RE, "private key"],
686
- [JWT_RE, "JWT"],
687
- [BEARER_TOKEN_RE, "bearer token"],
688
- [CONNECTION_STRING_RE, "database credentials"],
689
- ...API_KEY_PATTERNS
690
- ];
691
- var SQL_TOOL_RE = /\b(?:psql|mysql|sqlite3|pgcli|clickhouse-client)\b/;
692
- var DESTRUCTIVE_SQL_RE = /\b(?:DROP\s+(?:TABLE|DATABASE|SCHEMA)|TRUNCATE\b)/i;
693
- var DELETE_NO_WHERE_RE = /\bDELETE\s+FROM\b/i;
694
- var SQL_WHERE_RE = /\bWHERE\b/i;
695
- var 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;
696
- var 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/;
697
- var ENV_PRINTENV_RE = /(?:^|\s|;|&&|\|\|)(?:env|printenv)(?:\s|$|;|&&|\|)/;
698
- var ECHO_ENV_RE = /echo\s+.*\$\{?[A-Za-z_]/;
699
- var EXPORT_RE = /(?:^|\s|;|&&|\|\|)export\s+\w+/;
700
- var PS_ENV_VAR_RE = /\$env:[A-Za-z_]/i;
701
- var PS_CHILDITEM_ENV_RE = /(?:Get-ChildItem|dir|gci|ls)\s+Env:/i;
702
- var DOTNET_GETENV_RE = /\[Environment\]::GetEnvironment/i;
703
- var CMD_ECHO_ENV_RE = /echo\s+%[A-Za-z_]/i;
704
- var ENV_FILE_PATH_RE = /(?:^|[\\/])\.env(?:\.|$)/;
705
- var ENV_CMD_RE = /\.env(?:\b|\s|$|\.)/;
706
- var PS_ELEVATION_RE = /Start-Process\s+.*-Verb\s+RunAs/i;
707
- var RUNAS_RE = /(?:^|;|&&|\|\|)\s*runas\s/i;
708
- var CURL_PIPE_SH_RE = /(?:curl|wget)\s.*\|\s*(?:sh|bash|zsh|dash|ksh|csh|tcsh|fish|ash)\b/;
709
- var SEGMENT_SEPARATORS = /[;&|\n\r(){}`]+/;
710
- var COMMAND_PREFIX_TOKENS = new Set([
711
- "npx",
712
- "bunx",
713
- "pnpx",
714
- "npm",
715
- "pnpm",
716
- "yarn",
717
- "dlx",
718
- "exec",
719
- "run",
720
- "eval",
721
- "node",
722
- "bun",
723
- "deno",
724
- "env",
725
- "command",
726
- "builtin",
727
- "nohup",
728
- "setsid",
729
- "time",
730
- "timeout",
731
- "nice",
732
- "stdbuf",
733
- "xargs",
734
- "sudo",
735
- "doas",
736
- "sh",
737
- "bash",
738
- "zsh",
739
- "dash",
740
- "ksh",
741
- "fish",
742
- "ash"
743
- ]);
744
- var SELF_BINARY_TOKEN_RE = /(?:^|\/)failproofai[^/]*$/;
745
- var SELF_ENTRY_PATH_RE = /failproofai\/(?:dist\/(?:cli|index)\.mjs|bin\/failproofai\.mjs)$/;
746
- var CONFIG_SUBCOMMAND_RE = /^(?:config|configure|setup)$/;
747
- var PAUSE_FLAG_RE = /^--pause(?:=|$)/;
748
- var ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
749
- var RUNNER_OPERAND_RE = /^\d+[a-z]*$/i;
750
- function classifySelfInvocation(raw) {
751
- const command = raw.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, "$$$1");
752
- let found = null;
753
- const selfVars = new Set;
754
- for (const [, name, value] of command.matchAll(/(?:^|[\s;&|])([A-Za-z_][A-Za-z0-9_]*)=("[^"]*"|'[^']*'|[^\s;&|]+)/g)) {
755
- const bare = value.replace(/^["']|["']$/g, "");
756
- if (SELF_BINARY_TOKEN_RE.test(bare) || SELF_ENTRY_PATH_RE.test(bare))
757
- selfVars.add(name);
758
- }
759
- const isSelfVarRef = (token) => {
760
- const m = /^\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?$/.exec(token);
761
- return m ? selfVars.has(m[1]) : false;
762
- };
763
- for (const segment of command.split(SEGMENT_SEPARATORS)) {
764
- const tokens = segment.split(/\s+/).filter(Boolean);
765
- let i = 0;
766
- while (i < tokens.length) {
767
- const token = tokens[i];
768
- 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));
769
- if (!isSkippable)
770
- break;
771
- i++;
772
- }
773
- if (i >= tokens.length)
774
- continue;
775
- if (!SELF_BINARY_TOKEN_RE.test(tokens[i]) && !SELF_ENTRY_PATH_RE.test(tokens[i]) && !isSelfVarRef(tokens[i])) {
776
- continue;
777
- }
778
- const args = tokens.slice(i + 1);
779
- const configAt = args.findIndex((a) => CONFIG_SUBCOMMAND_RE.test(a));
780
- if (configAt !== -1 && args.slice(configAt + 1).some((a) => PAUSE_FLAG_RE.test(a))) {
781
- return "pause";
782
- }
783
- found = "cli";
784
- }
785
- return found;
786
- }
787
- function decodeAnsiC(body) {
788
- 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) => {
789
- try {
790
- if (seq[0] === "x")
791
- return String.fromCharCode(parseInt(seq.slice(1), 16));
792
- if (seq[0] === "u" || seq[0] === "U")
793
- return String.fromCodePoint(parseInt(seq.slice(1), 16));
794
- if (/^[0-7]+$/.test(seq))
795
- return String.fromCharCode(parseInt(seq, 8));
796
- } catch {
797
- return seq;
798
- }
799
- const named = {
800
- a: "\x07",
801
- b: "\b",
802
- e: "\x1B",
803
- f: "\f",
804
- n: `
805
- `,
806
- r: "\r",
807
- t: "\t",
808
- v: "\v",
809
- "\\": "\\",
810
- "'": "'",
811
- '"': '"',
812
- "?": "?"
813
- };
814
- return named[seq] ?? seq;
815
- });
816
- }
817
- function stripShellQuoting(command) {
818
- const joined = command.replace(/\\\r?\n/g, "");
819
- const ansiDecoded = joined.replace(/\$'((?:[^'\\]|\\.)*)'/g, (_, body) => decodeAnsiC(body));
820
- return ansiDecoded.replace(/\\(.)/g, "$1").replace(/['"]/g, "");
821
- }
822
- var PS_WEB_PIPE_RE = /(?:Invoke-WebRequest|iwr|Invoke-RestMethod|irm)\s+.*\|\s*(?:Invoke-Expression|iex)/i;
823
- var SHORT_FLAG_BUNDLE_RE = /^-[a-zA-Z]*f[a-zA-Z]*$/;
824
- var SAFE_FORCE_PREFIXES = ["--force-with-lease", "--force-if-includes"];
825
- var SECRET_FILE_RE = /\.(?:pem|key)$/;
826
- var SECRET_FILE_ID_RSA_RE = /id_rsa/;
827
- var SECRET_FILE_CREDENTIALS_RE = /credentials/;
828
- var GIT_COMMIT_MERGE_RE = /git\s+(commit|merge|rebase|cherry-pick)\b/;
829
- var FAILPROOFAI_STATE_WRITE_RE = /\b(?:rm|unlink|shred|mv|truncate)\b[^;&|]*\.failproofai(?:\/|\b)/;
830
- var 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)/;
831
- var GIT_AMEND_RE = /\bgit\s+commit\b.*--amend\b/;
832
- var GIT_STASH_DROP_RE = /\bgit\s+stash\s+(?:drop|clear)\b/;
833
- var GIT_ADD_ALL_RE = /\bgit\s+add\s+(?:-A\b|--all\b|\.(?:\s|$|;|&&|\|\|))/;
834
- var NPM_GLOBAL_RE = /\bnpm\s+(?:install|i)\b(?=.*(?:\s-g\b|--global\b))/;
835
- var YARN_GLOBAL_RE = /\byarn\s+global\s+add\b/;
836
- var PNPM_GLOBAL_RE = /\bpnpm\s+(?:add|install|i)\b(?=.*(?:\s-g\b|--global\b))/;
837
- var BUN_GLOBAL_RE = /\bbun\s+(?:install|add)\b(?=.*(?:\s-g\b|--global\b))/;
838
- var CARGO_INSTALL_RE = /\bcargo\s+install\b/;
839
- var PIP_SYSTEM_RE = /\bpip(?:3)?\s+install\b(?=.*(?:--user\b|--break-system-packages\b))/;
840
- var PKG_MANAGER_DETECTORS = {
841
- pip: [/\bpip\b/, /\bpip3\b/, /\bpython3?\s+-m\s+pip\b/],
842
- npm: [/\bnpm\b/, /\bnpx\b/],
843
- yarn: [/\byarn\b/],
844
- pnpm: [/\bpnpm\b/, /\bpnpx\b/],
845
- bun: [/\bbun\b/, /\bbunx\b/],
846
- uv: [/\buv\b/],
847
- poetry: [/\bpoetry\b/],
848
- pipenv: [/\bpipenv\b/],
849
- conda: [/\bconda\b/],
850
- cargo: [/\bcargo\b/]
851
- };
852
- var NOHUP_RE = /\bnohup\s+\S/;
853
- var SCREEN_DETACH_RE = /\bscreen\s+-[A-Za-z]*d[A-Za-z]*\b/;
854
- var TMUX_DETACH_RE = /\btmux\s+(?:new-session|new)\b[^|&;]*-d\b/;
855
- var DISOWN_RE = /\bdisown\b/;
856
- var BACKGROUND_AMPERSAND_RE = /(?<![&|])\s?&\s*(?:$|#|;)/;
857
- var KUBECTL_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*kubectl(?:\s|$)/;
858
- var TERRAFORM_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*(?:terraform|tofu)(?:\s|$)/;
859
- var AWS_CLI_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*aws(?:\s|$)/;
860
- var GCLOUD_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*gcloud(?:\s|$)/;
861
- var AZ_CLI_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*az(?:\s|$)/;
862
- var HELM_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*helm(?:\s|$)/;
863
- var 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/;
864
- var gitBranchCache = new Map;
865
- var GIT_BRANCH_CACHE_MAX_ENTRIES = 500;
866
- function statGitHeadMtimeMs(cwd) {
867
- try {
868
- return statSync2(join2(cwd, ".git", "HEAD")).mtimeMs;
869
- } catch {
870
- return null;
871
- }
872
- }
873
- function getCurrentBranch(cwd) {
874
- try {
875
- const headMtimeMs = statGitHeadMtimeMs(cwd);
876
- const cached = gitBranchCache.get(cwd);
877
- if (cached && headMtimeMs !== null && cached.headMtimeMs === headMtimeMs) {
878
- return cached.branch || null;
879
- }
880
- const branch = execSync("git rev-parse --abbrev-ref HEAD", {
881
- cwd,
882
- encoding: "utf8",
883
- stdio: ["pipe", "pipe", "pipe"],
884
- timeout: 3000
885
- }).trim();
886
- if (headMtimeMs !== null) {
887
- if (gitBranchCache.size >= GIT_BRANCH_CACHE_MAX_ENTRIES)
888
- gitBranchCache.clear();
889
- gitBranchCache.set(cwd, { branch, headMtimeMs });
890
- }
891
- return branch || null;
892
- } catch {
893
- return null;
894
- }
895
- }
896
- function getHeadSha(cwd) {
897
- try {
898
- const sha = execSync("git rev-parse HEAD", {
899
- cwd,
900
- encoding: "utf8",
901
- stdio: ["pipe", "pipe", "pipe"],
902
- timeout: 3000
903
- }).trim();
904
- return sha || null;
905
- } catch {
906
- return null;
907
- }
908
- }
909
- function getThirdPartyCheckRuns(cwd, sha) {
910
- try {
911
- const json = execFileSync("gh", [
912
- "api",
913
- `repos/{owner}/{repo}/commits/${sha}/check-runs`,
914
- "--jq",
915
- '.check_runs | map(select(.app.slug != "github-actions")) | map({name: .name, status: .status, conclusion: (.conclusion // "")})'
916
- ], {
917
- cwd,
918
- encoding: "utf8",
919
- stdio: ["pipe", "pipe", "pipe"],
920
- timeout: 15000
921
- }).trim();
922
- if (!json || json === "[]")
923
- return [];
924
- return JSON.parse(json);
925
- } catch {
926
- return [];
927
- }
928
- }
929
- function getCommitStatuses(cwd, sha) {
930
- try {
931
- const json = execFileSync("gh", [
932
- "api",
933
- `repos/{owner}/{repo}/commits/${sha}/statuses`,
934
- "--jq",
935
- "map({name: .context, state: .state}) | unique_by(.name)"
936
- ], {
937
- cwd,
938
- encoding: "utf8",
939
- stdio: ["pipe", "pipe", "pipe"],
940
- timeout: 15000
941
- }).trim();
942
- if (!json || json === "[]")
943
- return [];
944
- const statuses = JSON.parse(json);
945
- return statuses.map((s) => ({
946
- name: s.name,
947
- status: s.state === "pending" ? "in_progress" : "completed",
948
- conclusion: s.state === "pending" ? "" : s.state === "success" ? "success" : "failure"
949
- }));
950
- } catch {
951
- return [];
952
- }
953
- }
954
- function matchesAllowedPattern(cmd, pattern) {
955
- const cmdTokens = parseArgvTokens(cmd);
956
- const patTokens = parseArgvTokens(pattern);
957
- if (cmdTokens.length < patTokens.length)
958
- return false;
959
- if (cmdTokens.some((tok) => SHELL_OPERATORS.has(tok)))
960
- return false;
961
- if (cmdTokens.some((tok) => SHELL_METACHAR_RE.test(tok)))
962
- return false;
963
- return patTokens.every((tok, i) => tok === "*" || tok === cmdTokens[i]);
964
- }
965
- function sanitizeJwt(ctx) {
966
- const output = JSON.stringify(ctx.payload);
967
- if (JWT_RE.test(output)) {
968
- return {
969
- decision: "deny",
970
- reason: "JWT token detected in tool output",
971
- message: "[REDACTED: JWT token removed by failproofai]"
972
- };
973
- }
974
- return allow();
975
- }
976
- function sanitizeApiKeys(ctx) {
977
- const output = JSON.stringify(ctx.payload);
978
- for (const [pattern, label] of API_KEY_PATTERNS) {
979
- if (pattern.test(output)) {
980
- return {
981
- decision: "deny",
982
- reason: `${label} detected in tool output`,
983
- message: `[REDACTED: ${label} removed by failproofai]`
984
- };
985
- }
986
- }
987
- const additional = ctx.params?.additionalPatterns ?? [];
988
- for (const { regex, label } of additional) {
989
- try {
990
- if (new RegExp(regex).test(output)) {
991
- return {
992
- decision: "deny",
993
- reason: `${label} detected in tool output`,
994
- message: `[REDACTED: ${label} removed by failproofai]`
995
- };
996
- }
997
- } catch {
998
- hookLogWarn(`additionalPatterns: invalid regex "${regex}", skipping`);
999
- }
1000
- }
1001
- return allow();
1002
- }
1003
- function sanitizeConnectionStrings(ctx) {
1004
- const output = JSON.stringify(ctx.payload);
1005
- if (CONNECTION_STRING_RE.test(output)) {
1006
- return {
1007
- decision: "deny",
1008
- reason: "Database connection string with credentials detected in tool output",
1009
- message: "[REDACTED: connection string removed by failproofai]"
1010
- };
1011
- }
1012
- return allow();
1013
- }
1014
- function sanitizePrivateKeyContent(ctx) {
1015
- const output = JSON.stringify(ctx.payload);
1016
- if (PRIVATE_KEY_RE.test(output)) {
1017
- return {
1018
- decision: "deny",
1019
- reason: "Private key content detected in tool output",
1020
- message: "[REDACTED: private key content removed by failproofai]"
1021
- };
1022
- }
1023
- return allow();
1024
- }
1025
- function sanitizeBearerTokens(ctx) {
1026
- const output = JSON.stringify(ctx.payload);
1027
- if (BEARER_TOKEN_RE.test(output)) {
1028
- return {
1029
- decision: "deny",
1030
- reason: "Bearer token detected in tool output",
1031
- message: "[REDACTED: Bearer token removed by failproofai]"
1032
- };
1033
- }
1034
- return allow();
1035
- }
1036
- function warnDestructiveSql(ctx) {
1037
- if (ctx.toolName !== "Bash")
1038
- return allow();
1039
- const cmd = getCommand(ctx);
1040
- if (!SQL_TOOL_RE.test(cmd))
1041
- return allow();
1042
- if (DESTRUCTIVE_SQL_RE.test(cmd)) {
1043
- return instruct("STOP: This command contains destructive SQL (DROP/TRUNCATE/DELETE). Confirm with the user before executing.");
1044
- }
1045
- if (DELETE_NO_WHERE_RE.test(cmd) && !SQL_WHERE_RE.test(cmd)) {
1046
- return instruct("STOP: This command contains destructive SQL (DROP/TRUNCATE/DELETE). Confirm with the user before executing.");
1047
- }
1048
- return allow();
1049
- }
1050
- function warnLargeFileWrite(ctx) {
1051
- if (ctx.toolName !== "Write")
1052
- return allow();
1053
- const content = ctx.toolInput?.content;
1054
- if (typeof content !== "string")
1055
- return allow();
1056
- const thresholdKb = ctx.params?.thresholdKb ?? 1024;
1057
- const thresholdBytes = thresholdKb * 1024;
1058
- if (content.length > thresholdBytes) {
1059
- 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.`);
1060
- }
1061
- return allow();
1062
- }
1063
- function warnPackagePublish(ctx) {
1064
- if (ctx.toolName !== "Bash")
1065
- return allow();
1066
- const cmd = getCommand(ctx);
1067
- if (PUBLISH_CMD_RE.test(cmd)) {
1068
- return instruct("STOP: This command publishes a package to a public registry. Confirm with the user that this is intentional.");
1069
- }
1070
- return allow();
1071
- }
1072
- function protectEnvVars(ctx) {
1073
- if (ctx.toolName !== "Bash")
1074
- return allow();
1075
- const cmd = getCommand(ctx);
1076
- if (ENV_PRINTENV_RE.test(cmd)) {
1077
- return deny("Command reads environment variables");
1078
- }
1079
- if (ECHO_ENV_RE.test(cmd)) {
1080
- return deny("Command echoes environment variable");
1081
- }
1082
- if (EXPORT_RE.test(cmd)) {
1083
- return deny("Command exports environment variable");
1084
- }
1085
- if (PS_ENV_VAR_RE.test(cmd)) {
1086
- return deny("Command reads environment variable via PowerShell");
1087
- }
1088
- if (PS_CHILDITEM_ENV_RE.test(cmd)) {
1089
- return deny("Command reads environment variables via PowerShell");
1090
- }
1091
- if (DOTNET_GETENV_RE.test(cmd)) {
1092
- return deny("Command reads environment variable via .NET");
1093
- }
1094
- if (CMD_ECHO_ENV_RE.test(cmd)) {
1095
- return deny("Command echoes environment variable via cmd");
1096
- }
1097
- return allow();
1098
- }
1099
- function blockEnvFiles(ctx) {
1100
- const cmd = getCommand(ctx);
1101
- const filePath = getFilePath(ctx);
1102
- if (filePath && ENV_FILE_PATH_RE.test(filePath)) {
1103
- return deny("Access to .env file blocked");
1104
- }
1105
- if (ctx.toolName === "Bash" && ENV_CMD_RE.test(cmd)) {
1106
- return deny("Command references .env file");
1107
- }
1108
- return allow();
1109
- }
1110
- function quoteAwareSegments(command) {
1111
- const segments = [];
1112
- let tokens = [];
1113
- let token = "";
1114
- let quote = null;
1115
- const endToken = () => {
1116
- if (token)
1117
- tokens.push(token);
1118
- token = "";
1119
- };
1120
- const endSegment = () => {
1121
- endToken();
1122
- if (tokens.length)
1123
- segments.push(tokens);
1124
- tokens = [];
1125
- };
1126
- for (let i = 0;i < command.length; i++) {
1127
- const c = command[i];
1128
- if (quote) {
1129
- if (c === "\\" && quote === '"' && i + 1 < command.length) {
1130
- token += command[++i];
1131
- continue;
1132
- }
1133
- if (c === quote) {
1134
- quote = null;
1135
- continue;
1136
- }
1137
- token += c;
1138
- continue;
1139
- }
1140
- if (c === "\\" && i + 1 < command.length) {
1141
- token += command[++i];
1142
- continue;
1143
- }
1144
- if (c === '"' || c === "'") {
1145
- quote = c;
1146
- continue;
1147
- }
1148
- if (/\s/.test(c)) {
1149
- endToken();
1150
- continue;
1151
- }
1152
- if (/[;&|\n\r(){}`]/.test(c)) {
1153
- endSegment();
1154
- continue;
1155
- }
1156
- token += c;
1157
- }
1158
- endSegment();
1159
- return segments;
1160
- }
1161
- var EVAL_FLAG_RE = /^-{1,2}c$/;
1162
- function namesElevation(command, depth = 0) {
1163
- for (const tokens of quoteAwareSegments(command)) {
1164
- let i = 0;
1165
- while (i < tokens.length) {
1166
- const token = tokens[i];
1167
- const base = token.slice(token.lastIndexOf("/") + 1);
1168
- if (base === "sudo" || base === "doas")
1169
- return true;
1170
- const isSkippable = ENV_ASSIGNMENT_RE.test(token) || token.startsWith("-") || token.startsWith(">") || token.startsWith("<") || RUNNER_OPERAND_RE.test(token) || COMMAND_PREFIX_TOKENS.has(base);
1171
- if (!isSkippable)
1172
- break;
1173
- if (EVAL_FLAG_RE.test(token) && depth < 3 && i + 1 < tokens.length) {
1174
- if (namesElevation(tokens[i + 1], depth + 1))
1175
- return true;
1176
- }
1177
- i++;
1178
- }
1179
- }
1180
- return false;
1181
- }
1182
- function blockSudo(ctx) {
1183
- if (ctx.toolName !== "Bash")
1184
- return allow();
1185
- const cmd = getCommand(ctx).trimStart();
1186
- if (namesElevation(cmd)) {
1187
- const allowPatterns = ctx.params?.allowPatterns ?? [];
1188
- if (allowPatterns.some((p) => matchesAllowedPattern(cmd, p)))
1189
- return allow();
1190
- return deny("sudo commands are blocked");
1191
- }
1192
- if (PS_ELEVATION_RE.test(cmd)) {
1193
- return deny("Elevated process launch is blocked");
1194
- }
1195
- if (RUNAS_RE.test(cmd)) {
1196
- return deny("runas elevation is blocked");
1197
- }
1198
- return allow();
1199
- }
1200
- function blockCurlPipeSh(ctx) {
1201
- if (ctx.toolName !== "Bash")
1202
- return allow();
1203
- const cmd = getCommand(ctx);
1204
- if (CURL_PIPE_SH_RE.test(cmd)) {
1205
- return deny("Piping downloads to shell is blocked");
1206
- }
1207
- if (PS_WEB_PIPE_RE.test(cmd)) {
1208
- return deny("Piping downloads to Invoke-Expression is blocked");
1209
- }
1210
- return allow();
1211
- }
1212
- function extractGitPushArgs(cmd) {
1213
- return cmd.split(/&&|\|\||[|;\n]/).map((s) => s.trim()).filter((s) => /^git\s+push\s/.test(s)).map((s) => s.replace(/^git\s+push\s+/, ""));
1214
- }
1215
- function blockPushMaster(ctx) {
1216
- if (ctx.toolName !== "Bash")
1217
- return allow();
1218
- const protectedBranches = ctx.params?.protectedBranches ?? ["main", "master"];
1219
- if (protectedBranches.length === 0)
1220
- return allow();
1221
- const args = extractGitPushArgs(getCommand(ctx));
1222
- const branchPattern = new RegExp(`\\b(?:${protectedBranches.map((b) => b.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b`);
1223
- if (args.some((a) => branchPattern.test(a))) {
1224
- return deny(`Pushing to ${protectedBranches.join("/")} is blocked`);
1225
- }
1226
- return allow();
1227
- }
1228
- var HOME_PREFIX_RE = /^(?:~[A-Za-z0-9_.-]*|\$HOME|\$\{HOME\})(?=$|\/)/;
1229
- var RM_CMD_RE = /^(?:\/\S*\/)?rm$/;
1230
- var FIND_CMD_RE = /^(?:\/\S*\/)?find$/;
1231
- var FIND_EXEC_RE = /^-(?:exec|execdir|ok|okdir)$/;
1232
- var FIND_GLOBAL_OPT_RE = /^-(?:[HLP]|D|O\d*)$/;
1233
- var FIND_EXPR_START_RE = /^(?:-|\\?[(!])/;
1234
- var SCRATCH_ROOTS = ["/tmp", "/var/tmp"];
1235
- var CATASTROPHIC_DEPTH = 2;
1236
- function expandHomePrefix(path) {
1237
- const m = path.match(/^(?:~|\$HOME|\$\{HOME\})(?=$|\/)/);
1238
- return m ? homedir2() + path.slice(m[0].length) : path;
1239
- }
1240
- function stripTrailingGlob(path) {
1241
- return path.replace(/\/\*$/, "").replace(/\/+$/, "");
1242
- }
1243
- function isCatastrophicTarget(token) {
1244
- const raw = token.replace(/^['"]|['"]$/g, "");
1245
- if (raw === "")
1246
- return false;
1247
- const homePrefix = raw.match(HOME_PREFIX_RE);
1248
- if (!homePrefix && /^[$`]/.test(raw))
1249
- return true;
1250
- const belowRoot = homePrefix ? raw.slice(homePrefix[0].length) : raw.startsWith("/") ? raw : null;
1251
- if (belowRoot === null)
1252
- return false;
1253
- const segments = stripTrailingGlob(belowRoot).split("/").filter(Boolean);
1254
- if (!homePrefix && SCRATCH_ROOTS.some((r) => `/${segments.join("/")}`.startsWith(`${r}/`)))
1255
- return false;
1256
- return segments.length <= CATASTROPHIC_DEPTH;
1257
- }
1258
- function recursiveDeletionTargets(seg) {
1259
- const tokens = parseArgvTokens(seg);
1260
- const findIdx = tokens.findIndex((t) => FIND_CMD_RE.test(t));
1261
- if (findIdx >= 0) {
1262
- const expr = tokens.slice(findIdx + 1);
1263
- const execIdx = expr.findIndex((t) => FIND_EXEC_RE.test(t));
1264
- const deletes = expr.includes("-delete") || execIdx >= 0 && RM_CMD_RE.test(expr[execIdx + 1] ?? "");
1265
- if (deletes) {
1266
- let start = 0;
1267
- while (start < expr.length && FIND_GLOBAL_OPT_RE.test(expr[start])) {
1268
- start += expr[start] === "-D" ? 2 : 1;
1269
- }
1270
- const rest = expr.slice(start);
1271
- const end = rest.findIndex((t) => FIND_EXPR_START_RE.test(t));
1272
- return end < 0 ? rest : rest.slice(0, end);
1273
- }
1274
- }
1275
- const rmIdx = tokens.findIndex((t) => RM_CMD_RE.test(t));
1276
- if (rmIdx >= 0) {
1277
- const args = tokens.slice(rmIdx + 1);
1278
- const shortFlags = args.filter((t) => /^-[^-]/.test(t)).join("");
1279
- const longFlags = args.filter((t) => /^--/.test(t));
1280
- const recursive = /r/i.test(shortFlags) || longFlags.some((f) => /^--recursive$/i.test(f));
1281
- const force = /f/.test(shortFlags) || longFlags.some((f) => /^--force$/i.test(f));
1282
- if (recursive && force)
1283
- return args.filter((t) => !t.startsWith("-"));
1284
- }
1285
- return null;
1286
- }
1287
- function shellSegments(cmd) {
1288
- return cmd.split(/&&|\|\||[|;\n]/).map((s) => s.trim()).filter((s) => s !== "");
1289
- }
1290
- function deletionTargetIsAllowed(cmd, allowPaths) {
1291
- if (allowPaths.length === 0)
1292
- return false;
1293
- const normalizedAllowPaths = allowPaths.map((p) => stripTrailingGlob(expandHomePrefix(p)) || "/");
1294
- let sawRecursiveDelete = false;
1295
- for (const seg of shellSegments(cmd)) {
1296
- const targets = recursiveDeletionTargets(seg);
1297
- if (targets === null)
1298
- continue;
1299
- sawRecursiveDelete = true;
1300
- for (const target of targets) {
1301
- const normalized = stripTrailingGlob(expandHomePrefix(target)) || "/";
1302
- const covered = normalizedAllowPaths.some((np) => normalized === np || normalized.startsWith(np + "/"));
1303
- if (!covered) {
1304
- const segCovered = allowPaths.some((p) => {
1305
- const escaped = p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1306
- return new RegExp(`${escaped}(?:[/"'\\s/*]|$)`).test(seg);
1307
- });
1308
- if (!segCovered)
1309
- return false;
1310
- }
1311
- }
1312
- }
1313
- return sawRecursiveDelete;
1314
- }
1315
- function blockRmRf(ctx) {
1316
- if (ctx.toolName !== "Bash")
1317
- return allow();
1318
- const cmd = getCommand(ctx);
1319
- const hasCatastrophicTarget = shellSegments(cmd).some((seg) => {
1320
- const targets = recursiveDeletionTargets(seg);
1321
- return targets !== null && targets.some(isCatastrophicTarget);
1322
- });
1323
- if (hasCatastrophicTarget) {
1324
- const allowPaths = ctx.params?.allowPaths ?? [];
1325
- if (deletionTargetIsAllowed(cmd, allowPaths))
1326
- return allow();
1327
- return deny("Catastrophic deletion blocked");
1328
- }
1329
- if (/Remove-Item\s+.*-Recurse.*-Force.*(?:[A-Z]:\\(?:\s|$)|\\\*)/i.test(cmd)) {
1330
- return deny("Catastrophic deletion blocked");
1331
- }
1332
- if (/(?:rd|rmdir)\s+\/s\s+\/q\s+[A-Z]:\\/i.test(cmd)) {
1333
- return deny("Catastrophic deletion blocked");
1334
- }
1335
- return allow();
1336
- }
1337
- function blockForcePush(ctx) {
1338
- if (ctx.toolName !== "Bash")
1339
- return allow();
1340
- for (const segment of extractGitPushArgs(getCommand(ctx))) {
1341
- let sawEndOfOptions = false;
1342
- for (const token of segment.split(/\s+/)) {
1343
- if (token === "--") {
1344
- sawEndOfOptions = true;
1345
- continue;
1346
- }
1347
- if (sawEndOfOptions)
1348
- continue;
1349
- if (isForcePushFlag(token)) {
1350
- return deny("Force-pushing is blocked");
1351
- }
1352
- }
1353
- }
1354
- return allow();
1355
- }
1356
- function isForcePushFlag(token) {
1357
- if (token === "--force")
1358
- return true;
1359
- if (SAFE_FORCE_PREFIXES.some((prefix) => token.startsWith(prefix)))
1360
- return false;
1361
- if (token.startsWith("--force"))
1362
- return true;
1363
- return SHORT_FLAG_BUNDLE_RE.test(token);
1364
- }
1365
- function blockSecretsWrite(ctx) {
1366
- if (ctx.toolName !== "Write")
1367
- return allow();
1368
- const filePath = getFilePath(ctx);
1369
- if (SECRET_FILE_RE.test(filePath) || SECRET_FILE_ID_RSA_RE.test(filePath) || SECRET_FILE_CREDENTIALS_RE.test(filePath)) {
1370
- return deny("Writing secret key files is blocked");
1371
- }
1372
- const additionalPatterns = ctx.params?.additionalPatterns ?? [];
1373
- for (const pattern of additionalPatterns) {
1374
- if (filePath.includes(pattern)) {
1375
- return deny(`Writing blocked file pattern: ${pattern}`);
1376
- }
1377
- }
1378
- return allow();
1379
- }
1380
- var READ_LIKE_CMDS = /(?:^|;|&&|\|\||\|)\s*(?:ls|find|cat|head|tail|less|more|wc|file|stat|tree|du)\s/;
1381
- function extractAbsolutePaths(command) {
1382
- const paths = [];
1383
- const pathRe = /(?<![a-zA-Z0-9_.\-~\\*?:=])(?:~\/[^\s;|&"'()\[\]{}]*|~(?=\s|$|[;|&"'()\[\]{}])|\/[^\s;|&"'()\[\]{}]*)/g;
1384
- function addPaths(s) {
1385
- pathRe.lastIndex = 0;
1386
- let m;
1387
- while ((m = pathRe.exec(s)) !== null) {
1388
- let p = m[0];
1389
- if (p === "~")
1390
- p = homedir2();
1391
- else if (p.startsWith("~/"))
1392
- p = join2(homedir2(), p.slice(2));
1393
- paths.push(p);
1394
- }
1395
- }
1396
- let firstBarePipe = command.length;
1397
- let inDouble = false, inSingle = false;
1398
- for (let i = 0;i < command.length; i++) {
1399
- const c = command[i];
1400
- if (c === '"' && !inSingle)
1401
- inDouble = !inDouble;
1402
- else if (c === "'" && !inDouble)
1403
- inSingle = !inSingle;
1404
- else if (c === "|" && !inDouble && !inSingle) {
1405
- firstBarePipe = i;
1406
- break;
1407
- }
1408
- }
1409
- const firstSegment = command.slice(0, firstBarePipe);
1410
- const quotedRe = /"([^"]*)"|'([^']*)'/g;
1411
- let qm;
1412
- while ((qm = quotedRe.exec(firstSegment)) !== null) {
1413
- const content = qm[1] ?? qm[2] ?? "";
1414
- if (/[*?\[\]^$+()\\]/.test(content))
1415
- continue;
1416
- addPaths(content);
1417
- }
1418
- const stripped = command.replace(/"[^"]*"/g, (m) => " ".repeat(m.length)).replace(/'[^']*'/g, (m) => " ".repeat(m.length));
1419
- addPaths(stripped);
1420
- return paths;
1421
- }
1422
- function blockReadOutsideCwd(ctx) {
1423
- const cwd = process.env.CLAUDE_PROJECT_DIR || ctx.session?.cwd;
1424
- if (!cwd)
1425
- return allow();
1426
- const allowPaths = ctx.params?.allowPaths ?? [];
1427
- if (ctx.toolName === "Bash") {
1428
- const cmd = getCommand(ctx);
1429
- if (!READ_LIKE_CMDS.test(cmd))
1430
- return allow();
1431
- const paths = extractAbsolutePaths(cmd);
1432
- const cwdWithSep2 = cwd.endsWith("/") ? cwd : cwd + "/";
1433
- for (const p of paths) {
1434
- const resolved3 = resolve2(cwd, p);
1435
- if (isClaudeSettingsFile(resolved3)) {
1436
- return deny(`Reading agent settings file blocked: ${resolved3}`);
1437
- }
1438
- if (isClaudeInternalPath(resolved3))
1439
- continue;
1440
- if (resolved3 === "/dev/null")
1441
- continue;
1442
- if (resolved3 !== cwd && !resolved3.startsWith(cwdWithSep2)) {
1443
- if (allowPaths.some((ap) => resolved3 === ap || resolved3.startsWith(ap.endsWith("/") ? ap : ap + "/")))
1444
- continue;
1445
- return deny(`Bash read outside project directory blocked: ${resolved3}`);
1446
- }
1447
- }
1448
- return allow();
1449
- }
1450
- const filePath = getFilePath(ctx);
1451
- const searchPath = ctx.toolInput?.path ?? "";
1452
- const target = filePath || searchPath;
1453
- if (!target)
1454
- return allow();
1455
- const resolved2 = resolve2(cwd, target);
1456
- if (isClaudeSettingsFile(resolved2)) {
1457
- return deny(`Reading agent settings file blocked: ${resolved2}`);
1458
- }
1459
- if (isClaudeInternalPath(resolved2))
1460
- return allow();
1461
- if (resolved2 === "/dev/null")
1462
- return allow();
1463
- const cwdWithSep = cwd.endsWith("/") ? cwd : cwd + "/";
1464
- if (resolved2 !== cwd && !resolved2.startsWith(cwdWithSep)) {
1465
- if (allowPaths.some((ap) => resolved2 === ap || resolved2.startsWith(ap.endsWith("/") ? ap : ap + "/")))
1466
- return allow();
1467
- return deny(`Access outside project directory blocked: ${resolved2}`);
1468
- }
1469
- return allow();
1470
- }
1471
- function blockWorkOnMain(ctx) {
1472
- if (ctx.toolName !== "Bash")
1473
- return allow();
1474
- const cmd = getCommand(ctx);
1475
- const match = cmd.match(GIT_COMMIT_MERGE_RE);
1476
- if (!match)
1477
- return allow();
1478
- const cwd = ctx.session?.cwd;
1479
- if (!cwd)
1480
- return allow();
1481
- const branch = getCurrentBranch(cwd);
1482
- if (!branch)
1483
- return allow();
1484
- const protectedBranches = ctx.params?.protectedBranches ?? ["main", "master"];
1485
- if (protectedBranches.includes(branch)) {
1486
- return deny(`Git ${match[1]} on ${branch} is blocked. Create a feature branch first.`);
1487
- }
1488
- return allow();
1489
- }
1490
- function blockFailproofaiCommands(ctx) {
1491
- if (ctx.toolName !== "Bash")
1492
- return allow();
1493
- const cmd = getCommand(ctx);
1494
- const unescaped = stripShellQuoting(cmd);
1495
- const kind = classifySelfInvocation(cmd) ?? classifySelfInvocation(unescaped);
1496
- if (kind === "pause") {
1497
- 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.");
1498
- }
1499
- if (kind === "cli") {
1500
- return deny("Running failproofai CLI commands is blocked");
1501
- }
1502
- if (FAILPROOFAI_UNINSTALL_RE.test(cmd) || FAILPROOFAI_UNINSTALL_RE.test(unescaped)) {
1503
- return deny("Uninstalling failproofai is blocked");
1504
- }
1505
- if (FAILPROOFAI_STATE_WRITE_RE.test(cmd) || FAILPROOFAI_STATE_WRITE_RE.test(unescaped)) {
1506
- 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.");
1507
- }
1508
- return allow();
1509
- }
1510
- function blockInfraCli(ctx, re, denyMsg) {
1511
- if (ctx.toolName !== "Bash")
1512
- return allow();
1513
- const cmd = getCommand(ctx);
1514
- if (!re.test(cmd))
1515
- return allow();
1516
- const allowPatterns = ctx.params?.allowPatterns ?? [];
1517
- if (allowPatterns.some((p) => matchesAllowedPattern(cmd, p)))
1518
- return allow();
1519
- return deny(denyMsg);
1520
- }
1521
- function blockKubectl(ctx) {
1522
- return blockInfraCli(ctx, KUBECTL_RE, "kubectl commands are blocked");
1523
- }
1524
- function blockTerraform(ctx) {
1525
- return blockInfraCli(ctx, TERRAFORM_RE, "terraform/tofu commands are blocked");
1526
- }
1527
- function blockAwsCli(ctx) {
1528
- return blockInfraCli(ctx, AWS_CLI_RE, "aws CLI commands are blocked");
1529
- }
1530
- function blockGcloud(ctx) {
1531
- return blockInfraCli(ctx, GCLOUD_RE, "gcloud commands are blocked");
1532
- }
1533
- function blockAzCli(ctx) {
1534
- return blockInfraCli(ctx, AZ_CLI_RE, "az (Azure) CLI commands are blocked");
1535
- }
1536
- function blockHelm(ctx) {
1537
- return blockInfraCli(ctx, HELM_RE, "helm commands are blocked");
1538
- }
1539
- function blockGhPipeline(ctx) {
1540
- return blockInfraCli(ctx, GH_PIPELINE_RE, "gh pipeline-trigger commands are blocked");
1541
- }
1542
- var TOOL_CALL_TRACKER_MAX_BYTES = 65536;
1543
- async function warnRepeatedToolCalls(ctx) {
1544
- const THRESHOLD = 3;
1545
- const transcriptPath = ctx.session?.transcriptPath;
1546
- if (!transcriptPath || !ctx.toolName || !ctx.toolInput)
1547
- return allow();
1548
- const trackerPath = `${transcriptPath}.tool-calls.json`;
1549
- const fingerprint = JSON.stringify({ tool: ctx.toolName, input: ctx.toolInput });
1550
- let counts = {};
1551
- try {
1552
- const raw = await readFile(trackerPath, "utf8");
1553
- counts = JSON.parse(raw);
1554
- } catch {}
1555
- const prevCount = counts[fingerprint] ?? 0;
1556
- if (prevCount >= THRESHOLD) {
1557
- 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.`);
1558
- }
1559
- counts[fingerprint] = prevCount + 1;
1560
- try {
1561
- const serialized = JSON.stringify(counts);
1562
- if (serialized.length <= TOOL_CALL_TRACKER_MAX_BYTES) {
1563
- await writeFile(trackerPath, serialized, "utf8");
1564
- }
1565
- } catch {}
1566
- return allow();
1567
- }
1568
- function warnGitAmend(ctx) {
1569
- if (ctx.toolName !== "Bash")
1570
- return allow();
1571
- const cmd = getCommand(ctx);
1572
- if (GIT_AMEND_RE.test(cmd)) {
1573
- 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.");
1574
- }
1575
- return allow();
1576
- }
1577
- function warnGitStashDrop(ctx) {
1578
- if (ctx.toolName !== "Bash")
1579
- return allow();
1580
- const cmd = getCommand(ctx);
1581
- if (GIT_STASH_DROP_RE.test(cmd)) {
1582
- 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.");
1583
- }
1584
- return allow();
1585
- }
1586
- function warnAllFilesStaged(ctx) {
1587
- if (ctx.toolName !== "Bash")
1588
- return allow();
1589
- const cmd = getCommand(ctx);
1590
- if (GIT_ADD_ALL_RE.test(cmd)) {
1591
- 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.");
1592
- }
1593
- return allow();
1594
- }
1595
- function warnSchemaAlteration(ctx) {
1596
- if (ctx.toolName !== "Bash")
1597
- return allow();
1598
- const cmd = getCommand(ctx);
1599
- if (!SQL_TOOL_RE.test(cmd))
1600
- return allow();
1601
- if (SCHEMA_ALTER_RE.test(cmd)) {
1602
- 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.");
1603
- }
1604
- return allow();
1605
- }
1606
- function warnGlobalPackageInstall(ctx) {
1607
- if (ctx.toolName !== "Bash")
1608
- return allow();
1609
- const cmd = getCommand(ctx);
1610
- 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);
1611
- if (isGlobal) {
1612
- 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.");
1613
- }
1614
- return allow();
1615
- }
1616
- var SEGMENT_SPLIT_RE = /\s*(?:&&|\|\||\||;)\s*/;
1617
- function preferPackageManager(ctx) {
1618
- if (ctx.toolName !== "Bash")
1619
- return allow();
1620
- const cmd = getCommand(ctx);
1621
- if (!cmd)
1622
- return allow();
1623
- const allowed = ctx.params?.allowed ?? [];
1624
- if (allowed.length === 0)
1625
- return allow();
1626
- const allowedSet = new Set(allowed.map((a) => a.toLowerCase()));
1627
- const blocked = ctx.params?.blocked ?? [];
1628
- const allowedList = allowed.join(", ");
1629
- const segments = cmd.split(SEGMENT_SPLIT_RE);
1630
- for (const segment of segments) {
1631
- const trimmed = segment.trim();
1632
- if (!trimmed)
1633
- continue;
1634
- let segmentAllowed = false;
1635
- for (const manager of allowedSet) {
1636
- const patterns = PKG_MANAGER_DETECTORS[manager];
1637
- if (!patterns)
1638
- continue;
1639
- for (const pattern of patterns) {
1640
- if (pattern.test(trimmed)) {
1641
- segmentAllowed = true;
1642
- break;
1643
- }
1644
- }
1645
- if (segmentAllowed)
1646
- break;
1647
- }
1648
- if (segmentAllowed)
1649
- continue;
1650
- for (const [manager, patterns] of Object.entries(PKG_MANAGER_DETECTORS)) {
1651
- if (allowedSet.has(manager))
1652
- continue;
1653
- for (const pattern of patterns) {
1654
- if (pattern.test(trimmed)) {
1655
- return deny(`"${manager}" is not an allowed package manager. ` + `Allowed package managers for this project: ${allowedList}. ` + `Rewrite this command using an allowed package manager.`);
1656
- }
1657
- }
1658
- }
1659
- for (const name of blocked) {
1660
- const lower = name.toLowerCase();
1661
- if (allowedSet.has(lower))
1662
- continue;
1663
- const re = new RegExp(`\\b${lower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
1664
- if (re.test(trimmed)) {
1665
- return deny(`"${lower}" is not an allowed package manager. ` + `Allowed package managers for this project: ${allowedList}. ` + `Rewrite this command using an allowed package manager.`);
1666
- }
1667
- }
1668
- }
1669
- return allow();
1670
- }
1671
- function warnBackgroundProcess(ctx) {
1672
- if (ctx.toolName !== "Bash")
1673
- return allow();
1674
- const cmd = getCommand(ctx);
1675
- 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);
1676
- if (isBackground) {
1677
- 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.");
1678
- }
1679
- return allow();
1680
- }
1681
- function isPlanMode(ctx) {
1682
- return ctx.session?.permissionMode === "plan";
1683
- }
1684
- function requireCommitBeforeStop(ctx) {
1685
- if (isPlanMode(ctx))
1686
- return allow("Plan mode — no changes made, skipping commit check.");
1687
- const cwd = ctx.session?.cwd;
1688
- if (!cwd)
1689
- return allow("No working directory available, skipping commit check.");
1690
- try {
1691
- const status = execSync("git status --porcelain", {
1692
- cwd,
1693
- encoding: "utf8",
1694
- stdio: ["pipe", "pipe", "pipe"],
1695
- timeout: 5000
1696
- }).trim();
1697
- if (status.length > 0) {
1698
- return deny("You have uncommitted changes in the working directory. Commit all changes now.");
1699
- }
1700
- return allow("All changes are committed.");
1701
- } catch {
1702
- return allow("Not a git repository, skipping commit check.");
1703
- }
1704
- }
1705
- function requirePushBeforeStop(ctx) {
1706
- if (isPlanMode(ctx))
1707
- return allow("Plan mode — no changes made, skipping push check.");
1708
- const cwd = ctx.session?.cwd;
1709
- if (!cwd)
1710
- return allow("No working directory available, skipping push check.");
1711
- try {
1712
- const remotes = execSync("git remote", {
1713
- cwd,
1714
- encoding: "utf8",
1715
- stdio: ["pipe", "pipe", "pipe"],
1716
- timeout: 3000
1717
- }).trim();
1718
- if (!remotes)
1719
- return allow("No git remote configured, skipping push check.");
1720
- const remote = ctx.params?.remote ?? "origin";
1721
- const branch = getCurrentBranch(cwd);
1722
- if (!branch || branch === "HEAD")
1723
- return allow("Detached HEAD, skipping push check.");
1724
- const baseBranch = ctx.params?.baseBranch ?? "main";
1725
- if (branch === baseBranch) {
1726
- return allow(`On base branch "${baseBranch}", skipping push check.`);
1727
- }
1728
- try {
1729
- const ahead = execFileSync("git", ["log", `${remote}/${baseBranch}..HEAD`, "--oneline"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
1730
- if (!ahead) {
1731
- return allow(`No commits ahead of ${remote}/${baseBranch}, skipping push check.`);
1732
- }
1733
- const diff = execFileSync("git", ["diff", "--stat", `${remote}/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
1734
- if (!diff) {
1735
- return allow(`No file changes compared to ${remote}/${baseBranch}, skipping push check.`);
1736
- }
1737
- } catch {}
1738
- let hasTracking = false;
1739
- try {
1740
- execFileSync("git", ["rev-parse", "--verify", `${remote}/${branch}`], {
1741
- cwd,
1742
- encoding: "utf8",
1743
- stdio: ["pipe", "pipe", "pipe"],
1744
- timeout: 3000
1745
- });
1746
- hasTracking = true;
1747
- } catch {}
1748
- if (!hasTracking) {
1749
- return deny(`Branch "${branch}" has not been pushed to remote "${remote}". ` + `Run now: git push -u ${remote} ${branch}`);
1750
- }
1751
- const unpushed = execFileSync("git", ["log", `${remote}/${branch}..HEAD`, "--oneline"], {
1752
- cwd,
1753
- encoding: "utf8",
1754
- stdio: ["pipe", "pipe", "pipe"],
1755
- timeout: 5000
1756
- }).trim();
1757
- if (unpushed.length > 0) {
1758
- const commitCount = unpushed.split(`
1759
- `).length;
1760
- return deny(`You have ${commitCount} unpushed commit${commitCount > 1 ? "s" : ""} on branch "${branch}". ` + `Run now: git push`);
1761
- }
1762
- return allow(`All commits pushed to "${remote}".`);
1763
- } catch {
1764
- return allow("Could not check push status, skipping.");
1765
- }
1766
- }
1767
- function requirePrBeforeStop(ctx) {
1768
- if (isPlanMode(ctx))
1769
- return allow("Plan mode — no changes made, skipping PR check.");
1770
- const cwd = ctx.session?.cwd;
1771
- if (!cwd)
1772
- return allow("No working directory available, skipping PR check.");
1773
- try {
1774
- try {
1775
- execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
1776
- } catch {
1777
- return allow("GitHub CLI (gh) not installed, skipping PR check.");
1778
- }
1779
- const branch = getCurrentBranch(cwd);
1780
- if (!branch || branch === "HEAD")
1781
- return allow("Detached HEAD, skipping PR check.");
1782
- const baseBranch = ctx.params?.baseBranch ?? "main";
1783
- if (branch === baseBranch) {
1784
- return allow(`On base branch "${baseBranch}", skipping PR check.`);
1785
- }
1786
- try {
1787
- const ahead = execFileSync("git", ["log", `origin/${baseBranch}..HEAD`, "--oneline"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
1788
- if (!ahead) {
1789
- return allow(`No commits ahead of origin/${baseBranch}, skipping PR check.`);
1790
- }
1791
- const diff = execFileSync("git", ["diff", "--stat", `origin/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
1792
- if (!diff) {
1793
- return allow(`No file changes compared to origin/${baseBranch}, skipping PR check.`);
1794
- }
1795
- } catch {}
1796
- let prJson;
1797
- try {
1798
- prJson = execSync("gh pr view --json number,url,state", {
1799
- cwd,
1800
- encoding: "utf8",
1801
- stdio: ["pipe", "pipe", "pipe"],
1802
- timeout: 15000
1803
- }).trim();
1804
- } catch {
1805
- return deny(`No pull request found for branch "${branch}". ` + `Run now: gh pr create`);
1806
- }
1807
- const pr = JSON.parse(prJson);
1808
- if (pr.state === "OPEN") {
1809
- return allow(`PR #${pr.number} exists: ${pr.url}`);
1810
- }
1811
- if (pr.state === "MERGED") {
1812
- return allow(`PR #${pr.number} was merged: ${pr.url}. ` + `Switch off this branch (e.g. 'git checkout ${baseBranch} && git pull') before stopping again.`);
1813
- }
1814
- return deny(`Pull request for branch "${branch}" is ${pr.state.toLowerCase()}. Run now: gh pr create`);
1815
- } catch {
1816
- return allow("Could not check PR status, skipping.");
1817
- }
1818
- }
1819
- function requireNoConflictsBeforeStop(ctx) {
1820
- if (isPlanMode(ctx))
1821
- return allow("Plan mode — no changes made, skipping conflict check.");
1822
- const cwd = ctx.session?.cwd;
1823
- if (!cwd)
1824
- return allow("No working directory available, skipping conflict check.");
1825
- const branch = getCurrentBranch(cwd);
1826
- if (!branch || branch === "HEAD")
1827
- return allow("Detached HEAD, skipping conflict check.");
1828
- const baseBranch = ctx.params?.baseBranch ?? "main";
1829
- if (branch === baseBranch) {
1830
- return allow(`On base branch "${baseBranch}", skipping conflict check.`);
1831
- }
1832
- try {
1833
- execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
1834
- } catch {
1835
- return allow("gh CLI not installed, skipping conflict check.");
1836
- }
1837
- let prJson;
1838
- try {
1839
- prJson = execSync("gh pr view --json mergeable,number,url,state", {
1840
- cwd,
1841
- encoding: "utf8",
1842
- stdio: ["pipe", "pipe", "pipe"],
1843
- timeout: 15000
1844
- }).trim();
1845
- } catch {
1846
- return allow("No pull request found for branch, skipping conflict check.");
1847
- }
1848
- let pr;
1849
- try {
1850
- pr = JSON.parse(prJson);
1851
- } catch {
1852
- return allow("Could not parse gh pr view output, skipping conflict check.");
1853
- }
1854
- if (pr.state !== "OPEN") {
1855
- return allow(`PR #${pr.number} is ${pr.state.toLowerCase()}; skipping conflict check.`);
1856
- }
1857
- try {
1858
- execFileSync("git", ["rev-parse", "--verify", `origin/${baseBranch}`], {
1859
- cwd,
1860
- encoding: "utf8",
1861
- stdio: ["pipe", "pipe", "pipe"],
1862
- timeout: 3000
1863
- });
1864
- const ahead = execFileSync("git", ["log", `origin/${baseBranch}..HEAD`, "--oneline"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
1865
- if (ahead) {
1866
- execFileSync("git", ["merge-tree", "--write-tree", "--name-only", `origin/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 1e4 });
1867
- }
1868
- } catch (err) {
1869
- const e = err;
1870
- if (e.status === 1) {
1871
- const out = (typeof e.stdout === "string" ? e.stdout : e.stdout?.toString("utf8") ?? "").trim();
1872
- const lines = out.split(`
1873
- `);
1874
- const files = [];
1875
- for (let i = 1;i < lines.length; i++) {
1876
- const line = lines[i];
1877
- if (line === "")
1878
- break;
1879
- files.push(line);
1880
- }
1881
- const fileList = files.length ? files.join(", ") : "one or more files";
1882
- return deny(`Branch "${branch}" has merge conflicts with ${baseBranch} in: ${fileList}. ` + `Rebase or merge origin/${baseBranch} now and resolve the conflicts.`);
1883
- }
1884
- }
1885
- if (pr.mergeable === "CONFLICTING") {
1886
- return deny(`PR #${pr.number} has merge conflicts per GitHub (${pr.url}). ` + `Rebase or merge origin/${baseBranch} now and resolve the conflicts.`);
1887
- }
1888
- if (pr.mergeable === "UNKNOWN") {
1889
- 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.`);
1890
- }
1891
- return allow(`PR #${pr.number} merges cleanly per GitHub.`);
1892
- }
1893
- function requireCiGreenBeforeStop(ctx) {
1894
- if (isPlanMode(ctx))
1895
- return allow("Plan mode — no changes made, skipping CI check.");
1896
- const cwd = ctx.session?.cwd;
1897
- if (!cwd)
1898
- return allow("No working directory available, skipping CI check.");
1899
- try {
1900
- try {
1901
- execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
1902
- } catch {
1903
- return allow("GitHub CLI (gh) not installed, skipping CI check.");
1904
- }
1905
- const branch = getCurrentBranch(cwd);
1906
- if (!branch || branch === "HEAD")
1907
- return allow("Detached HEAD, skipping CI check.");
1908
- const sha = getHeadSha(cwd);
1909
- let workflowRuns = [];
1910
- try {
1911
- 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();
1912
- if (runsJson && runsJson !== "[]") {
1913
- const allWorkflowRuns = JSON.parse(runsJson);
1914
- const headRuns = sha ? allWorkflowRuns.filter((r) => r.headSha === sha) : allWorkflowRuns;
1915
- const seen = new Set;
1916
- workflowRuns = headRuns.filter((r) => {
1917
- if (seen.has(r.name))
1918
- return false;
1919
- seen.add(r.name);
1920
- return true;
1921
- });
1922
- }
1923
- } catch {}
1924
- let thirdPartyChecks = [];
1925
- let commitStatuses = [];
1926
- if (sha) {
1927
- thirdPartyChecks = getThirdPartyCheckRuns(cwd, sha);
1928
- commitStatuses = getCommitStatuses(cwd, sha);
1929
- }
1930
- const allChecks = [...workflowRuns, ...thirdPartyChecks, ...commitStatuses];
1931
- if (allChecks.length === 0)
1932
- return allow(`No CI runs found for branch "${branch}".`);
1933
- const failing = allChecks.filter((r) => r.status === "completed" && r.conclusion !== "success" && r.conclusion !== "skipped" && r.conclusion !== "cancelled" && r.conclusion !== "neutral");
1934
- if (failing.length > 0) {
1935
- const names = failing.map((r) => `"${r.name}"`).join(", ");
1936
- return deny(`CI checks are failing on branch "${branch}": ${names}. Fix the failing checks now.`);
1937
- }
1938
- const pending = allChecks.filter((r) => r.status === "in_progress" || r.status === "queued" || r.status === "waiting");
1939
- if (pending.length > 0) {
1940
- const names = pending.map((r) => `"${r.name}"`).join(", ");
1941
- return deny(`CI checks are still running on branch "${branch}": ${names}. Wait for all checks to complete, then verify they pass.`);
1942
- }
1943
- return allow(`All CI checks passed on branch "${branch}".`);
1944
- } catch {
1945
- return allow("Could not check CI status, skipping.");
1946
- }
1947
- }
1948
- var POLICY_IMPLEMENTATIONS = {
1949
- "sanitize-jwt": sanitizeJwt,
1950
- "sanitize-api-keys": sanitizeApiKeys,
1951
- "sanitize-connection-strings": sanitizeConnectionStrings,
1952
- "sanitize-private-key-content": sanitizePrivateKeyContent,
1953
- "sanitize-bearer-tokens": sanitizeBearerTokens,
1954
- "protect-env-vars": protectEnvVars,
1955
- "block-env-files": blockEnvFiles,
1956
- "block-read-outside-cwd": blockReadOutsideCwd,
1957
- "block-sudo": blockSudo,
1958
- "block-curl-pipe-sh": blockCurlPipeSh,
1959
- "block-rm-rf": blockRmRf,
1960
- "block-failproofai-commands": blockFailproofaiCommands,
1961
- "block-kubectl": blockKubectl,
1962
- "block-terraform": blockTerraform,
1963
- "block-aws-cli": blockAwsCli,
1964
- "block-gcloud": blockGcloud,
1965
- "block-az-cli": blockAzCli,
1966
- "block-helm": blockHelm,
1967
- "block-gh-pipeline": blockGhPipeline,
1968
- "block-secrets-write": blockSecretsWrite,
1969
- "block-push-master": blockPushMaster,
1970
- "block-force-push": blockForcePush,
1971
- "block-work-on-main": blockWorkOnMain,
1972
- "warn-git-amend": warnGitAmend,
1973
- "warn-git-stash-drop": warnGitStashDrop,
1974
- "warn-all-files-staged": warnAllFilesStaged,
1975
- "warn-destructive-sql": warnDestructiveSql,
1976
- "warn-schema-alteration": warnSchemaAlteration,
1977
- "warn-package-publish": warnPackagePublish,
1978
- "warn-global-package-install": warnGlobalPackageInstall,
1979
- "prefer-package-manager": preferPackageManager,
1980
- "warn-large-file-write": warnLargeFileWrite,
1981
- "warn-background-process": warnBackgroundProcess,
1982
- "warn-repeated-tool-calls": warnRepeatedToolCalls,
1983
- "require-commit-before-stop": requireCommitBeforeStop,
1984
- "require-push-before-stop": requirePushBeforeStop,
1985
- "require-pr-before-stop": requirePrBeforeStop,
1986
- "require-no-conflicts-before-stop": requireNoConflictsBeforeStop,
1987
- "require-ci-green-before-stop": requireCiGreenBeforeStop
1988
- };
1989
- function assertCatalogBijection() {
1990
- const implNames = new Set(Object.keys(POLICY_IMPLEMENTATIONS));
1991
- const missing = POLICY_CATALOG.filter((e) => !implNames.has(e.name)).map((e) => e.name);
1992
- if (missing.length > 0) {
1993
- throw new Error(`failproofai: builtin policies missing an implementation: ${missing.join(", ")}`);
1994
- }
1995
- const catalogNames = new Set(POLICY_CATALOG.map((e) => e.name));
1996
- const orphaned = [...implNames].filter((n) => !catalogNames.has(n));
1997
- if (orphaned.length > 0) {
1998
- throw new Error(`failproofai: policy implementations with no catalog entry: ${orphaned.join(", ")}`);
1999
- }
2000
- }
2001
- assertCatalogBijection();
2002
- var BUILTIN_POLICIES = POLICY_CATALOG.map((entry) => ({
2003
- ...entry,
2004
- fn: POLICY_IMPLEMENTATIONS[entry.name]
2005
- }));
2006
-
2007
- // policy-pack/.entry.generated.ts
2008
- for (const policy of BUILTIN_POLICIES) {
2009
- if (policy.alwaysOn)
2010
- continue;
2011
- customPolicies.add({
2012
- name: policy.name,
2013
- description: policy.description,
2014
- match: policy.match,
2015
- fn: policy.fn
2016
- });
2017
- }