failproofai 0.0.1-beta.9 → 0.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 (118) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/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.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  13. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  14. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  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 +2 -2
  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/_head.segment.rsc +4 -4
  22. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  23. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  24. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  25. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  26. package/.next/standalone/.next/server/app/index.html +1 -1
  27. package/.next/standalone/.next/server/app/index.rsc +15 -15
  28. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  29. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  30. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  31. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  32. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  33. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  34. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  37. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  39. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  40. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  42. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  43. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  44. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  47. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/chunks/[root-of-the-server]__02nt~6d._.js +1 -1
  50. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
  51. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +2 -2
  52. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09icjsf._.js +2 -2
  53. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09z7o2x._.js +2 -2
  54. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0su5_t~._.js → [root-of-the-server]__0a3kr67._.js} +2 -2
  55. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +2 -2
  56. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +2 -2
  57. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0osi8nq._.js +2 -2
  58. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0yhzo9v._.js → [root-of-the-server]__0rbuarm._.js} +2 -2
  59. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +2 -2
  60. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +2 -2
  61. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +2 -2
  62. package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +1 -1
  65. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0rd0oc-._.js +1 -1
  66. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  67. package/.next/standalone/.next/server/pages/404.html +2 -2
  68. package/.next/standalone/.next/server/pages/500.html +1 -1
  69. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  70. package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
  71. package/.next/standalone/.next/static/chunks/{124wu0bxsexm6.js → 0a08gn8709y98.js} +1 -1
  72. package/.next/standalone/.next/static/chunks/{12olt9p45z-lq.js → 0gcz-jqgqz~9m.js} +1 -1
  73. package/.next/standalone/.next/static/chunks/{0pc9pc_pilpi9.js → 0jhw8ofx.5g_e.js} +1 -1
  74. package/.next/standalone/.next/static/chunks/{0.aencsvb-yev.js → 0kob_5.phc~sk.js} +1 -1
  75. package/.next/standalone/.next/static/chunks/{0frzv~pmu0hsf.js → 0mjc3aq2wxvlt.js} +1 -1
  76. package/.next/standalone/.next/static/chunks/{0nygnut7i45jn.js → 0mr-jhx402yci.js} +1 -1
  77. package/.next/standalone/.next/static/chunks/{0qjjki0j187__.js → 0q7z97izctgrw.js} +1 -1
  78. package/.next/standalone/.next/static/chunks/{14xe0g0rgwk18.js → 0qvj8bhl661lq.js} +1 -1
  79. package/.next/standalone/AGENTS.md +80 -0
  80. package/.next/standalone/CLAUDE.md +137 -0
  81. package/.next/standalone/README.md +9 -1
  82. package/.next/standalone/bin/failproofai.mjs +12 -1
  83. package/.next/standalone/dist/cli.mjs +2891 -0
  84. package/.next/standalone/dist/index.js +80 -0
  85. package/.next/standalone/docs/architecture.md +5 -1
  86. package/.next/standalone/docs/built-in-policies.md +5 -1
  87. package/.next/standalone/docs/cli-reference.md +5 -1
  88. package/.next/standalone/docs/configuration.md +5 -1
  89. package/.next/standalone/docs/custom-hooks.md +5 -1
  90. package/.next/standalone/docs/dashboard.md +5 -1
  91. package/.next/standalone/docs/docs.json +83 -0
  92. package/.next/standalone/docs/favicon.ico +0 -0
  93. package/.next/standalone/docs/getting-started.md +8 -2
  94. package/.next/standalone/docs/introduction.md +47 -0
  95. package/.next/standalone/docs/logo/exosphere-dark.png +0 -0
  96. package/.next/standalone/docs/logo/exosphere-light.png +0 -0
  97. package/.next/standalone/docs/package-aliases.md +32 -26
  98. package/.next/standalone/docs/testing.md +5 -1
  99. package/.next/standalone/examples/policies-notification.js +77 -32
  100. package/.next/standalone/package.json +7 -5
  101. package/.next/standalone/scripts/launch.ts +1 -1
  102. package/.next/standalone/scripts/postinstall.mjs +11 -0
  103. package/.next/standalone/scripts/sync-hook-events-prompt.md +60 -0
  104. package/.next/standalone/server.js +1 -1
  105. package/.next/standalone/src/hooks/types.ts +11 -2
  106. package/README.md +9 -1
  107. package/bin/failproofai.mjs +12 -1
  108. package/dist/cli.mjs +2891 -0
  109. package/dist/index.js +80 -0
  110. package/package.json +7 -5
  111. package/scripts/launch.ts +1 -1
  112. package/scripts/postinstall.mjs +11 -0
  113. package/scripts/sync-hook-events-prompt.md +60 -0
  114. package/src/hooks/types.ts +11 -2
  115. package/.next/standalone/docs/index.md +0 -48
  116. /package/.next/standalone/.next/static/{o1smiVYETTvcMet_AU8oi → Dnk96sbMPjYOx1pdLdOH0}/_buildManifest.js +0 -0
  117. /package/.next/standalone/.next/static/{o1smiVYETTvcMet_AU8oi → Dnk96sbMPjYOx1pdLdOH0}/_clientMiddlewareManifest.js +0 -0
  118. /package/.next/standalone/.next/static/{o1smiVYETTvcMet_AU8oi → Dnk96sbMPjYOx1pdLdOH0}/_ssgManifest.js +0 -0
@@ -0,0 +1,2891 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
+
18
+ // src/hooks/hook-logger.ts
19
+ import {
20
+ appendFileSync,
21
+ renameSync,
22
+ mkdirSync,
23
+ existsSync,
24
+ statSync
25
+ } from "node:fs";
26
+ import { join } from "node:path";
27
+ import { homedir } from "node:os";
28
+ function ensureResolved() {
29
+ if (resolved)
30
+ return;
31
+ resolved = true;
32
+ const rawLevel = (process.env.FAILPROOFAI_LOG_LEVEL ?? "").toLowerCase();
33
+ if (rawLevel === "info" || rawLevel === "warn" || rawLevel === "error") {
34
+ currentLevel = rawLevel;
35
+ }
36
+ const rawFile = (process.env.FAILPROOFAI_HOOK_LOG_FILE ?? "").trim();
37
+ if (rawFile) {
38
+ fileLoggingEnabled = true;
39
+ if (rawFile !== "1" && rawFile !== "true") {
40
+ logDir = rawFile;
41
+ }
42
+ }
43
+ }
44
+ function shouldEmit(level) {
45
+ ensureResolved();
46
+ return LEVEL_ORDER[level] >= LEVEL_ORDER[currentLevel];
47
+ }
48
+ function emitStderr(label, msg) {
49
+ process.stderr.write(`[failproofai:hook] ${label} ${msg}
50
+ `);
51
+ }
52
+ function ensureLogDir() {
53
+ if (!existsSync(logDir)) {
54
+ mkdirSync(logDir, { recursive: true });
55
+ }
56
+ }
57
+ function rotateIfNeeded(filePath) {
58
+ try {
59
+ const stats = statSync(filePath);
60
+ if (stats.size >= MAX_FILE_SIZE) {
61
+ const archiveName = `hooks-${Date.now()}.log`;
62
+ renameSync(filePath, join(logDir, archiveName));
63
+ }
64
+ } catch {}
65
+ }
66
+ function appendToFile(label, msg) {
67
+ if (!fileLoggingEnabled)
68
+ return;
69
+ try {
70
+ ensureLogDir();
71
+ const filePath = join(logDir, LOG_FILENAME);
72
+ rotateIfNeeded(filePath);
73
+ const timestamp = new Date().toISOString();
74
+ const line = `[${timestamp}] ${label} ${msg}
75
+ `;
76
+ appendFileSync(filePath, line, "utf-8");
77
+ } catch {}
78
+ }
79
+ function hookLogInfo(msg) {
80
+ if (!shouldEmit("info"))
81
+ return;
82
+ emitStderr("INFO", msg);
83
+ appendToFile("INFO", msg);
84
+ }
85
+ function hookLogWarn(msg) {
86
+ if (!shouldEmit("warn"))
87
+ return;
88
+ emitStderr("WARN", msg);
89
+ appendToFile("WARN", msg);
90
+ }
91
+ function hookLogError(msg) {
92
+ if (!shouldEmit("error"))
93
+ return;
94
+ emitStderr("ERROR", msg);
95
+ appendToFile("ERROR", msg);
96
+ }
97
+ var LEVEL_ORDER, MAX_FILE_SIZE, LOG_FILENAME = "hooks.log", DEFAULT_LOG_DIR, resolved = false, currentLevel = "warn", fileLoggingEnabled = false, logDir;
98
+ var init_hook_logger = __esm(() => {
99
+ LEVEL_ORDER = { info: 0, warn: 1, error: 2 };
100
+ MAX_FILE_SIZE = 512 * 1024;
101
+ DEFAULT_LOG_DIR = join(homedir(), ".failproofai", "logs");
102
+ logDir = DEFAULT_LOG_DIR;
103
+ });
104
+
105
+ // src/hooks/hooks-config.ts
106
+ import { readFileSync, writeFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
107
+ import { resolve, dirname } from "node:path";
108
+ import { homedir as homedir2 } from "node:os";
109
+ function readConfigAt(path) {
110
+ if (!existsSync2(path))
111
+ return {};
112
+ try {
113
+ const raw = readFileSync(path, "utf8");
114
+ return JSON.parse(raw);
115
+ } catch (err) {
116
+ hookLogWarn(`failed to parse config at ${path}: ${err instanceof Error ? err.message : String(err)}`);
117
+ return {};
118
+ }
119
+ }
120
+ function readMergedHooksConfig(cwd) {
121
+ const base = cwd ? resolve(cwd) : process.cwd();
122
+ const projectPath = resolve(base, ".failproofai", "policies-config.json");
123
+ const localPath = resolve(base, ".failproofai", "policies-config.local.json");
124
+ const globalPath = resolve(homedir2(), ".failproofai", "policies-config.json");
125
+ const project = readConfigAt(projectPath);
126
+ const local = readConfigAt(localPath);
127
+ const global_ = readConfigAt(globalPath);
128
+ const enabledSet = new Set([
129
+ ...project.enabledPolicies ?? [],
130
+ ...local.enabledPolicies ?? [],
131
+ ...global_.enabledPolicies ?? []
132
+ ]);
133
+ const mergedParams = {};
134
+ for (const scope of [project, local, global_]) {
135
+ if (!scope.policyParams)
136
+ continue;
137
+ for (const [policyName, params] of Object.entries(scope.policyParams)) {
138
+ if (!(policyName in mergedParams)) {
139
+ mergedParams[policyName] = params;
140
+ }
141
+ }
142
+ }
143
+ const customPoliciesPath = project.customPoliciesPath ?? local.customPoliciesPath ?? global_.customPoliciesPath;
144
+ const llm = project.llm ?? local.llm ?? global_.llm;
145
+ return {
146
+ enabledPolicies: [...enabledSet],
147
+ ...Object.keys(mergedParams).length > 0 ? { policyParams: mergedParams } : {},
148
+ ...customPoliciesPath !== undefined ? { customPoliciesPath } : {},
149
+ ...llm !== undefined ? { llm } : {}
150
+ };
151
+ }
152
+ function getConfigPath() {
153
+ return resolve(homedir2(), ".failproofai", "policies-config.json");
154
+ }
155
+ function readHooksConfig() {
156
+ const configPath = getConfigPath();
157
+ if (!existsSync2(configPath)) {
158
+ return { enabledPolicies: [] };
159
+ }
160
+ try {
161
+ const raw = readFileSync(configPath, "utf8");
162
+ return JSON.parse(raw);
163
+ } catch (err) {
164
+ hookLogWarn(`failed to parse config at ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
165
+ return { enabledPolicies: [] };
166
+ }
167
+ }
168
+ function writeHooksConfig(config) {
169
+ const configPath = getConfigPath();
170
+ const dir = dirname(configPath);
171
+ if (!existsSync2(dir)) {
172
+ mkdirSync2(dir, { recursive: true });
173
+ }
174
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + `
175
+ `, "utf8");
176
+ }
177
+ var init_hooks_config = __esm(() => {
178
+ init_hook_logger();
179
+ });
180
+
181
+ // src/hooks/policy-helpers.ts
182
+ function allow() {
183
+ return { decision: "allow" };
184
+ }
185
+ function deny(reason) {
186
+ return { decision: "deny", reason };
187
+ }
188
+ function instruct(reason) {
189
+ return { decision: "instruct", reason };
190
+ }
191
+
192
+ // src/hooks/policy-registry.ts
193
+ function getIndexCache() {
194
+ return globalThis[INDEX_CACHE_KEY];
195
+ }
196
+ function setIndexCache(cache) {
197
+ globalThis[INDEX_CACHE_KEY] = cache;
198
+ }
199
+ function getRegistry() {
200
+ const g = globalThis;
201
+ if (!g[REGISTRY_KEY]) {
202
+ g[REGISTRY_KEY] = [];
203
+ }
204
+ return g[REGISTRY_KEY];
205
+ }
206
+ function registerPolicy(name, description, fn, match, priority = 0) {
207
+ const registry = getRegistry();
208
+ const idx = registry.findIndex((p) => p.name === name);
209
+ const entry = { name, description, fn, match, priority };
210
+ if (idx >= 0) {
211
+ registry[idx] = entry;
212
+ } else {
213
+ registry.push(entry);
214
+ }
215
+ setIndexCache(null);
216
+ }
217
+ function getPoliciesForEvent(eventType, toolName) {
218
+ let cache = getIndexCache();
219
+ if (!cache) {
220
+ cache = new Map;
221
+ setIndexCache(cache);
222
+ }
223
+ const key = `${eventType}:${toolName ?? ""}`;
224
+ const cached = cache.get(key);
225
+ if (cached)
226
+ return cached;
227
+ const result = getRegistry().filter((p) => {
228
+ if (p.match.events && p.match.events.length > 0) {
229
+ if (!p.match.events.includes(eventType))
230
+ return false;
231
+ }
232
+ if (p.match.toolNames && p.match.toolNames.length > 0) {
233
+ if (!toolName || !p.match.toolNames.includes(toolName))
234
+ return false;
235
+ }
236
+ return true;
237
+ }).sort((a, b) => b.priority - a.priority);
238
+ cache.set(key, result);
239
+ return result;
240
+ }
241
+ function clearPolicies() {
242
+ const g = globalThis;
243
+ g[REGISTRY_KEY] = [];
244
+ setIndexCache(null);
245
+ }
246
+ var REGISTRY_KEY = "__FAILPROOFAI_POLICY_REGISTRY__", INDEX_CACHE_KEY = "__FAILPROOFAI_POLICY_INDEX_CACHE__";
247
+
248
+ // src/hooks/builtin-policies.ts
249
+ import { resolve as resolve2, join as join2 } from "node:path";
250
+ import { readFile, writeFile } from "node:fs/promises";
251
+ import { execSync } from "node:child_process";
252
+ import { homedir as homedir3 } from "node:os";
253
+ function isClaudeInternalPath(resolved2) {
254
+ const claudeDir = join2(homedir3(), ".claude");
255
+ return resolved2 === claudeDir || resolved2.startsWith(claudeDir + "/");
256
+ }
257
+ function isClaudeSettingsFile(resolved2) {
258
+ return /[\\/]\.claude[\\/]settings(?:\.[^/\\]+)?\.json$/.test(resolved2);
259
+ }
260
+ function getCommand(ctx) {
261
+ return ctx.toolInput?.command ?? "";
262
+ }
263
+ function getFilePath(ctx) {
264
+ return ctx.toolInput?.file_path ?? "";
265
+ }
266
+ function parseArgvTokens(cmd) {
267
+ return cmd.trim().split(/\s+/).map((t) => t.replace(/^['"]|['"]$/g, ""));
268
+ }
269
+ function matchesAllowedPattern(cmd, pattern) {
270
+ const cmdTokens = parseArgvTokens(cmd);
271
+ const patTokens = parseArgvTokens(pattern);
272
+ if (cmdTokens.length < patTokens.length)
273
+ return false;
274
+ if (cmdTokens.some((tok) => SHELL_OPERATORS.has(tok)))
275
+ return false;
276
+ if (cmdTokens.some((tok) => SHELL_METACHAR_RE.test(tok)))
277
+ return false;
278
+ return patTokens.every((tok, i) => tok === "*" || tok === cmdTokens[i]);
279
+ }
280
+ function sanitizeJwt(ctx) {
281
+ const output = JSON.stringify(ctx.payload);
282
+ if (JWT_RE.test(output)) {
283
+ return {
284
+ decision: "deny",
285
+ reason: "JWT token detected in tool output",
286
+ message: "[REDACTED: JWT token removed by failproofai]"
287
+ };
288
+ }
289
+ return allow();
290
+ }
291
+ function sanitizeApiKeys(ctx) {
292
+ const output = JSON.stringify(ctx.payload);
293
+ for (const [pattern, label] of API_KEY_PATTERNS) {
294
+ if (pattern.test(output)) {
295
+ return {
296
+ decision: "deny",
297
+ reason: `${label} detected in tool output`,
298
+ message: `[REDACTED: ${label} removed by failproofai]`
299
+ };
300
+ }
301
+ }
302
+ const additional = ctx.params?.additionalPatterns ?? [];
303
+ for (const { regex, label } of additional) {
304
+ try {
305
+ if (new RegExp(regex).test(output)) {
306
+ return {
307
+ decision: "deny",
308
+ reason: `${label} detected in tool output`,
309
+ message: `[REDACTED: ${label} removed by failproofai]`
310
+ };
311
+ }
312
+ } catch {
313
+ hookLogWarn(`additionalPatterns: invalid regex "${regex}", skipping`);
314
+ }
315
+ }
316
+ return allow();
317
+ }
318
+ function sanitizeConnectionStrings(ctx) {
319
+ const output = JSON.stringify(ctx.payload);
320
+ if (CONNECTION_STRING_RE.test(output)) {
321
+ return {
322
+ decision: "deny",
323
+ reason: "Database connection string with credentials detected in tool output",
324
+ message: "[REDACTED: connection string removed by failproofai]"
325
+ };
326
+ }
327
+ return allow();
328
+ }
329
+ function sanitizePrivateKeyContent(ctx) {
330
+ const output = JSON.stringify(ctx.payload);
331
+ if (PRIVATE_KEY_RE.test(output)) {
332
+ return {
333
+ decision: "deny",
334
+ reason: "Private key content detected in tool output",
335
+ message: "[REDACTED: private key content removed by failproofai]"
336
+ };
337
+ }
338
+ return allow();
339
+ }
340
+ function sanitizeBearerTokens(ctx) {
341
+ const output = JSON.stringify(ctx.payload);
342
+ if (BEARER_TOKEN_RE.test(output)) {
343
+ return {
344
+ decision: "deny",
345
+ reason: "Bearer token detected in tool output",
346
+ message: "[REDACTED: Bearer token removed by failproofai]"
347
+ };
348
+ }
349
+ return allow();
350
+ }
351
+ function warnDestructiveSql(ctx) {
352
+ if (ctx.toolName !== "Bash")
353
+ return allow();
354
+ const cmd = getCommand(ctx);
355
+ if (!SQL_TOOL_RE.test(cmd))
356
+ return allow();
357
+ if (DESTRUCTIVE_SQL_RE.test(cmd)) {
358
+ return instruct("STOP: This command contains destructive SQL (DROP/TRUNCATE/DELETE). Confirm with the user before executing.");
359
+ }
360
+ if (DELETE_NO_WHERE_RE.test(cmd) && !SQL_WHERE_RE.test(cmd)) {
361
+ return instruct("STOP: This command contains destructive SQL (DROP/TRUNCATE/DELETE). Confirm with the user before executing.");
362
+ }
363
+ return allow();
364
+ }
365
+ function warnLargeFileWrite(ctx) {
366
+ if (ctx.toolName !== "Write")
367
+ return allow();
368
+ const content = ctx.toolInput?.content;
369
+ if (typeof content !== "string")
370
+ return allow();
371
+ const thresholdKb = ctx.params?.thresholdKb ?? 1024;
372
+ const thresholdBytes = thresholdKb * 1024;
373
+ if (content.length > thresholdBytes) {
374
+ 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.`);
375
+ }
376
+ return allow();
377
+ }
378
+ function warnPackagePublish(ctx) {
379
+ if (ctx.toolName !== "Bash")
380
+ return allow();
381
+ const cmd = getCommand(ctx);
382
+ if (PUBLISH_CMD_RE.test(cmd)) {
383
+ return instruct("STOP: This command publishes a package to a public registry. Confirm with the user that this is intentional.");
384
+ }
385
+ return allow();
386
+ }
387
+ function protectEnvVars(ctx) {
388
+ if (ctx.toolName !== "Bash")
389
+ return allow();
390
+ const cmd = getCommand(ctx);
391
+ if (ENV_PRINTENV_RE.test(cmd)) {
392
+ return deny("Command reads environment variables");
393
+ }
394
+ if (ECHO_ENV_RE.test(cmd)) {
395
+ return deny("Command echoes environment variable");
396
+ }
397
+ if (EXPORT_RE.test(cmd)) {
398
+ return deny("Command exports environment variable");
399
+ }
400
+ if (PS_ENV_VAR_RE.test(cmd)) {
401
+ return deny("Command reads environment variable via PowerShell");
402
+ }
403
+ if (PS_CHILDITEM_ENV_RE.test(cmd)) {
404
+ return deny("Command reads environment variables via PowerShell");
405
+ }
406
+ if (DOTNET_GETENV_RE.test(cmd)) {
407
+ return deny("Command reads environment variable via .NET");
408
+ }
409
+ if (CMD_ECHO_ENV_RE.test(cmd)) {
410
+ return deny("Command echoes environment variable via cmd");
411
+ }
412
+ return allow();
413
+ }
414
+ function blockEnvFiles(ctx) {
415
+ const cmd = getCommand(ctx);
416
+ const filePath = getFilePath(ctx);
417
+ if (filePath && ENV_FILE_PATH_RE.test(filePath)) {
418
+ return deny("Access to .env file blocked");
419
+ }
420
+ if (ctx.toolName === "Bash" && ENV_CMD_RE.test(cmd)) {
421
+ return deny("Command references .env file");
422
+ }
423
+ return allow();
424
+ }
425
+ function blockSudo(ctx) {
426
+ if (ctx.toolName !== "Bash")
427
+ return allow();
428
+ const cmd = getCommand(ctx).trimStart();
429
+ if (SUDO_RE.test(cmd) || cmd.startsWith("sudo ")) {
430
+ const allowPatterns = ctx.params?.allowPatterns ?? [];
431
+ if (allowPatterns.some((p) => matchesAllowedPattern(cmd, p)))
432
+ return allow();
433
+ return deny("sudo commands are blocked");
434
+ }
435
+ if (PS_ELEVATION_RE.test(cmd)) {
436
+ return deny("Elevated process launch is blocked");
437
+ }
438
+ if (RUNAS_RE.test(cmd)) {
439
+ return deny("runas elevation is blocked");
440
+ }
441
+ return allow();
442
+ }
443
+ function blockCurlPipeSh(ctx) {
444
+ if (ctx.toolName !== "Bash")
445
+ return allow();
446
+ const cmd = getCommand(ctx);
447
+ if (CURL_PIPE_SH_RE.test(cmd)) {
448
+ return deny("Piping downloads to shell is blocked");
449
+ }
450
+ if (PS_WEB_PIPE_RE.test(cmd)) {
451
+ return deny("Piping downloads to Invoke-Expression is blocked");
452
+ }
453
+ return allow();
454
+ }
455
+ function extractGitPushArgs(cmd) {
456
+ return cmd.split(/&&|\|\||[|;\n]/).map((s) => s.trim()).filter((s) => /^git\s+push\s/.test(s)).map((s) => s.replace(/^git\s+push\s+/, ""));
457
+ }
458
+ function blockPushMaster(ctx) {
459
+ if (ctx.toolName !== "Bash")
460
+ return allow();
461
+ const protectedBranches = ctx.params?.protectedBranches ?? ["main", "master"];
462
+ if (protectedBranches.length === 0)
463
+ return allow();
464
+ const args = extractGitPushArgs(getCommand(ctx));
465
+ const branchPattern = new RegExp(`\\b(?:${protectedBranches.map((b) => b.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b`);
466
+ if (args.some((a) => branchPattern.test(a))) {
467
+ return deny(`Pushing to ${protectedBranches.join("/")} is blocked`);
468
+ }
469
+ return allow();
470
+ }
471
+ function rmTargetIsAllowed(cmd, allowPaths) {
472
+ if (allowPaths.length === 0)
473
+ return false;
474
+ const segments = cmd.split(/&&|\|\||[|;\n]/).map((s) => s.trim()).filter((s) => /\brm\b/.test(s));
475
+ if (segments.length === 0)
476
+ return false;
477
+ for (const seg of segments) {
478
+ const tokens = parseArgvTokens(seg);
479
+ const rmIdx = tokens.findIndex((t) => t === "rm");
480
+ if (rmIdx < 0)
481
+ continue;
482
+ const flagTokens = tokens.slice(rmIdx + 1).filter((t) => /^-[^-]/.test(t));
483
+ if (!/r/i.test(flagTokens.join("")))
484
+ continue;
485
+ const pathArgs = tokens.slice(rmIdx + 1).filter((t) => !t.startsWith("-"));
486
+ for (const target of pathArgs) {
487
+ const normalized = target.replace(/\/\*$/, "").replace(/\/+$/, "") || "/";
488
+ const covered = allowPaths.some((p) => {
489
+ const np = p.replace(/\/+$/, "") || "/";
490
+ return normalized === np || normalized.startsWith(np + "/");
491
+ });
492
+ if (!covered) {
493
+ const segCovered = allowPaths.some((p) => {
494
+ const escaped = p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
495
+ return new RegExp(`${escaped}(?:[/"'\\s/*]|$)`).test(seg);
496
+ });
497
+ if (!segCovered)
498
+ return false;
499
+ }
500
+ }
501
+ }
502
+ return true;
503
+ }
504
+ function blockRmRf(ctx) {
505
+ if (ctx.toolName !== "Bash")
506
+ return allow();
507
+ const cmd = getCommand(ctx);
508
+ const hasDestructivePath = /(?:\/\s*$|\/\*|~)/.test(cmd);
509
+ if (hasDestructivePath && (/rm\s+-[^\s]*r[^\s]*f[^\s]*/.test(cmd) || /rm\s+-[^\s]*f[^\s]*r[^\s]*/.test(cmd))) {
510
+ const allowPaths = ctx.params?.allowPaths ?? [];
511
+ if (rmTargetIsAllowed(cmd, allowPaths))
512
+ return allow();
513
+ return deny("Catastrophic deletion blocked");
514
+ }
515
+ if (hasDestructivePath && /\brm\b/.test(cmd)) {
516
+ const tokens = parseArgvTokens(cmd);
517
+ const shortFlags = tokens.filter((t) => /^-[^-]/.test(t)).join("");
518
+ if (/r/i.test(shortFlags) && /f/.test(shortFlags)) {
519
+ const allowPaths = ctx.params?.allowPaths ?? [];
520
+ if (rmTargetIsAllowed(cmd, allowPaths))
521
+ return allow();
522
+ return deny("Catastrophic deletion blocked");
523
+ }
524
+ }
525
+ if (/Remove-Item\s+.*-Recurse.*-Force.*(?:[A-Z]:\\(?:\s|$)|\\\*)/i.test(cmd)) {
526
+ return deny("Catastrophic deletion blocked");
527
+ }
528
+ if (/(?:rd|rmdir)\s+\/s\s+\/q\s+[A-Z]:\\/i.test(cmd)) {
529
+ return deny("Catastrophic deletion blocked");
530
+ }
531
+ return allow();
532
+ }
533
+ function blockForcePush(ctx) {
534
+ if (ctx.toolName !== "Bash")
535
+ return allow();
536
+ const args = extractGitPushArgs(getCommand(ctx));
537
+ if (args.some((a) => FORCE_PUSH_RE.test(a))) {
538
+ return deny("Force-pushing is blocked");
539
+ }
540
+ return allow();
541
+ }
542
+ function blockSecretsWrite(ctx) {
543
+ if (ctx.toolName !== "Write")
544
+ return allow();
545
+ const filePath = getFilePath(ctx);
546
+ if (SECRET_FILE_RE.test(filePath) || SECRET_FILE_ID_RSA_RE.test(filePath) || SECRET_FILE_CREDENTIALS_RE.test(filePath)) {
547
+ return deny("Writing secret key files is blocked");
548
+ }
549
+ const additionalPatterns = ctx.params?.additionalPatterns ?? [];
550
+ for (const pattern of additionalPatterns) {
551
+ if (filePath.includes(pattern)) {
552
+ return deny(`Writing blocked file pattern: ${pattern}`);
553
+ }
554
+ }
555
+ return allow();
556
+ }
557
+ function extractAbsolutePaths(command) {
558
+ const paths = [];
559
+ const pathRe = /(?<![a-zA-Z0-9_.\-~\\])(?:~\/[^\s;|&"'()\[\]{}]*|~(?=\s|$|[;|&"'()\[\]{}])|\/[^\s;|&"'()\[\]{}]*)/g;
560
+ function addPaths(s) {
561
+ pathRe.lastIndex = 0;
562
+ let m;
563
+ while ((m = pathRe.exec(s)) !== null) {
564
+ let p = m[0];
565
+ if (p === "~")
566
+ p = homedir3();
567
+ else if (p.startsWith("~/"))
568
+ p = join2(homedir3(), p.slice(2));
569
+ paths.push(p);
570
+ }
571
+ }
572
+ let firstBarePipe = command.length;
573
+ let inDouble = false, inSingle = false;
574
+ for (let i = 0;i < command.length; i++) {
575
+ const c = command[i];
576
+ if (c === '"' && !inSingle)
577
+ inDouble = !inDouble;
578
+ else if (c === "'" && !inDouble)
579
+ inSingle = !inSingle;
580
+ else if (c === "|" && !inDouble && !inSingle) {
581
+ firstBarePipe = i;
582
+ break;
583
+ }
584
+ }
585
+ const firstSegment = command.slice(0, firstBarePipe);
586
+ const quotedRe = /"([^"]*)"|'([^']*)'/g;
587
+ let qm;
588
+ while ((qm = quotedRe.exec(firstSegment)) !== null) {
589
+ const content = qm[1] ?? qm[2] ?? "";
590
+ if (/[*?\[\]^$+()\\]/.test(content))
591
+ continue;
592
+ addPaths(content);
593
+ }
594
+ const stripped = command.replace(/"[^"]*"/g, (m) => " ".repeat(m.length)).replace(/'[^']*'/g, (m) => " ".repeat(m.length));
595
+ addPaths(stripped);
596
+ return paths;
597
+ }
598
+ function blockReadOutsideCwd(ctx) {
599
+ const cwd = ctx.session?.cwd;
600
+ if (!cwd)
601
+ return allow();
602
+ const allowPaths = ctx.params?.allowPaths ?? [];
603
+ if (ctx.toolName === "Bash") {
604
+ const cmd = getCommand(ctx);
605
+ if (!READ_LIKE_CMDS.test(cmd))
606
+ return allow();
607
+ const paths = extractAbsolutePaths(cmd);
608
+ const cwdWithSep2 = cwd.endsWith("/") ? cwd : cwd + "/";
609
+ for (const p of paths) {
610
+ const resolved3 = resolve2(cwd, p);
611
+ if (isClaudeSettingsFile(resolved3)) {
612
+ return deny(`Reading Claude settings file blocked: ${resolved3}`);
613
+ }
614
+ if (isClaudeInternalPath(resolved3))
615
+ continue;
616
+ if (resolved3 === "/dev/null")
617
+ continue;
618
+ if (resolved3 !== cwd && !resolved3.startsWith(cwdWithSep2)) {
619
+ if (allowPaths.some((ap) => resolved3 === ap || resolved3.startsWith(ap.endsWith("/") ? ap : ap + "/")))
620
+ continue;
621
+ return deny(`Bash read outside project directory blocked: ${resolved3}`);
622
+ }
623
+ }
624
+ return allow();
625
+ }
626
+ const filePath = getFilePath(ctx);
627
+ const searchPath = ctx.toolInput?.path ?? "";
628
+ const target = filePath || searchPath;
629
+ if (!target)
630
+ return allow();
631
+ const resolved2 = resolve2(cwd, target);
632
+ if (isClaudeSettingsFile(resolved2)) {
633
+ return deny(`Reading Claude settings file blocked: ${resolved2}`);
634
+ }
635
+ if (isClaudeInternalPath(resolved2))
636
+ return allow();
637
+ if (resolved2 === "/dev/null")
638
+ return allow();
639
+ const cwdWithSep = cwd.endsWith("/") ? cwd : cwd + "/";
640
+ if (resolved2 !== cwd && !resolved2.startsWith(cwdWithSep)) {
641
+ if (allowPaths.some((ap) => resolved2 === ap || resolved2.startsWith(ap.endsWith("/") ? ap : ap + "/")))
642
+ return allow();
643
+ return deny(`Access outside project directory blocked: ${resolved2}`);
644
+ }
645
+ return allow();
646
+ }
647
+ function blockWorkOnMain(ctx) {
648
+ if (ctx.toolName !== "Bash")
649
+ return allow();
650
+ const cmd = getCommand(ctx);
651
+ if (!GIT_COMMIT_MERGE_RE.test(cmd))
652
+ return allow();
653
+ const cwd = ctx.session?.cwd;
654
+ if (!cwd)
655
+ return allow();
656
+ try {
657
+ let branch = gitBranchCache.get(cwd);
658
+ if (branch === undefined) {
659
+ branch = execSync("git rev-parse --abbrev-ref HEAD", {
660
+ cwd,
661
+ encoding: "utf8",
662
+ timeout: 3000
663
+ }).trim();
664
+ gitBranchCache.set(cwd, branch);
665
+ }
666
+ const protectedBranches = ctx.params?.protectedBranches ?? ["main", "master"];
667
+ if (protectedBranches.includes(branch)) {
668
+ return deny(`Git ${cmd.match(/git\s+(\S+)/)?.[1] ?? "operation"} on ${branch} is blocked. Create a feature branch first.`);
669
+ }
670
+ } catch {
671
+ return allow();
672
+ }
673
+ return allow();
674
+ }
675
+ function blockFailproofaiCommands(ctx) {
676
+ if (ctx.toolName !== "Bash")
677
+ return allow();
678
+ const cmd = getCommand(ctx);
679
+ if (FAILPROOFAI_CLI_RE.test(cmd)) {
680
+ return deny("Running failproofai CLI commands is blocked");
681
+ }
682
+ if (FAILPROOFAI_UNINSTALL_RE.test(cmd)) {
683
+ return deny("Uninstalling failproofai is blocked");
684
+ }
685
+ return allow();
686
+ }
687
+ async function warnRepeatedToolCalls(ctx) {
688
+ const THRESHOLD = 3;
689
+ const transcriptPath = ctx.session?.transcriptPath;
690
+ if (!transcriptPath || !ctx.toolName || !ctx.toolInput)
691
+ return allow();
692
+ const trackerPath = `${transcriptPath}.tool-calls.json`;
693
+ const fingerprint = JSON.stringify({ tool: ctx.toolName, input: ctx.toolInput });
694
+ let counts = {};
695
+ try {
696
+ const raw = await readFile(trackerPath, "utf8");
697
+ counts = JSON.parse(raw);
698
+ } catch {}
699
+ const prevCount = counts[fingerprint] ?? 0;
700
+ if (prevCount >= THRESHOLD) {
701
+ 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.`);
702
+ }
703
+ counts[fingerprint] = prevCount + 1;
704
+ try {
705
+ const serialized = JSON.stringify(counts);
706
+ if (serialized.length <= TOOL_CALL_TRACKER_MAX_BYTES) {
707
+ await writeFile(trackerPath, serialized, "utf8");
708
+ }
709
+ } catch {}
710
+ return allow();
711
+ }
712
+ function warnGitAmend(ctx) {
713
+ if (ctx.toolName !== "Bash")
714
+ return allow();
715
+ const cmd = getCommand(ctx);
716
+ if (GIT_AMEND_RE.test(cmd)) {
717
+ 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.");
718
+ }
719
+ return allow();
720
+ }
721
+ function warnGitStashDrop(ctx) {
722
+ if (ctx.toolName !== "Bash")
723
+ return allow();
724
+ const cmd = getCommand(ctx);
725
+ if (GIT_STASH_DROP_RE.test(cmd)) {
726
+ 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.");
727
+ }
728
+ return allow();
729
+ }
730
+ function warnAllFilesStaged(ctx) {
731
+ if (ctx.toolName !== "Bash")
732
+ return allow();
733
+ const cmd = getCommand(ctx);
734
+ if (GIT_ADD_ALL_RE.test(cmd)) {
735
+ 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.");
736
+ }
737
+ return allow();
738
+ }
739
+ function warnSchemaAlteration(ctx) {
740
+ if (ctx.toolName !== "Bash")
741
+ return allow();
742
+ const cmd = getCommand(ctx);
743
+ if (!SQL_TOOL_RE.test(cmd))
744
+ return allow();
745
+ if (SCHEMA_ALTER_RE.test(cmd)) {
746
+ 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.");
747
+ }
748
+ return allow();
749
+ }
750
+ function warnGlobalPackageInstall(ctx) {
751
+ if (ctx.toolName !== "Bash")
752
+ return allow();
753
+ const cmd = getCommand(ctx);
754
+ 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);
755
+ if (isGlobal) {
756
+ 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.");
757
+ }
758
+ return allow();
759
+ }
760
+ function warnBackgroundProcess(ctx) {
761
+ if (ctx.toolName !== "Bash")
762
+ return allow();
763
+ const cmd = getCommand(ctx);
764
+ 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);
765
+ if (isBackground) {
766
+ 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.");
767
+ }
768
+ return allow();
769
+ }
770
+ function registerBuiltinPolicies(enabledNames) {
771
+ const enabledSet = new Set(enabledNames);
772
+ for (const policy of BUILTIN_POLICIES) {
773
+ if (enabledSet.has(policy.name)) {
774
+ registerPolicy(policy.name, policy.description, policy.fn, policy.match);
775
+ }
776
+ }
777
+ }
778
+ var SHELL_OPERATORS, SHELL_METACHAR_RE, JWT_RE, API_KEY_PATTERNS, CONNECTION_STRING_RE, PRIVATE_KEY_RE, BEARER_TOKEN_RE, SQL_TOOL_RE, DESTRUCTIVE_SQL_RE, DELETE_NO_WHERE_RE, SQL_WHERE_RE, SCHEMA_ALTER_RE, PUBLISH_CMD_RE, ENV_PRINTENV_RE, ECHO_ENV_RE, EXPORT_RE, PS_ENV_VAR_RE, PS_CHILDITEM_ENV_RE, DOTNET_GETENV_RE, CMD_ECHO_ENV_RE, ENV_FILE_PATH_RE, ENV_CMD_RE, SUDO_RE, PS_ELEVATION_RE, RUNAS_RE, CURL_PIPE_SH_RE, PS_WEB_PIPE_RE, FORCE_PUSH_RE, SECRET_FILE_RE, SECRET_FILE_ID_RSA_RE, SECRET_FILE_CREDENTIALS_RE, GIT_COMMIT_MERGE_RE, FAILPROOFAI_CLI_RE, FAILPROOFAI_UNINSTALL_RE, GIT_AMEND_RE, GIT_STASH_DROP_RE, GIT_ADD_ALL_RE, NPM_GLOBAL_RE, YARN_GLOBAL_RE, PNPM_GLOBAL_RE, BUN_GLOBAL_RE, CARGO_INSTALL_RE, PIP_SYSTEM_RE, NOHUP_RE, SCREEN_DETACH_RE, TMUX_DETACH_RE, DISOWN_RE, BACKGROUND_AMPERSAND_RE, gitBranchCache, READ_LIKE_CMDS, TOOL_CALL_TRACKER_MAX_BYTES = 65536, BUILTIN_POLICIES;
779
+ var init_builtin_policies = __esm(() => {
780
+ init_hook_logger();
781
+ SHELL_OPERATORS = new Set(["&&", "||", "|", ";"]);
782
+ SHELL_METACHAR_RE = /[;&<>`$()\\]/;
783
+ JWT_RE = /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/;
784
+ API_KEY_PATTERNS = [
785
+ [/sk-ant-[A-Za-z0-9\-_]{20,}/, "Anthropic API key"],
786
+ [/sk-proj-[A-Za-z0-9\-_]{20,}/, "OpenAI project API key"],
787
+ [/sk-[A-Za-z0-9]{20,}/, "OpenAI API key"],
788
+ [/ghp_[A-Za-z0-9]{36}/, "GitHub personal access token"],
789
+ [/github_pat_[A-Za-z0-9_]{82}/, "GitHub fine-grained token"],
790
+ [/AKIA[A-Z0-9]{16}/, "AWS access key ID"],
791
+ [/sk_live_[A-Za-z0-9]{24,}/, "Stripe live secret key"],
792
+ [/sk_test_[A-Za-z0-9]{24,}/, "Stripe test secret key"],
793
+ [/AIza[0-9A-Za-z\-_]{35}/, "Google API key"]
794
+ ];
795
+ CONNECTION_STRING_RE = /(?:postgresql|postgres|mysql|mongodb(?:\+srv)?|redis|amqps?|smtps?):\/\/[^@\s]+@/;
796
+ PRIVATE_KEY_RE = /-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----/;
797
+ BEARER_TOKEN_RE = /Authorization:\s*Bearer\s+[A-Za-z0-9\-._~+/]{20,}/i;
798
+ SQL_TOOL_RE = /\b(?:psql|mysql|sqlite3|pgcli|clickhouse-client)\b/;
799
+ DESTRUCTIVE_SQL_RE = /\b(?:DROP\s+(?:TABLE|DATABASE|SCHEMA)|TRUNCATE\b)/i;
800
+ DELETE_NO_WHERE_RE = /\bDELETE\s+FROM\b/i;
801
+ SQL_WHERE_RE = /\bWHERE\b/i;
802
+ 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;
803
+ 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/;
804
+ ENV_PRINTENV_RE = /(?:^|\s|;|&&|\|\|)(?:env|printenv)(?:\s|$|;|&&|\|)/;
805
+ ECHO_ENV_RE = /echo\s+.*\$[A-Za-z_]/;
806
+ EXPORT_RE = /(?:^|\s|;|&&|\|\|)export\s+\w+/;
807
+ PS_ENV_VAR_RE = /\$env:[A-Za-z_]/i;
808
+ PS_CHILDITEM_ENV_RE = /(?:Get-ChildItem|dir|gci|ls)\s+Env:/i;
809
+ DOTNET_GETENV_RE = /\[Environment\]::GetEnvironment/i;
810
+ CMD_ECHO_ENV_RE = /echo\s+%[A-Za-z_]/i;
811
+ ENV_FILE_PATH_RE = /(?:^|[\\/])\.env(?:\.|$)/;
812
+ ENV_CMD_RE = /\.env(?:\b|\s|$|\.)/;
813
+ SUDO_RE = /(?:^|;|&&|\|\|)\s*sudo\s/;
814
+ PS_ELEVATION_RE = /Start-Process\s+.*-Verb\s+RunAs/i;
815
+ RUNAS_RE = /(?:^|;|&&|\|\|)\s*runas\s/i;
816
+ CURL_PIPE_SH_RE = /(?:curl|wget)\s.*\|\s*(?:sh|bash|zsh)/;
817
+ PS_WEB_PIPE_RE = /(?:Invoke-WebRequest|iwr|Invoke-RestMethod|irm)\s+.*\|\s*(?:Invoke-Expression|iex)/i;
818
+ FORCE_PUSH_RE = /(?:--force|-f\b)/;
819
+ SECRET_FILE_RE = /\.(?:pem|key)$/;
820
+ SECRET_FILE_ID_RSA_RE = /id_rsa/;
821
+ SECRET_FILE_CREDENTIALS_RE = /credentials/;
822
+ GIT_COMMIT_MERGE_RE = /git\s+(?:commit|merge|rebase|cherry-pick)\b/;
823
+ FAILPROOFAI_CLI_RE = /(?:^|;|&&|\|\||\|)\s*failproofai(?:\s|$)/;
824
+ 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)/;
825
+ GIT_AMEND_RE = /\bgit\s+commit\b.*--amend\b/;
826
+ GIT_STASH_DROP_RE = /\bgit\s+stash\s+(?:drop|clear)\b/;
827
+ GIT_ADD_ALL_RE = /\bgit\s+add\s+(?:-A\b|--all\b|\.(?:\s|$|;|&&|\|\|))/;
828
+ NPM_GLOBAL_RE = /\bnpm\s+(?:install|i)\b(?=.*(?:\s-g\b|--global\b))/;
829
+ YARN_GLOBAL_RE = /\byarn\s+global\s+add\b/;
830
+ PNPM_GLOBAL_RE = /\bpnpm\s+(?:add|install|i)\b(?=.*(?:\s-g\b|--global\b))/;
831
+ BUN_GLOBAL_RE = /\bbun\s+(?:install|add)\b(?=.*(?:\s-g\b|--global\b))/;
832
+ CARGO_INSTALL_RE = /\bcargo\s+install\b/;
833
+ PIP_SYSTEM_RE = /\bpip(?:3)?\s+install\b(?=.*(?:--user\b|--break-system-packages\b))/;
834
+ NOHUP_RE = /\bnohup\s+\S/;
835
+ SCREEN_DETACH_RE = /\bscreen\s+-[A-Za-z]*d[A-Za-z]*\b/;
836
+ TMUX_DETACH_RE = /\btmux\s+(?:new-session|new)\b[^|&;]*-d\b/;
837
+ DISOWN_RE = /\bdisown\b/;
838
+ BACKGROUND_AMPERSAND_RE = /(?<![&|])\s?&\s*(?:$|#|;)/;
839
+ gitBranchCache = new Map;
840
+ READ_LIKE_CMDS = /(?:^|;|&&|\|\||\|)\s*(?:ls|find|cat|head|tail|less|more|wc|file|stat|tree|du)\s/;
841
+ BUILTIN_POLICIES = [
842
+ {
843
+ name: "sanitize-jwt",
844
+ description: "Stop Claude from reading JWTs in tool responses",
845
+ fn: sanitizeJwt,
846
+ match: { events: ["PostToolUse"] },
847
+ defaultEnabled: true,
848
+ category: "Sanitize"
849
+ },
850
+ {
851
+ name: "sanitize-api-keys",
852
+ description: "Stop Claude from reading API keys (OpenAI, Anthropic, GitHub, AWS, Stripe, Google) in tool responses",
853
+ fn: sanitizeApiKeys,
854
+ match: { events: ["PostToolUse"] },
855
+ defaultEnabled: true,
856
+ category: "Sanitize",
857
+ params: {
858
+ additionalPatterns: {
859
+ type: "pattern[]",
860
+ description: "Additional API key patterns to scrub, each with { regex, label }",
861
+ default: []
862
+ }
863
+ }
864
+ },
865
+ {
866
+ name: "sanitize-connection-strings",
867
+ description: "Stop Claude from reading database connection strings with embedded credentials in tool responses",
868
+ fn: sanitizeConnectionStrings,
869
+ match: { events: ["PostToolUse"] },
870
+ defaultEnabled: true,
871
+ category: "Sanitize"
872
+ },
873
+ {
874
+ name: "sanitize-private-key-content",
875
+ description: "Stop Claude from reading PEM private key content in tool responses",
876
+ fn: sanitizePrivateKeyContent,
877
+ match: { events: ["PostToolUse"] },
878
+ defaultEnabled: true,
879
+ category: "Sanitize"
880
+ },
881
+ {
882
+ name: "sanitize-bearer-tokens",
883
+ description: "Stop Claude from reading Authorization Bearer tokens in tool responses",
884
+ fn: sanitizeBearerTokens,
885
+ match: { events: ["PostToolUse"] },
886
+ defaultEnabled: true,
887
+ category: "Sanitize"
888
+ },
889
+ {
890
+ name: "protect-env-vars",
891
+ description: "Prevent commands that read environment variables",
892
+ fn: protectEnvVars,
893
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
894
+ defaultEnabled: true,
895
+ category: "Environment"
896
+ },
897
+ {
898
+ name: "block-env-files",
899
+ description: "Block reading/writing .env files",
900
+ fn: blockEnvFiles,
901
+ match: { events: ["PreToolUse"] },
902
+ defaultEnabled: true,
903
+ category: "Environment"
904
+ },
905
+ {
906
+ name: "block-read-outside-cwd",
907
+ description: "Block file reads outside the session working directory",
908
+ fn: blockReadOutsideCwd,
909
+ match: { events: ["PreToolUse"], toolNames: ["Read", "Glob", "Grep", "Bash"] },
910
+ defaultEnabled: false,
911
+ category: "Environment",
912
+ params: {
913
+ allowPaths: {
914
+ type: "string[]",
915
+ description: "Absolute paths outside cwd that are allowed to be read",
916
+ default: []
917
+ }
918
+ }
919
+ },
920
+ {
921
+ name: "block-sudo",
922
+ description: "Block sudo commands",
923
+ fn: blockSudo,
924
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
925
+ defaultEnabled: true,
926
+ category: "Dangerous Commands",
927
+ params: {
928
+ allowPatterns: {
929
+ type: "string[]",
930
+ description: "Sudo command patterns to allow, matched token-by-token (e.g. 'sudo systemctl status')",
931
+ default: []
932
+ }
933
+ }
934
+ },
935
+ {
936
+ name: "block-curl-pipe-sh",
937
+ description: "Block piping downloads to shell",
938
+ fn: blockCurlPipeSh,
939
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
940
+ defaultEnabled: true,
941
+ category: "Dangerous Commands"
942
+ },
943
+ {
944
+ name: "block-rm-rf",
945
+ description: "Prevent catastrophic deletions",
946
+ fn: blockRmRf,
947
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
948
+ defaultEnabled: false,
949
+ category: "Dangerous Commands",
950
+ params: {
951
+ allowPaths: {
952
+ type: "string[]",
953
+ description: "Paths that are allowed to be recursively deleted",
954
+ default: []
955
+ }
956
+ }
957
+ },
958
+ {
959
+ name: "block-failproofai-commands",
960
+ description: "Block failproofai CLI commands and uninstallation",
961
+ fn: blockFailproofaiCommands,
962
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
963
+ defaultEnabled: true,
964
+ category: "Dangerous Commands"
965
+ },
966
+ {
967
+ name: "block-secrets-write",
968
+ description: "Block writing secret key files",
969
+ fn: blockSecretsWrite,
970
+ match: { events: ["PreToolUse"], toolNames: ["Write"] },
971
+ defaultEnabled: false,
972
+ category: "Dangerous Commands",
973
+ params: {
974
+ additionalPatterns: {
975
+ type: "string[]",
976
+ description: "Additional filename patterns (substrings) to block",
977
+ default: []
978
+ }
979
+ }
980
+ },
981
+ {
982
+ name: "block-push-master",
983
+ description: "Block pushing to main/master",
984
+ fn: blockPushMaster,
985
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
986
+ defaultEnabled: true,
987
+ category: "Git",
988
+ params: {
989
+ protectedBranches: {
990
+ type: "string[]",
991
+ description: "Branch names to protect from direct pushes",
992
+ default: ["main", "master"]
993
+ }
994
+ }
995
+ },
996
+ {
997
+ name: "block-force-push",
998
+ description: "Prevent force-pushing to any branch",
999
+ fn: blockForcePush,
1000
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1001
+ defaultEnabled: false,
1002
+ category: "Git"
1003
+ },
1004
+ {
1005
+ name: "block-work-on-main",
1006
+ description: "Block git commits and merges on main/master branch",
1007
+ fn: blockWorkOnMain,
1008
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1009
+ defaultEnabled: false,
1010
+ category: "Git",
1011
+ params: {
1012
+ protectedBranches: {
1013
+ type: "string[]",
1014
+ description: "Branch names where commits/merges are blocked",
1015
+ default: ["main", "master"]
1016
+ }
1017
+ }
1018
+ },
1019
+ {
1020
+ name: "warn-git-amend",
1021
+ description: "Warns before amending git commits, which rewrites history",
1022
+ fn: warnGitAmend,
1023
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1024
+ defaultEnabled: false,
1025
+ category: "Git"
1026
+ },
1027
+ {
1028
+ name: "warn-git-stash-drop",
1029
+ description: "Warns before permanently deleting stashed changes",
1030
+ fn: warnGitStashDrop,
1031
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1032
+ defaultEnabled: false,
1033
+ category: "Git"
1034
+ },
1035
+ {
1036
+ name: "warn-all-files-staged",
1037
+ description: "Warns before staging all working tree files with git add -A / . / --all",
1038
+ fn: warnAllFilesStaged,
1039
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1040
+ defaultEnabled: false,
1041
+ category: "Git"
1042
+ },
1043
+ {
1044
+ name: "warn-destructive-sql",
1045
+ description: "Warn before executing destructive SQL (DROP/TRUNCATE/DELETE without WHERE) via database clients",
1046
+ fn: warnDestructiveSql,
1047
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1048
+ defaultEnabled: false,
1049
+ category: "Database"
1050
+ },
1051
+ {
1052
+ name: "warn-schema-alteration",
1053
+ description: "Warns before SQL schema changes (ALTER TABLE with column or rename operations)",
1054
+ fn: warnSchemaAlteration,
1055
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1056
+ defaultEnabled: false,
1057
+ category: "Database"
1058
+ },
1059
+ {
1060
+ name: "warn-package-publish",
1061
+ description: "Warn before publishing packages to public registries (npm, PyPI, crates.io, RubyGems, etc.)",
1062
+ fn: warnPackagePublish,
1063
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1064
+ defaultEnabled: false,
1065
+ category: "Packages & System"
1066
+ },
1067
+ {
1068
+ name: "warn-global-package-install",
1069
+ description: "Warns before installing packages globally (npm -g, cargo install, etc.)",
1070
+ fn: warnGlobalPackageInstall,
1071
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1072
+ defaultEnabled: false,
1073
+ category: "Packages & System"
1074
+ },
1075
+ {
1076
+ name: "warn-large-file-write",
1077
+ description: "Warn before writing files larger than 1MB (configurable via thresholdKb param)",
1078
+ fn: warnLargeFileWrite,
1079
+ match: { events: ["PreToolUse"], toolNames: ["Write"] },
1080
+ defaultEnabled: false,
1081
+ category: "Packages & System",
1082
+ params: {
1083
+ thresholdKb: {
1084
+ type: "number",
1085
+ description: "File size threshold in KB above which a warning is issued",
1086
+ default: 1024
1087
+ }
1088
+ }
1089
+ },
1090
+ {
1091
+ name: "warn-background-process",
1092
+ description: "Warns before starting detached or background processes",
1093
+ fn: warnBackgroundProcess,
1094
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1095
+ defaultEnabled: false,
1096
+ category: "Packages & System"
1097
+ },
1098
+ {
1099
+ name: "warn-repeated-tool-calls",
1100
+ description: "Warn when the same tool is called 3+ times with identical parameters",
1101
+ fn: warnRepeatedToolCalls,
1102
+ match: { events: ["PreToolUse"] },
1103
+ defaultEnabled: false,
1104
+ category: "AI Behavior"
1105
+ }
1106
+ ];
1107
+ });
1108
+
1109
+ // src/hooks/policy-evaluator.ts
1110
+ async function evaluatePolicies(eventType, payload, session, config) {
1111
+ const toolName = payload.tool_name;
1112
+ const toolInput = payload.tool_input;
1113
+ const policies = getPoliciesForEvent(eventType, toolName);
1114
+ hookLogInfo(`evaluating ${policies.length} policies for ${eventType}`);
1115
+ if (policies.length === 0) {
1116
+ return { exitCode: 0, stdout: "", stderr: "", policyName: null, reason: null, decision: "allow" };
1117
+ }
1118
+ const baseCtx = {
1119
+ eventType,
1120
+ payload,
1121
+ toolName,
1122
+ toolInput,
1123
+ session
1124
+ };
1125
+ let instructPolicyName = null;
1126
+ let instructReason = null;
1127
+ for (const policy of policies) {
1128
+ const schema = POLICY_PARAMS_MAP.get(policy.name);
1129
+ let ctx;
1130
+ if (schema) {
1131
+ const userParams = config?.policyParams?.[policy.name] ?? {};
1132
+ const resolvedParams = {};
1133
+ for (const [key, spec] of Object.entries(schema)) {
1134
+ resolvedParams[key] = key in userParams ? userParams[key] : spec.default;
1135
+ }
1136
+ ctx = { ...baseCtx, params: resolvedParams };
1137
+ } else {
1138
+ ctx = { ...baseCtx, params: {} };
1139
+ }
1140
+ let result;
1141
+ try {
1142
+ result = await policy.fn(ctx);
1143
+ } catch (err) {
1144
+ hookLogWarn(`policy "${policy.name}" threw: ${err instanceof Error ? err.message : String(err)}`);
1145
+ continue;
1146
+ }
1147
+ if (result.decision === "deny") {
1148
+ const reason = result.reason ?? `Blocked by policy: ${policy.name}`;
1149
+ hookLogInfo(`deny by "${policy.name}": ${reason}`);
1150
+ const displayTool = ctx.toolName ?? "unknown tool";
1151
+ if (eventType === "PreToolUse") {
1152
+ const response = {
1153
+ hookSpecificOutput: {
1154
+ hookEventName: eventType,
1155
+ permissionDecision: "deny",
1156
+ permissionDecisionReason: `Blocked ${displayTool} by failproofai because: ${reason}, as per the policy configured by the user`
1157
+ }
1158
+ };
1159
+ return {
1160
+ exitCode: 0,
1161
+ stdout: JSON.stringify(response),
1162
+ stderr: "",
1163
+ policyName: policy.name,
1164
+ reason,
1165
+ decision: "deny"
1166
+ };
1167
+ }
1168
+ if (eventType === "PostToolUse") {
1169
+ const response = {
1170
+ hookSpecificOutput: {
1171
+ hookEventName: eventType,
1172
+ additionalContext: `Blocked ${displayTool} by failproofai because: ${reason}, as per the policy configured by the user`
1173
+ }
1174
+ };
1175
+ return {
1176
+ exitCode: 0,
1177
+ stdout: JSON.stringify(response),
1178
+ stderr: "",
1179
+ policyName: policy.name,
1180
+ reason,
1181
+ decision: "deny"
1182
+ };
1183
+ }
1184
+ return {
1185
+ exitCode: 2,
1186
+ stdout: "",
1187
+ stderr: "",
1188
+ policyName: policy.name,
1189
+ reason,
1190
+ decision: "deny"
1191
+ };
1192
+ }
1193
+ if (result.decision === "instruct" && !instructPolicyName) {
1194
+ instructPolicyName = policy.name;
1195
+ instructReason = result.reason ?? `Instruction from policy: ${policy.name}`;
1196
+ hookLogInfo(`instruct by "${policy.name}": ${instructReason}`);
1197
+ }
1198
+ }
1199
+ if (instructPolicyName && instructReason) {
1200
+ if (eventType === "Stop") {
1201
+ return {
1202
+ exitCode: 2,
1203
+ stdout: "",
1204
+ stderr: instructReason,
1205
+ policyName: instructPolicyName,
1206
+ reason: instructReason,
1207
+ decision: "instruct"
1208
+ };
1209
+ }
1210
+ const response = {
1211
+ hookSpecificOutput: {
1212
+ hookEventName: eventType,
1213
+ additionalContext: `Instruction from failproofai: ${instructReason}`
1214
+ }
1215
+ };
1216
+ return {
1217
+ exitCode: 0,
1218
+ stdout: JSON.stringify(response),
1219
+ stderr: "",
1220
+ policyName: instructPolicyName,
1221
+ reason: instructReason,
1222
+ decision: "instruct"
1223
+ };
1224
+ }
1225
+ return { exitCode: 0, stdout: "", stderr: "", policyName: null, reason: null, decision: "allow" };
1226
+ }
1227
+ var POLICY_PARAMS_MAP;
1228
+ var init_policy_evaluator = __esm(() => {
1229
+ init_builtin_policies();
1230
+ init_hook_logger();
1231
+ POLICY_PARAMS_MAP = new Map(BUILTIN_POLICIES.filter((p) => p.params).map((p) => [p.name, p.params]));
1232
+ });
1233
+
1234
+ // src/hooks/custom-hooks-registry.ts
1235
+ function getRegistry2() {
1236
+ const g = globalThis;
1237
+ if (!Array.isArray(g[REGISTRY_KEY2]))
1238
+ g[REGISTRY_KEY2] = [];
1239
+ return g[REGISTRY_KEY2];
1240
+ }
1241
+ function getCustomHooks() {
1242
+ return getRegistry2();
1243
+ }
1244
+ function clearCustomHooks() {
1245
+ const g = globalThis;
1246
+ g[REGISTRY_KEY2] = [];
1247
+ }
1248
+ var REGISTRY_KEY2 = "__failproofai_custom_hooks__";
1249
+ var init_custom_hooks_registry = () => {};
1250
+
1251
+ // src/hooks/loader-utils.ts
1252
+ import { readFile as readFile2, writeFile as writeFile2, unlink, access } from "fs/promises";
1253
+ import { resolve as resolve3, dirname as dirname2, relative } from "path";
1254
+ import { pathToFileURL } from "url";
1255
+ async function fileExists(path) {
1256
+ try {
1257
+ await access(path);
1258
+ return true;
1259
+ } catch {
1260
+ return false;
1261
+ }
1262
+ }
1263
+ async function findDistIndex() {
1264
+ const distPath = process.env.FAILPROOFAI_DIST_PATH;
1265
+ if (distPath) {
1266
+ const candidate = resolve3(distPath, "index.js");
1267
+ if (await fileExists(candidate))
1268
+ return candidate;
1269
+ }
1270
+ const candidates = [
1271
+ resolve3(dirname2(process.execPath), "..", "assets", "dist", "index.js"),
1272
+ resolve3(process.cwd(), "dist", "index.js"),
1273
+ resolve3(process.cwd(), "node_modules", "failproofai", "dist", "index.js")
1274
+ ];
1275
+ for (const c of candidates) {
1276
+ if (await fileExists(c))
1277
+ return c;
1278
+ }
1279
+ return null;
1280
+ }
1281
+ async function resolveLocalImport(fromDir, specifier) {
1282
+ const base = resolve3(fromDir, specifier);
1283
+ const candidates = [base, `${base}.js`, `${base}.mjs`, `${base}.ts`, resolve3(base, "index.js")];
1284
+ for (const c of candidates) {
1285
+ if (await fileExists(c))
1286
+ return c;
1287
+ }
1288
+ return null;
1289
+ }
1290
+ async function createEsmShim(distIndex, distUrl) {
1291
+ const shimPath = distIndex + ".__failproofai_esm_shim__.mjs";
1292
+ const shimCode = [
1293
+ `import _cjs from '${distUrl}';`,
1294
+ `export const createApp = _cjs.createApp;`,
1295
+ `export const getQueueCondition = _cjs.getQueueCondition;`,
1296
+ `export const clearQueueCondition = _cjs.clearQueueCondition;`,
1297
+ `export const customPolicies = _cjs.customPolicies;`,
1298
+ `export const allow = _cjs.allow;`,
1299
+ `export const deny = _cjs.deny;`,
1300
+ `export const instruct = _cjs.instruct;`,
1301
+ `export default _cjs;`
1302
+ ].join(`
1303
+ `);
1304
+ await writeFile2(shimPath, shimCode, "utf-8");
1305
+ return { shimPath, shimUrl: pathToFileURL(shimPath).href };
1306
+ }
1307
+ async function rewriteFileTree(entryPath, distUrl, distIndex) {
1308
+ const queue = [entryPath];
1309
+ const visited = new Set;
1310
+ const tmpFiles = [];
1311
+ let esmShimUrl = null;
1312
+ if (distIndex && distUrl) {
1313
+ const shim = await createEsmShim(distIndex, distUrl);
1314
+ tmpFiles.push(shim.shimPath);
1315
+ esmShimUrl = shim.shimUrl;
1316
+ }
1317
+ while (queue.length > 0) {
1318
+ const filePath = queue.shift();
1319
+ if (visited.has(filePath))
1320
+ continue;
1321
+ visited.add(filePath);
1322
+ let code = await readFile2(filePath, "utf-8");
1323
+ if (esmShimUrl) {
1324
+ code = code.replace(/from\s+(['"])(?:claudeye|failproofai)\1/g, `from '${esmShimUrl}'`);
1325
+ }
1326
+ if (distIndex) {
1327
+ code = code.replace(/require\s*\(\s*(['"])(?:claudeye|failproofai)\1\s*\)/g, `require('${distIndex.replace(/\\/g, "\\\\")}')`);
1328
+ }
1329
+ const dir = dirname2(filePath);
1330
+ const rewrites = new Map;
1331
+ for (const re of [LOCAL_IMPORT_RE, LOCAL_REQUIRE_RE]) {
1332
+ const freshRe = new RegExp(re.source, re.flags);
1333
+ let match;
1334
+ while ((match = freshRe.exec(code)) !== null) {
1335
+ const specifier = match[2];
1336
+ if (rewrites.has(specifier))
1337
+ continue;
1338
+ const resolved2 = await resolveLocalImport(dir, specifier);
1339
+ if (!resolved2)
1340
+ continue;
1341
+ if (!visited.has(resolved2) && !queue.includes(resolved2)) {
1342
+ queue.push(resolved2);
1343
+ }
1344
+ let relPath = relative(dir, resolved2 + TMP_SUFFIX).split("\\").join("/");
1345
+ if (!relPath.startsWith("."))
1346
+ relPath = "./" + relPath;
1347
+ rewrites.set(specifier, relPath);
1348
+ }
1349
+ }
1350
+ const sortedSpecs = [...rewrites.keys()].sort((a, b) => b.length - a.length);
1351
+ for (const specifier of sortedSpecs) {
1352
+ const replacement = rewrites.get(specifier);
1353
+ const escaped = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1354
+ code = code.replace(new RegExp(`'${escaped}'`, "g"), `'${replacement}'`);
1355
+ code = code.replace(new RegExp(`"${escaped}"`, "g"), `"${replacement}"`);
1356
+ }
1357
+ const tmpPath = filePath + TMP_SUFFIX;
1358
+ await writeFile2(tmpPath, code, "utf-8");
1359
+ tmpFiles.push(tmpPath);
1360
+ }
1361
+ return tmpFiles;
1362
+ }
1363
+ async function cleanupTmpFiles(tmpFiles) {
1364
+ for (const tmp of tmpFiles) {
1365
+ try {
1366
+ await unlink(tmp);
1367
+ } catch {}
1368
+ }
1369
+ }
1370
+ var TMP_SUFFIX = ".__failproofai_tmp__.mjs", LOCAL_IMPORT_RE, LOCAL_REQUIRE_RE;
1371
+ var init_loader_utils = __esm(() => {
1372
+ LOCAL_IMPORT_RE = /(?:import\s+(?:[\s\S]*?\s+from\s+)?|export\s+(?:[\s\S]*?\s+from\s+))(['"])(\.\.?\/[^'"]+)\1/g;
1373
+ LOCAL_REQUIRE_RE = /require\s*\(\s*(['"])(\.\.?\/[^'"]+)\1\s*\)/g;
1374
+ });
1375
+
1376
+ // src/hooks/custom-hooks-loader.ts
1377
+ import { resolve as resolve4, isAbsolute } from "node:path";
1378
+ import { existsSync as existsSync3 } from "node:fs";
1379
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
1380
+ async function loadCustomHooks(customPoliciesPath, opts) {
1381
+ if (!customPoliciesPath)
1382
+ return [];
1383
+ const absPath = isAbsolute(customPoliciesPath) ? customPoliciesPath : resolve4(process.cwd(), customPoliciesPath);
1384
+ if (!existsSync3(absPath)) {
1385
+ if (opts?.strict)
1386
+ throw new Error(`Custom hooks file not found: ${absPath}`);
1387
+ hookLogWarn(`customPoliciesPath not found: ${absPath}`);
1388
+ return [];
1389
+ }
1390
+ clearCustomHooks();
1391
+ const g = globalThis;
1392
+ g[LOADING_KEY] = true;
1393
+ let tmpFiles = [];
1394
+ try {
1395
+ const distIndex = await findDistIndex();
1396
+ const distUrl = distIndex ? pathToFileURL2(distIndex).href : null;
1397
+ tmpFiles = await rewriteFileTree(absPath, distUrl, distIndex);
1398
+ const entryTmp = absPath + TMP_SUFFIX;
1399
+ const fileUrl = pathToFileURL2(entryTmp).href;
1400
+ await import(fileUrl);
1401
+ } catch (err) {
1402
+ const msg = err instanceof Error ? err.message : String(err);
1403
+ if (opts?.strict)
1404
+ throw new Error(`Failed to load custom hooks from ${absPath}: ${msg}`);
1405
+ hookLogError(`failed to load custom hooks from ${absPath}: ${msg}`);
1406
+ return [];
1407
+ } finally {
1408
+ g[LOADING_KEY] = false;
1409
+ await cleanupTmpFiles(tmpFiles);
1410
+ }
1411
+ return getCustomHooks();
1412
+ }
1413
+ var LOADING_KEY = "__FAILPROOFAI_LOADING_HOOKS__";
1414
+ var init_custom_hooks_loader = __esm(() => {
1415
+ init_hook_logger();
1416
+ init_custom_hooks_registry();
1417
+ init_loader_utils();
1418
+ });
1419
+
1420
+ // src/hooks/hook-activity-store.ts
1421
+ import {
1422
+ readFileSync as readFileSync2,
1423
+ writeFileSync as writeFileSync2,
1424
+ appendFileSync as appendFileSync2,
1425
+ renameSync as renameSync2,
1426
+ readdirSync,
1427
+ mkdirSync as mkdirSync3,
1428
+ existsSync as existsSync4,
1429
+ statSync as statSync2,
1430
+ unlinkSync
1431
+ } from "node:fs";
1432
+ import { join as join3 } from "node:path";
1433
+ import { homedir as homedir4 } from "node:os";
1434
+ function ensureDir() {
1435
+ if (!existsSync4(storeDir)) {
1436
+ mkdirSync3(storeDir, { recursive: true });
1437
+ }
1438
+ }
1439
+ function acquireLock() {
1440
+ ensureDir();
1441
+ const lockPath = join3(storeDir, LOCK_FILE);
1442
+ const deadline = Date.now() + LOCK_STALE_MS;
1443
+ while (Date.now() < deadline) {
1444
+ try {
1445
+ writeFileSync2(lockPath, String(process.pid), { flag: "wx" });
1446
+ return;
1447
+ } catch (e) {
1448
+ if (e.code !== "EEXIST")
1449
+ return;
1450
+ try {
1451
+ const s = statSync2(lockPath);
1452
+ if (Date.now() - s.mtimeMs > LOCK_STALE_MS) {
1453
+ writeFileSync2(lockPath, String(process.pid), "utf-8");
1454
+ return;
1455
+ }
1456
+ } catch {}
1457
+ }
1458
+ }
1459
+ }
1460
+ function releaseLock() {
1461
+ try {
1462
+ unlinkSync(join3(storeDir, LOCK_FILE));
1463
+ } catch {}
1464
+ }
1465
+ function persistHookActivity(entry) {
1466
+ ensureDir();
1467
+ acquireLock();
1468
+ try {
1469
+ const currentPath = join3(storeDir, CURRENT_FILE);
1470
+ const countPath = join3(storeDir, COUNT_FILE);
1471
+ const lineCount = readCount(countPath);
1472
+ if (lineCount >= PAGE_SIZE) {
1473
+ try {
1474
+ rotate(currentPath, countPath);
1475
+ } catch (e) {
1476
+ if (e.code !== "ENOENT")
1477
+ throw e;
1478
+ }
1479
+ }
1480
+ appendFileSync2(currentPath, JSON.stringify(entry) + `
1481
+ `, "utf-8");
1482
+ writeCount(countPath, lineCount >= PAGE_SIZE ? 1 : lineCount + 1);
1483
+ updateStats(entry);
1484
+ } finally {
1485
+ releaseLock();
1486
+ }
1487
+ }
1488
+ function rotate(currentPath, countPath) {
1489
+ const archiveName = `page-${Date.now()}-${rotateSeq++}.jsonl`;
1490
+ const archivePath = join3(storeDir, archiveName);
1491
+ renameSync2(currentPath, archivePath);
1492
+ writeCount(countPath, 0);
1493
+ }
1494
+ function readCount(countPath) {
1495
+ try {
1496
+ const n = parseInt(readFileSync2(countPath, "utf-8"), 10);
1497
+ return isNaN(n) ? 0 : n;
1498
+ } catch {
1499
+ return 0;
1500
+ }
1501
+ }
1502
+ function writeCount(countPath, n) {
1503
+ try {
1504
+ writeFileSync2(countPath, String(n), "utf-8");
1505
+ } catch {}
1506
+ }
1507
+ function readStoredStats() {
1508
+ try {
1509
+ return JSON.parse(readFileSync2(join3(storeDir, STATS_FILE), "utf-8"));
1510
+ } catch {
1511
+ return { totalEvents: 0, denyCount: 0, policyMap: {} };
1512
+ }
1513
+ }
1514
+ function updateStats(entry) {
1515
+ const s = readStoredStats();
1516
+ s.totalEvents += 1;
1517
+ if (entry.decision === "deny")
1518
+ s.denyCount += 1;
1519
+ if (entry.policyName) {
1520
+ s.policyMap[entry.policyName] = (s.policyMap[entry.policyName] ?? 0) + 1;
1521
+ }
1522
+ const tmpPath = join3(storeDir, `stats.json.${process.pid}.tmp`);
1523
+ try {
1524
+ writeFileSync2(tmpPath, JSON.stringify(s), "utf-8");
1525
+ renameSync2(tmpPath, join3(storeDir, STATS_FILE));
1526
+ } catch {
1527
+ try {
1528
+ unlinkSync(tmpPath);
1529
+ } catch {}
1530
+ }
1531
+ }
1532
+ var PAGE_SIZE = 25, DEFAULT_STORE_DIR, CURRENT_FILE = "current.jsonl", COUNT_FILE = "current.count", STATS_FILE = "stats.json", LOCK_FILE = "current.lock", LOCK_STALE_MS = 2000, storeDir, rotateSeq = 0;
1533
+ var init_hook_activity_store = __esm(() => {
1534
+ DEFAULT_STORE_DIR = join3(homedir4(), ".failproofai", "cache", "hook-activity");
1535
+ storeDir = DEFAULT_STORE_DIR;
1536
+ });
1537
+
1538
+ // package.json
1539
+ var version2 = "0.0.2-beta.1";
1540
+ var init_package = () => {};
1541
+
1542
+ // src/posthog-key.ts
1543
+ var POSTHOG_API_KEY = "phc_Ac1Ww1GqKc0z1SyrRWbmatEeQdlOQIsDEEdP8l8JRgX";
1544
+
1545
+ // src/hooks/hook-telemetry.ts
1546
+ async function trackHookEvent(distinctId, event, properties) {
1547
+ if (process.env.FAILPROOFAI_TELEMETRY_DISABLED === "1")
1548
+ return;
1549
+ const body = JSON.stringify({
1550
+ api_key: process.env.FAILPROOFAI_POSTHOG_KEY ?? API_KEY,
1551
+ event,
1552
+ distinct_id: distinctId,
1553
+ properties: { ...properties, $lib: "failproofai-hooks", failproofai_version: version2 }
1554
+ });
1555
+ try {
1556
+ await fetch(process.env.FAILPROOFAI_POSTHOG_HOST ? `${process.env.FAILPROOFAI_POSTHOG_HOST}/capture/` : CAPTURE_URL, {
1557
+ method: "POST",
1558
+ headers: { "Content-Type": "application/json" },
1559
+ body,
1560
+ signal: AbortSignal.timeout(5000)
1561
+ });
1562
+ } catch {}
1563
+ }
1564
+ var API_KEY, CAPTURE_URL = "https://us.i.posthog.com/capture/";
1565
+ var init_hook_telemetry = __esm(() => {
1566
+ init_package();
1567
+ API_KEY = POSTHOG_API_KEY;
1568
+ });
1569
+
1570
+ // lib/telemetry-id.ts
1571
+ import fs from "node:fs";
1572
+ import path from "node:path";
1573
+ import os from "node:os";
1574
+ import crypto from "node:crypto";
1575
+ import { execSync as execSync2 } from "node:child_process";
1576
+ function hashToId(raw) {
1577
+ return crypto.createHmac("sha256", NAMESPACE).update(raw).digest("hex");
1578
+ }
1579
+ function getPlatformMachineId() {
1580
+ try {
1581
+ const platform = os.platform();
1582
+ if (platform === "linux") {
1583
+ for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
1584
+ try {
1585
+ const id = fs.readFileSync(p, "utf-8").trim();
1586
+ if (id)
1587
+ return id;
1588
+ } catch {}
1589
+ }
1590
+ } else if (platform === "darwin") {
1591
+ const out = execSync2("ioreg -rd1 -c IOPlatformExpertDevice", {
1592
+ encoding: "utf-8",
1593
+ timeout: 3000
1594
+ });
1595
+ const m = out.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
1596
+ if (m?.[1])
1597
+ return m[1];
1598
+ } else if (platform === "win32") {
1599
+ const out = execSync2('reg query "HKLM\\SOFTWARE\\Microsoft\\Cryptography" /v MachineGuid', { encoding: "utf-8", timeout: 3000 });
1600
+ const m = out.match(/MachineGuid\s+REG_SZ\s+(\S+)/);
1601
+ if (m?.[1])
1602
+ return m[1];
1603
+ }
1604
+ } catch {}
1605
+ return;
1606
+ }
1607
+ function getSystemPropertiesId() {
1608
+ return [
1609
+ os.hostname(),
1610
+ os.homedir(),
1611
+ os.platform(),
1612
+ os.arch(),
1613
+ os.cpus()[0]?.model ?? ""
1614
+ ].join(":");
1615
+ }
1616
+ function getFileBasedId() {
1617
+ try {
1618
+ const existing = fs.readFileSync(ID_FILE, "utf-8").trim();
1619
+ if (existing)
1620
+ return existing;
1621
+ } catch {}
1622
+ const id = crypto.randomUUID();
1623
+ try {
1624
+ fs.mkdirSync(ID_DIR, { recursive: true });
1625
+ fs.writeFileSync(ID_FILE, id, "utf-8");
1626
+ } catch {}
1627
+ return id;
1628
+ }
1629
+ function getInstanceId() {
1630
+ if (cachedId)
1631
+ return cachedId;
1632
+ const machineId = getPlatformMachineId();
1633
+ if (machineId) {
1634
+ cachedId = hashToId(machineId);
1635
+ return cachedId;
1636
+ }
1637
+ const sysProps = getSystemPropertiesId();
1638
+ if (sysProps) {
1639
+ cachedId = hashToId(sysProps);
1640
+ return cachedId;
1641
+ }
1642
+ cachedId = getFileBasedId();
1643
+ return cachedId;
1644
+ }
1645
+ var NAMESPACE = "failproofai-telemetry-v1", ID_DIR, ID_FILE, cachedId;
1646
+ var init_telemetry_id = __esm(() => {
1647
+ ID_DIR = path.join(os.homedir(), ".failproofai");
1648
+ ID_FILE = path.join(ID_DIR, "instance-id");
1649
+ });
1650
+
1651
+ // src/hooks/handler.ts
1652
+ var exports_handler = {};
1653
+ __export(exports_handler, {
1654
+ handleHookEvent: () => handleHookEvent
1655
+ });
1656
+ async function handleHookEvent(eventType) {
1657
+ const startTime = performance.now();
1658
+ const MAX_STDIN_BYTES = 1048576;
1659
+ let payload = "";
1660
+ try {
1661
+ payload = await new Promise((resolve5, reject) => {
1662
+ const chunks = [];
1663
+ let totalBytes = 0;
1664
+ process.stdin.setEncoding("utf8");
1665
+ process.stdin.on("data", (chunk) => {
1666
+ totalBytes += Buffer.byteLength(chunk);
1667
+ if (totalBytes > MAX_STDIN_BYTES) {
1668
+ hookLogWarn(`stdin payload exceeds 1 MB for ${eventType}, discarding`);
1669
+ process.stdin.destroy();
1670
+ resolve5("");
1671
+ return;
1672
+ }
1673
+ chunks.push(chunk);
1674
+ });
1675
+ process.stdin.on("end", () => resolve5(chunks.join("")));
1676
+ process.stdin.on("error", reject);
1677
+ if (process.stdin.readableEnded)
1678
+ resolve5("");
1679
+ });
1680
+ } catch {
1681
+ hookLogWarn(`stdin read failed for ${eventType}`);
1682
+ }
1683
+ let parsed = {};
1684
+ if (payload) {
1685
+ try {
1686
+ parsed = JSON.parse(payload);
1687
+ } catch {
1688
+ hookLogWarn(`payload parse failed for ${eventType} (${payload.length} bytes)`);
1689
+ }
1690
+ }
1691
+ const session = {
1692
+ sessionId: parsed.session_id,
1693
+ transcriptPath: parsed.transcript_path,
1694
+ cwd: parsed.cwd,
1695
+ permissionMode: parsed.permission_mode,
1696
+ hookEventName: parsed.hook_event_name
1697
+ };
1698
+ const config = readMergedHooksConfig(session.cwd);
1699
+ clearPolicies();
1700
+ registerBuiltinPolicies(config.enabledPolicies);
1701
+ const customHooksList = await loadCustomHooks(config.customPoliciesPath);
1702
+ for (const hook of customHooksList) {
1703
+ const hookName = hook.name;
1704
+ const fn = async (ctx) => {
1705
+ try {
1706
+ const result2 = await Promise.race([
1707
+ hook.fn(ctx),
1708
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 1e4))
1709
+ ]);
1710
+ return result2;
1711
+ } catch (err) {
1712
+ const msg = err instanceof Error ? err.message : String(err);
1713
+ const isTimeout = msg === "timeout";
1714
+ hookLogWarn(`custom hook "${hookName}" failed: ${msg}`);
1715
+ trackHookEvent(getInstanceId(), "custom_hook_error", {
1716
+ hook_name: hookName,
1717
+ error_type: isTimeout ? "timeout" : "exception",
1718
+ event_type: eventType
1719
+ });
1720
+ return { decision: "allow" };
1721
+ }
1722
+ };
1723
+ registerPolicy(`custom/${hookName}`, hook.description ?? "", fn, hook.match ?? {}, -1);
1724
+ }
1725
+ if (customHooksList.length > 0) {
1726
+ trackHookEvent(getInstanceId(), "custom_hooks_loaded", {
1727
+ custom_hooks_count: customHooksList.length,
1728
+ custom_hook_names: customHooksList.map((h) => h.name),
1729
+ event_types_covered: [...new Set(customHooksList.flatMap((h) => h.match?.events ?? []))]
1730
+ });
1731
+ }
1732
+ hookLogInfo(`event=${eventType} policies=${config.enabledPolicies.length} custom=${customHooksList.length}`);
1733
+ const result = await evaluatePolicies(eventType, parsed, session, config);
1734
+ const durationMs = Math.round(performance.now() - startTime);
1735
+ hookLogInfo(`result=${result.decision} policy=${result.policyName ?? "none"} duration=${durationMs}ms`);
1736
+ if (result.stdout) {
1737
+ process.stdout.write(result.stdout);
1738
+ }
1739
+ if (result.stderr) {
1740
+ process.stderr.write(result.stderr);
1741
+ }
1742
+ try {
1743
+ persistHookActivity({
1744
+ timestamp: Date.now(),
1745
+ eventType,
1746
+ toolName: parsed.tool_name ?? null,
1747
+ policyName: result.policyName,
1748
+ decision: result.decision,
1749
+ reason: result.reason,
1750
+ durationMs,
1751
+ sessionId: session.sessionId,
1752
+ transcriptPath: session.transcriptPath,
1753
+ cwd: session.cwd,
1754
+ permissionMode: session.permissionMode,
1755
+ hookEventName: session.hookEventName
1756
+ });
1757
+ } catch {
1758
+ hookLogWarn("activity persistence failed");
1759
+ }
1760
+ if (result.decision === "deny" || result.decision === "instruct") {
1761
+ try {
1762
+ const isCustomHook = result.policyName?.startsWith("custom/") ?? false;
1763
+ const hasCustomParams = !isCustomHook && !!(result.policyName && config.policyParams?.[result.policyName]);
1764
+ const paramKeysOverridden = hasCustomParams ? Object.keys(config.policyParams[result.policyName]) : [];
1765
+ const distinctId = getInstanceId();
1766
+ await trackHookEvent(distinctId, "hook_policy_triggered", {
1767
+ event_type: eventType,
1768
+ tool_name: parsed.tool_name ?? null,
1769
+ policy_name: result.policyName,
1770
+ decision: result.decision,
1771
+ is_custom_hook: isCustomHook,
1772
+ has_custom_params: hasCustomParams,
1773
+ param_keys_overridden: paramKeysOverridden
1774
+ });
1775
+ } catch {}
1776
+ }
1777
+ return result.exitCode;
1778
+ }
1779
+ var init_handler = __esm(() => {
1780
+ init_hooks_config();
1781
+ init_builtin_policies();
1782
+ init_policy_evaluator();
1783
+ init_custom_hooks_loader();
1784
+ init_hook_activity_store();
1785
+ init_hook_telemetry();
1786
+ init_telemetry_id();
1787
+ init_hook_logger();
1788
+ });
1789
+
1790
+ // src/hooks/types.ts
1791
+ var HOOK_SCOPES, HOOK_EVENT_TYPES, FAILPROOFAI_HOOK_MARKER = "__failproofai_hook__";
1792
+ var init_types = __esm(() => {
1793
+ HOOK_SCOPES = ["user", "project", "local"];
1794
+ HOOK_EVENT_TYPES = [
1795
+ "SessionStart",
1796
+ "SessionEnd",
1797
+ "UserPromptSubmit",
1798
+ "PreToolUse",
1799
+ "PermissionRequest",
1800
+ "PermissionDenied",
1801
+ "PostToolUse",
1802
+ "PostToolUseFailure",
1803
+ "Notification",
1804
+ "SubagentStart",
1805
+ "SubagentStop",
1806
+ "TaskCreated",
1807
+ "TaskCompleted",
1808
+ "Stop",
1809
+ "StopFailure",
1810
+ "TeammateIdle",
1811
+ "InstructionsLoaded",
1812
+ "ConfigChange",
1813
+ "CwdChanged",
1814
+ "FileChanged",
1815
+ "WorktreeCreate",
1816
+ "WorktreeRemove",
1817
+ "PreCompact",
1818
+ "PostCompact",
1819
+ "Elicitation",
1820
+ "ElicitationResult"
1821
+ ];
1822
+ });
1823
+
1824
+ // src/hooks/install-prompt.ts
1825
+ import * as readline from "node:readline";
1826
+ async function promptPolicySelection(preSelected, options = {}) {
1827
+ const { includeBeta = false } = options;
1828
+ if (!process.stdin.isTTY) {
1829
+ const available = BUILTIN_POLICIES.filter((p) => includeBeta || !p.beta);
1830
+ if (preSelected)
1831
+ return preSelected.filter((name) => available.some((p) => p.name === name));
1832
+ return available.filter((p) => p.defaultEnabled).map((p) => p.name);
1833
+ }
1834
+ const preSelectedSet = preSelected ? new Set(preSelected) : null;
1835
+ const items = BUILTIN_POLICIES.filter((p) => includeBeta || !p.beta).map((p) => ({
1836
+ name: p.name,
1837
+ description: p.description,
1838
+ category: p.category,
1839
+ selected: preSelectedSet ? preSelectedSet.has(p.name) : p.defaultEnabled,
1840
+ beta: !!p.beta
1841
+ }));
1842
+ const total = items.length;
1843
+ const WINDOW_SIZE = 8;
1844
+ let cursor = 0;
1845
+ let search = "";
1846
+ let lastLineCount = 0;
1847
+ let cursorHidden = false;
1848
+ function hideCursor() {
1849
+ if (!cursorHidden) {
1850
+ process.stdout.write("\x1B[?25l");
1851
+ cursorHidden = true;
1852
+ }
1853
+ }
1854
+ function showCursor() {
1855
+ if (cursorHidden) {
1856
+ process.stdout.write("\x1B[?25h");
1857
+ cursorHidden = false;
1858
+ }
1859
+ }
1860
+ function truncateLine(line, width) {
1861
+ let visual = 0;
1862
+ let result = "";
1863
+ let i = 0;
1864
+ while (i < line.length) {
1865
+ if (line[i] === "\x1B" && line[i + 1] === "[") {
1866
+ let j = i + 2;
1867
+ while (j < line.length && !/[A-Za-z]/.test(line[j]))
1868
+ j++;
1869
+ j++;
1870
+ result += line.slice(i, j);
1871
+ i = j;
1872
+ } else {
1873
+ if (visual >= width)
1874
+ break;
1875
+ result += line[i];
1876
+ visual++;
1877
+ i++;
1878
+ }
1879
+ }
1880
+ return result;
1881
+ }
1882
+ function getFiltered() {
1883
+ if (!search)
1884
+ return items;
1885
+ const q = search.toLowerCase();
1886
+ return items.filter((i) => i.name.toLowerCase().includes(q) || i.description.toLowerCase().includes(q));
1887
+ }
1888
+ function buildDisplayRows(filtered) {
1889
+ const categoryOrder = [];
1890
+ const categoryEnabledCount = new Map;
1891
+ const categoryTotalCount = new Map;
1892
+ for (const p of items) {
1893
+ if (!categoryEnabledCount.has(p.category)) {
1894
+ categoryOrder.push(p.category);
1895
+ categoryEnabledCount.set(p.category, 0);
1896
+ categoryTotalCount.set(p.category, 0);
1897
+ }
1898
+ categoryTotalCount.set(p.category, categoryTotalCount.get(p.category) + 1);
1899
+ if (p.selected)
1900
+ categoryEnabledCount.set(p.category, categoryEnabledCount.get(p.category) + 1);
1901
+ }
1902
+ const filteredByCategory = new Map;
1903
+ for (const item of filtered) {
1904
+ const bucket = filteredByCategory.get(item.category) ?? [];
1905
+ bucket.push(item);
1906
+ filteredByCategory.set(item.category, bucket);
1907
+ }
1908
+ const rows = [];
1909
+ let idx = 0;
1910
+ for (const cat of categoryOrder) {
1911
+ const catFiltered = filteredByCategory.get(cat);
1912
+ if (!catFiltered || catFiltered.length === 0)
1913
+ continue;
1914
+ rows.push({ kind: "header", category: cat, enabledCount: categoryEnabledCount.get(cat), totalCount: categoryTotalCount.get(cat) });
1915
+ for (const item of catFiltered) {
1916
+ rows.push({ kind: "item", item, filteredIndex: idx++ });
1917
+ }
1918
+ }
1919
+ return rows;
1920
+ }
1921
+ function render() {
1922
+ const cols = process.stdout.columns || 120;
1923
+ hideCursor();
1924
+ const filtered = getFiltered();
1925
+ const shown = filtered.length;
1926
+ if (shown > 0 && cursor >= shown)
1927
+ cursor = shown - 1;
1928
+ const lines = [];
1929
+ lines.push(" Failproof AI — Policy Manager");
1930
+ lines.push("");
1931
+ const innerWidth = Math.max(20, cols - 6);
1932
+ const topBorder = " ┌" + "─".repeat(innerWidth + 2) + "┐";
1933
+ const botBorder = " └" + "─".repeat(innerWidth + 2) + "┘";
1934
+ const cursorChar = "\x1B[7m \x1B[0m";
1935
+ const countPart = search ? ` \x1B[2m(${shown}/${total})\x1B[0m` : ` \x1B[2m(${total} policies)\x1B[0m`;
1936
+ const searchContent = `\x1B[1mSearch:\x1B[0m ${search}${cursorChar}${countPart}`;
1937
+ lines.push(topBorder);
1938
+ lines.push(` │ ${searchContent}`);
1939
+ lines.push(botBorder);
1940
+ lines.push("");
1941
+ if (shown === 0) {
1942
+ lines.push(" \x1B[2mNo policies match “" + search + "”\x1B[0m");
1943
+ for (let i = 0;i < WINDOW_SIZE + 1; i++)
1944
+ lines.push("");
1945
+ } else {
1946
+ const displayRows = buildDisplayRows(filtered);
1947
+ let cursorDisplayRow = 0;
1948
+ for (let i = 0;i < displayRows.length; i++) {
1949
+ const row = displayRows[i];
1950
+ if (row.kind === "item" && row.filteredIndex === cursor) {
1951
+ cursorDisplayRow = i;
1952
+ break;
1953
+ }
1954
+ }
1955
+ let windowStart = cursorDisplayRow - Math.floor(WINDOW_SIZE / 2);
1956
+ windowStart = Math.max(0, windowStart);
1957
+ windowStart = Math.min(windowStart, Math.max(0, displayRows.length - WINDOW_SIZE));
1958
+ const windowEnd = Math.min(displayRows.length, windowStart + WINDOW_SIZE);
1959
+ const aboveItems = displayRows.slice(0, windowStart).filter((r) => r.kind === "item").length;
1960
+ if (aboveItems > 0) {
1961
+ lines.push(` \x1B[2m ↑ ${aboveItems} more above\x1B[0m`);
1962
+ } else {
1963
+ lines.push("");
1964
+ }
1965
+ for (let i = windowStart;i < windowEnd; i++) {
1966
+ const row = displayRows[i];
1967
+ if (row.kind === "header") {
1968
+ const label = ` ${row.category.toUpperCase()} (${row.enabledCount}/${row.totalCount}) `;
1969
+ const prefix = "── ";
1970
+ const prefixLen = 3 + label.length;
1971
+ const dashLen = Math.max(2, cols - 2 - prefixLen);
1972
+ lines.push(` \x1B[2m${prefix}${label}${"─".repeat(dashLen)}\x1B[0m`);
1973
+ } else {
1974
+ const item = row.item;
1975
+ const isActive = row.filteredIndex === cursor;
1976
+ const pointer = isActive ? "\x1B[36m❯\x1B[0m" : " ";
1977
+ const check = item.selected ? "\x1B[32m[✓]\x1B[0m" : "[ ]";
1978
+ const namePart = isActive ? `\x1B[1;36m${item.name}\x1B[0m` : item.name;
1979
+ const betaPart = item.beta ? " \x1B[35m[beta]\x1B[0m" : "";
1980
+ const pad = " ".repeat(Math.max(1, 28 - item.name.length));
1981
+ const desc = `\x1B[2m${item.description}\x1B[0m`;
1982
+ lines.push(` ${pointer} ${check} ${namePart}${betaPart}${pad}${desc}`);
1983
+ }
1984
+ }
1985
+ for (let i = windowEnd - windowStart;i < WINDOW_SIZE; i++) {
1986
+ lines.push("");
1987
+ }
1988
+ const belowItems = displayRows.slice(windowEnd).filter((r) => r.kind === "item").length;
1989
+ if (belowItems > 0) {
1990
+ lines.push(` \x1B[2m ↓ ${belowItems} more below\x1B[0m`);
1991
+ } else {
1992
+ lines.push("");
1993
+ }
1994
+ }
1995
+ lines.push("");
1996
+ lines.push(" \x1B[2m" + "─".repeat(cols - 2) + "\x1B[0m");
1997
+ lines.push(" [↑↓] Move [Space] Toggle [Ctrl+A] All [Ctrl+S] Save [Esc] Clear [^C] Quit");
1998
+ lines.push("");
1999
+ lines.push(" \x1B[2mTip: `policies` for a flat list · `policies --install <name…>` to skip prompt\x1B[0m");
2000
+ if (!includeBeta) {
2001
+ lines.push(" \x1B[2mTip: `policies --install --beta` to include beta policies\x1B[0m");
2002
+ }
2003
+ if (lastLineCount > 0) {
2004
+ process.stdout.write(`\x1B[${lastLineCount}A\x1B[J`);
2005
+ }
2006
+ const output = lines.map((l) => l === "" ? l : truncateLine(l, cols)).join(`
2007
+ `) + `
2008
+ `;
2009
+ process.stdout.write(output);
2010
+ lastLineCount = lines.length;
2011
+ }
2012
+ return new Promise((resolve5) => {
2013
+ render();
2014
+ process.stdin.setRawMode(true);
2015
+ process.stdin.resume();
2016
+ readline.emitKeypressEvents(process.stdin);
2017
+ function keypressHandler(_str, key) {
2018
+ if (!key)
2019
+ return;
2020
+ if (key.ctrl && key.name === "c") {
2021
+ cleanup();
2022
+ process.exit(0);
2023
+ }
2024
+ const filtered = getFiltered();
2025
+ if (key.name === "up") {
2026
+ if (filtered.length > 0) {
2027
+ cursor = cursor > 0 ? cursor - 1 : filtered.length - 1;
2028
+ }
2029
+ render();
2030
+ } else if (key.name === "down") {
2031
+ if (filtered.length > 0) {
2032
+ cursor = cursor < filtered.length - 1 ? cursor + 1 : 0;
2033
+ }
2034
+ render();
2035
+ } else if (key.name === "return" || key.name === "space") {
2036
+ const item = filtered[cursor];
2037
+ if (item)
2038
+ item.selected = !item.selected;
2039
+ render();
2040
+ } else if (key.name === "escape") {
2041
+ search = "";
2042
+ cursor = 0;
2043
+ render();
2044
+ } else if (key.ctrl && key.name === "a") {
2045
+ const allSelected = filtered.length > 0 && filtered.every((i) => i.selected);
2046
+ for (const item of filtered)
2047
+ item.selected = !allSelected;
2048
+ render();
2049
+ } else if (key.ctrl && key.name === "s") {
2050
+ cleanup();
2051
+ const selected = items.filter((i) => i.selected).map((i) => i.name);
2052
+ process.stdout.write(`
2053
+ `);
2054
+ resolve5(selected);
2055
+ } else if (key.name === "backspace" || key.name === "delete") {
2056
+ if (search.length > 0) {
2057
+ search = search.slice(0, -1);
2058
+ cursor = 0;
2059
+ render();
2060
+ }
2061
+ } else if (_str && _str.length === 1 && !key.ctrl && !key.meta) {
2062
+ search += _str;
2063
+ cursor = 0;
2064
+ render();
2065
+ }
2066
+ }
2067
+ function cleanup() {
2068
+ showCursor();
2069
+ process.stdin.removeListener("keypress", keypressHandler);
2070
+ process.stdin.setRawMode(false);
2071
+ process.stdin.pause();
2072
+ }
2073
+ process.stdin.on("keypress", keypressHandler);
2074
+ });
2075
+ }
2076
+ var init_install_prompt = __esm(() => {
2077
+ init_builtin_policies();
2078
+ });
2079
+
2080
+ // src/hooks/manager.ts
2081
+ var exports_manager = {};
2082
+ __export(exports_manager, {
2083
+ removeHooks: () => removeHooks,
2084
+ listHooks: () => listHooks,
2085
+ installHooks: () => installHooks,
2086
+ hooksInstalledInSettings: () => hooksInstalledInSettings,
2087
+ getSettingsPath: () => getSettingsPath
2088
+ });
2089
+ import { execSync as execSync3 } from "node:child_process";
2090
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync4 } from "node:fs";
2091
+ import { resolve as resolve5, dirname as dirname3 } from "node:path";
2092
+ import { homedir as homedir5, platform, arch, release, hostname } from "node:os";
2093
+ function getSettingsPath(scope, cwd) {
2094
+ const base = cwd ? resolve5(cwd) : process.cwd();
2095
+ switch (scope) {
2096
+ case "user":
2097
+ return resolve5(homedir5(), ".claude", "settings.json");
2098
+ case "project":
2099
+ return resolve5(base, ".claude", "settings.json");
2100
+ case "local":
2101
+ return resolve5(base, ".claude", "settings.local.json");
2102
+ }
2103
+ }
2104
+ function scopeLabel(scope) {
2105
+ switch (scope) {
2106
+ case "user":
2107
+ return `~/.claude/settings.json`;
2108
+ case "project":
2109
+ return `{cwd}/.claude/settings.json`;
2110
+ case "local":
2111
+ return `{cwd}/.claude/settings.local.json`;
2112
+ }
2113
+ }
2114
+ function readSettings(settingsPath) {
2115
+ if (!existsSync5(settingsPath)) {
2116
+ return {};
2117
+ }
2118
+ const raw = readFileSync3(settingsPath, "utf8");
2119
+ return JSON.parse(raw);
2120
+ }
2121
+ function writeSettings(settingsPath, settings) {
2122
+ mkdirSync4(dirname3(settingsPath), { recursive: true });
2123
+ writeFileSync3(settingsPath, JSON.stringify(settings, null, 2) + `
2124
+ `, "utf8");
2125
+ }
2126
+ function resolveFailproofaiBinary() {
2127
+ try {
2128
+ const cmd = process.platform === "win32" ? "where failproofai" : "which failproofai";
2129
+ const result = execSync3(cmd, { encoding: "utf8" }).trim();
2130
+ return result.split(`
2131
+ `)[0].trim();
2132
+ } catch {
2133
+ throw new Error(`failproofai binary not found in PATH.
2134
+ ` + "Install it globally first: npm install -g failproofai");
2135
+ }
2136
+ }
2137
+ function isFailproofaiHook(hook) {
2138
+ if (hook[FAILPROOFAI_HOOK_MARKER] === true)
2139
+ return true;
2140
+ const cmd = typeof hook.command === "string" ? hook.command : "";
2141
+ return cmd.includes("failproofai") && cmd.includes("--hook");
2142
+ }
2143
+ function validatePolicyNames(names) {
2144
+ const invalid = names.filter((n) => !VALID_POLICY_NAMES.has(n));
2145
+ if (invalid.length > 0) {
2146
+ const validList = [...VALID_POLICY_NAMES].join(", ");
2147
+ throw new Error(`Unknown policy name(s): ${invalid.join(", ")}
2148
+ ` + `Valid policies: ${validList}`);
2149
+ }
2150
+ }
2151
+ function deduplicateScopes(scopes, cwd) {
2152
+ const seen = new Set;
2153
+ return scopes.filter((s) => {
2154
+ const p = getSettingsPath(s, cwd);
2155
+ if (seen.has(p))
2156
+ return false;
2157
+ seen.add(p);
2158
+ return true;
2159
+ });
2160
+ }
2161
+ function hooksInstalledInSettings(scope, cwd) {
2162
+ const settingsPath = getSettingsPath(scope, cwd);
2163
+ if (!existsSync5(settingsPath))
2164
+ return false;
2165
+ try {
2166
+ const settings = readSettings(settingsPath);
2167
+ if (!settings.hooks)
2168
+ return false;
2169
+ for (const matchers of Object.values(settings.hooks)) {
2170
+ if (!Array.isArray(matchers))
2171
+ continue;
2172
+ for (const matcher of matchers) {
2173
+ if (!matcher.hooks)
2174
+ continue;
2175
+ if (matcher.hooks.some((h) => isFailproofaiHook(h))) {
2176
+ return true;
2177
+ }
2178
+ }
2179
+ }
2180
+ } catch {}
2181
+ return false;
2182
+ }
2183
+ function removeHooksFromSettingsFile(settingsPath) {
2184
+ const settings = readSettings(settingsPath);
2185
+ if (!settings.hooks)
2186
+ return 0;
2187
+ let removed = 0;
2188
+ for (const eventType of Object.keys(settings.hooks)) {
2189
+ const matchers = settings.hooks[eventType];
2190
+ if (!Array.isArray(matchers))
2191
+ continue;
2192
+ for (let i = matchers.length - 1;i >= 0; i--) {
2193
+ const matcher = matchers[i];
2194
+ if (!matcher.hooks)
2195
+ continue;
2196
+ const before = matcher.hooks.length;
2197
+ matcher.hooks = matcher.hooks.filter((h) => !isFailproofaiHook(h));
2198
+ removed += before - matcher.hooks.length;
2199
+ if (matcher.hooks.length === 0) {
2200
+ matchers.splice(i, 1);
2201
+ }
2202
+ }
2203
+ if (matchers.length === 0) {
2204
+ delete settings.hooks[eventType];
2205
+ }
2206
+ }
2207
+ if (Object.keys(settings.hooks).length === 0) {
2208
+ delete settings.hooks;
2209
+ }
2210
+ writeSettings(settingsPath, settings);
2211
+ return removed;
2212
+ }
2213
+ async function installHooks(policyNames, scope = "user", cwd, includeBeta = false, source, customPoliciesPath, removeCustomHooks = false) {
2214
+ const binaryPath = resolveFailproofaiBinary();
2215
+ const previousConfig = readHooksConfig();
2216
+ const previousEnabled = new Set(previousConfig.enabledPolicies);
2217
+ let selectedPolicies;
2218
+ if (policyNames !== undefined) {
2219
+ let incoming;
2220
+ if (policyNames.length === 1 && policyNames[0] === "all") {
2221
+ incoming = BUILTIN_POLICIES.filter((p) => includeBeta || !p.beta).map((p) => p.name);
2222
+ } else {
2223
+ if (policyNames.length > 0)
2224
+ validatePolicyNames(policyNames);
2225
+ incoming = policyNames;
2226
+ }
2227
+ selectedPolicies = [...new Set([...previousConfig.enabledPolicies, ...incoming])];
2228
+ } else {
2229
+ const preSelected = previousConfig.enabledPolicies.length > 0 ? previousConfig.enabledPolicies : undefined;
2230
+ selectedPolicies = await promptPolicySelection(preSelected, { includeBeta });
2231
+ }
2232
+ const configToWrite = { ...previousConfig, enabledPolicies: selectedPolicies };
2233
+ if (removeCustomHooks) {
2234
+ delete configToWrite.customPoliciesPath;
2235
+ } else if (customPoliciesPath) {
2236
+ configToWrite.customPoliciesPath = resolve5(customPoliciesPath);
2237
+ let validatedHooks = [];
2238
+ try {
2239
+ validatedHooks = await loadCustomHooks(configToWrite.customPoliciesPath, { strict: true });
2240
+ } catch (err) {
2241
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
2242
+ process.exit(1);
2243
+ }
2244
+ if (validatedHooks.length === 0) {
2245
+ console.error(`Error: no hooks registered in ${customPoliciesPath}. ` + `Make sure your file calls customPolicies.add(...) at least once.`);
2246
+ process.exit(1);
2247
+ }
2248
+ console.log(`
2249
+ Validated ${validatedHooks.length} custom hook(s): ${validatedHooks.map((h) => h.name).join(", ")}`);
2250
+ }
2251
+ writeHooksConfig(configToWrite);
2252
+ console.log(`
2253
+ Enabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}`);
2254
+ if (removeCustomHooks) {
2255
+ console.log("Custom hooks path cleared.");
2256
+ } else if (configToWrite.customPoliciesPath) {
2257
+ console.log(`Custom hooks path: ${configToWrite.customPoliciesPath}`);
2258
+ }
2259
+ const settingsPath = getSettingsPath(scope, cwd);
2260
+ const settings = readSettings(settingsPath);
2261
+ if (!settings.hooks) {
2262
+ settings.hooks = {};
2263
+ }
2264
+ for (const eventType of HOOK_EVENT_TYPES) {
2265
+ const command = `"${binaryPath}" --hook ${eventType}`;
2266
+ const hookEntry = {
2267
+ type: "command",
2268
+ command,
2269
+ timeout: 60000,
2270
+ [FAILPROOFAI_HOOK_MARKER]: true
2271
+ };
2272
+ if (!settings.hooks[eventType]) {
2273
+ settings.hooks[eventType] = [];
2274
+ }
2275
+ const matchers = settings.hooks[eventType];
2276
+ let found = false;
2277
+ for (const matcher of matchers) {
2278
+ if (!matcher.hooks)
2279
+ continue;
2280
+ const failproofaiIdx = matcher.hooks.findIndex((h) => isFailproofaiHook(h));
2281
+ if (failproofaiIdx >= 0) {
2282
+ matcher.hooks[failproofaiIdx] = hookEntry;
2283
+ found = true;
2284
+ break;
2285
+ }
2286
+ }
2287
+ if (!found) {
2288
+ matchers.push({ hooks: [hookEntry] });
2289
+ }
2290
+ }
2291
+ writeSettings(settingsPath, settings);
2292
+ try {
2293
+ const newSet = new Set(selectedPolicies);
2294
+ const policiesAdded = selectedPolicies.filter((p) => !previousEnabled.has(p));
2295
+ const policiesRemoved = [...previousEnabled].filter((p) => !newSet.has(p));
2296
+ const distinctId = getInstanceId();
2297
+ await trackHookEvent(distinctId, "hooks_installed", {
2298
+ scope,
2299
+ policies: selectedPolicies,
2300
+ policy_count: selectedPolicies.length,
2301
+ policies_added: policiesAdded,
2302
+ policies_removed: policiesRemoved,
2303
+ ...source ? { source } : {},
2304
+ platform: platform(),
2305
+ arch: arch(),
2306
+ os_release: release(),
2307
+ hostname_hash: hashToId(hostname()),
2308
+ has_custom_hooks_path: !!configToWrite.customPoliciesPath,
2309
+ has_policy_params: !!(configToWrite.policyParams && Object.keys(configToWrite.policyParams).length > 0),
2310
+ param_policy_names: configToWrite.policyParams ? Object.keys(configToWrite.policyParams) : []
2311
+ });
2312
+ } catch {}
2313
+ console.log(`Failproof AI hooks installed for all ${HOOK_EVENT_TYPES.length} event types (scope: ${scope}).`);
2314
+ console.log(`Settings: ${settingsPath}`);
2315
+ console.log(`Binary: ${binaryPath}`);
2316
+ const otherScopes = deduplicateScopes(HOOK_SCOPES, cwd).filter((s) => s !== scope);
2317
+ const duplicates = otherScopes.filter((s) => hooksInstalledInSettings(s, cwd));
2318
+ if (duplicates.length > 0) {
2319
+ const scopeList = duplicates.map((s) => `${s} (${scopeLabel(s)})`).join(", ");
2320
+ console.log();
2321
+ console.log(`\x1B[33mWarning: Failproof AI hooks are also installed at ${scopeList}.\x1B[0m`);
2322
+ console.log(`Having hooks in multiple scopes may cause duplicate policy evaluation.`);
2323
+ console.log(`Use \`failproofai policies --uninstall --scope ${duplicates[0]}\` to remove the other installation,`);
2324
+ console.log(`or \`failproofai policies\` to see all scopes.`);
2325
+ }
2326
+ }
2327
+ async function removeHooks(policyNames, scope = "user", cwd, opts) {
2328
+ if (opts?.removeCustomHooks) {
2329
+ const config = readHooksConfig();
2330
+ delete config.customPoliciesPath;
2331
+ writeHooksConfig(config);
2332
+ console.log("Custom hooks path cleared.");
2333
+ }
2334
+ if (policyNames && policyNames.length > 0 && !(policyNames.length === 1 && policyNames[0] === "all")) {
2335
+ validatePolicyNames(policyNames);
2336
+ const config = readHooksConfig();
2337
+ const removeSet = new Set(policyNames);
2338
+ const remaining = config.enabledPolicies.filter((p) => !removeSet.has(p));
2339
+ const notEnabled = policyNames.filter((p) => !config.enabledPolicies.includes(p));
2340
+ if (notEnabled.length > 0) {
2341
+ console.log(`Warning: policy(ies) not currently enabled: ${notEnabled.join(", ")}`);
2342
+ }
2343
+ const { policyParams: existingParams, ...baseConfig } = config;
2344
+ const filteredParams = existingParams ? Object.fromEntries(Object.entries(existingParams).filter(([k]) => !removeSet.has(k))) : null;
2345
+ const updatedConfig = {
2346
+ ...baseConfig,
2347
+ enabledPolicies: remaining,
2348
+ ...filteredParams && Object.keys(filteredParams).length > 0 ? { policyParams: filteredParams } : {}
2349
+ };
2350
+ writeHooksConfig(updatedConfig);
2351
+ try {
2352
+ const distinctId = getInstanceId();
2353
+ const actuallyRemoved = policyNames.filter((p) => config.enabledPolicies.includes(p));
2354
+ await trackHookEvent(distinctId, "hooks_removed", {
2355
+ scope,
2356
+ removal_mode: opts?.betaOnly ? "beta_policies" : "policies",
2357
+ beta_only: opts?.betaOnly ?? false,
2358
+ policies_removed: actuallyRemoved,
2359
+ removed_count: actuallyRemoved.length,
2360
+ ...opts?.source ? { source: opts.source } : {},
2361
+ platform: platform(),
2362
+ arch: arch(),
2363
+ os_release: release(),
2364
+ hostname_hash: hashToId(hostname())
2365
+ });
2366
+ } catch {}
2367
+ console.log(`Disabled ${policyNames.length - notEnabled.length} policy(ies).`);
2368
+ console.log(`Remaining: ${remaining.length > 0 ? remaining.join(", ") : "(none)"}`);
2369
+ return;
2370
+ }
2371
+ const configBeforeRemoval = readHooksConfig();
2372
+ const scopesToRemove = scope === "all" ? [...HOOK_SCOPES] : [scope];
2373
+ let totalRemoved = 0;
2374
+ for (const s of scopesToRemove) {
2375
+ const settingsPath = getSettingsPath(s, cwd);
2376
+ if (!existsSync5(settingsPath)) {
2377
+ if (scope !== "all") {
2378
+ console.log("No settings file found. Nothing to remove.");
2379
+ return;
2380
+ }
2381
+ continue;
2382
+ }
2383
+ const settings = readSettings(settingsPath);
2384
+ if (!settings.hooks) {
2385
+ if (scope !== "all") {
2386
+ console.log("No hooks found in settings. Nothing to remove.");
2387
+ return;
2388
+ }
2389
+ continue;
2390
+ }
2391
+ const removed = removeHooksFromSettingsFile(settingsPath);
2392
+ totalRemoved += removed;
2393
+ if (scope !== "all") {
2394
+ console.log(`Removed ${removed} failproofai hook(s) from settings.`);
2395
+ console.log(`Settings: ${settingsPath}`);
2396
+ }
2397
+ }
2398
+ if (scope === "all") {
2399
+ console.log(`Removed ${totalRemoved} failproofai hook(s) from all scopes.`);
2400
+ for (const s of scopesToRemove) {
2401
+ console.log(` ${s}: ${getSettingsPath(s, cwd)}`);
2402
+ }
2403
+ }
2404
+ try {
2405
+ const distinctId = getInstanceId();
2406
+ await trackHookEvent(distinctId, "hooks_removed", {
2407
+ scope,
2408
+ removal_mode: "hooks",
2409
+ policies_removed: configBeforeRemoval.enabledPolicies,
2410
+ removed_count: totalRemoved,
2411
+ ...opts?.source ? { source: opts.source } : {},
2412
+ platform: platform(),
2413
+ arch: arch(),
2414
+ os_release: release(),
2415
+ hostname_hash: hashToId(hostname())
2416
+ });
2417
+ } catch {}
2418
+ if (scope === "all" || !HOOK_SCOPES.some((s) => hooksInstalledInSettings(s, cwd))) {
2419
+ const existingForClear = readHooksConfig();
2420
+ const { customPoliciesPath: _drop, policyParams: _dropParams, ...restClear } = existingForClear;
2421
+ writeHooksConfig({ ...restClear, enabledPolicies: [] });
2422
+ }
2423
+ }
2424
+ async function listHooks(cwd) {
2425
+ const config = readMergedHooksConfig(cwd);
2426
+ const enabledSet = new Set(config.enabledPolicies);
2427
+ const uniqueScopes = deduplicateScopes(HOOK_SCOPES, cwd);
2428
+ const installedScopes = uniqueScopes.filter((s) => hooksInstalledInSettings(s, cwd));
2429
+ const regularPolicies = BUILTIN_POLICIES.filter((p) => !p.beta);
2430
+ const betaPolicies = BUILTIN_POLICIES.filter((p) => p.beta);
2431
+ const nameColWidth = Math.max(...BUILTIN_POLICIES.map((p) => p.name.length)) + 2;
2432
+ const builtinPolicyNames = new Set(BUILTIN_POLICIES.map((p) => p.name));
2433
+ const printParamsSummary = (policyName, indent) => {
2434
+ const params = config.policyParams?.[policyName];
2435
+ if (!params)
2436
+ return;
2437
+ for (const [key, val] of Object.entries(params)) {
2438
+ console.log(`${indent} ${key}: ${JSON.stringify(val)}`);
2439
+ }
2440
+ };
2441
+ const statusCol = 8;
2442
+ const printSimpleRow = (policy) => {
2443
+ const mark = enabledSet.has(policy.name) ? `\x1B[32m✓\x1B[0m` : " ";
2444
+ console.log(` ${mark}${" ".repeat(statusCol - 1)}${policy.name.padEnd(nameColWidth)}${policy.description}`);
2445
+ printParamsSummary(policy.name, ` ${" ".repeat(statusCol)}`);
2446
+ };
2447
+ const printBetaSection = (printRow) => {
2448
+ if (betaPolicies.length > 0) {
2449
+ console.log(`
2450
+ \x1B[2m── Beta ──\x1B[0m`);
2451
+ for (const policy of betaPolicies)
2452
+ printRow(policy);
2453
+ }
2454
+ };
2455
+ if (installedScopes.length === 0) {
2456
+ console.log(`
2457
+ Failproof AI Policies — not installed
2458
+ `);
2459
+ console.log(` ${"Status".padEnd(statusCol)}${"Name".padEnd(nameColWidth)}Description`);
2460
+ console.log(` ${"─".repeat(6)} ${"─".repeat(nameColWidth - 2)} ${"─".repeat(38)}`);
2461
+ for (const policy of regularPolicies)
2462
+ printSimpleRow(policy);
2463
+ printBetaSection(printSimpleRow);
2464
+ if (config.enabledPolicies.length > 0) {
2465
+ console.log("\n Policies not installed. Run `failproofai policies --install` to activate.");
2466
+ } else {
2467
+ console.log("\n Run `failproofai policies --install` to get started.");
2468
+ }
2469
+ console.log(` Config: ~/.failproofai/policies-config.json
2470
+ `);
2471
+ } else if (installedScopes.length === 1) {
2472
+ const scope = installedScopes[0];
2473
+ console.log(`
2474
+ Failproof AI Hook Policies (${scope})
2475
+ `);
2476
+ console.log(` ${"Status".padEnd(statusCol)}${"Name".padEnd(nameColWidth)}Description`);
2477
+ console.log(` ${"─".repeat(6)} ${"─".repeat(nameColWidth - 2)} ${"─".repeat(38)}`);
2478
+ for (const policy of regularPolicies)
2479
+ printSimpleRow(policy);
2480
+ printBetaSection(printSimpleRow);
2481
+ console.log(`
2482
+ Config: ~/.failproofai/policies-config.json
2483
+ `);
2484
+ } else {
2485
+ const COL = 9;
2486
+ const scopeLabelMap = {
2487
+ user: "User",
2488
+ project: "Project",
2489
+ local: "Local"
2490
+ };
2491
+ console.log(`
2492
+ Failproof AI Hook Policies
2493
+ `);
2494
+ const buildScopePrefix = () => {
2495
+ let s = " ";
2496
+ for (const sc of installedScopes)
2497
+ s += scopeLabelMap[sc].padEnd(COL);
2498
+ return s;
2499
+ };
2500
+ const scopeHeaderWidth = installedScopes.length * COL;
2501
+ console.log(`${buildScopePrefix()}${"Name".padEnd(nameColWidth)}Description`);
2502
+ console.log(` ${"─".repeat(scopeHeaderWidth)}${"─".repeat(nameColWidth)}${"─".repeat(38)}`);
2503
+ const printMultiScopeRow = (policy) => {
2504
+ const enabled = enabledSet.has(policy.name);
2505
+ let row = " ";
2506
+ for (const _scope of installedScopes) {
2507
+ if (enabled) {
2508
+ row += `\x1B[32m✓ ON\x1B[0m` + " ".repeat(COL - 4);
2509
+ } else {
2510
+ row += " OFF" + " ".repeat(COL - 5);
2511
+ }
2512
+ }
2513
+ row += policy.name.padEnd(nameColWidth) + policy.description;
2514
+ console.log(row);
2515
+ printParamsSummary(policy.name, ` ${" ".repeat(scopeHeaderWidth)}`);
2516
+ };
2517
+ for (const policy of regularPolicies)
2518
+ printMultiScopeRow(policy);
2519
+ if (betaPolicies.length > 0) {
2520
+ console.log(`
2521
+ \x1B[2m── Beta ──\x1B[0m`);
2522
+ for (const policy of betaPolicies)
2523
+ printMultiScopeRow(policy);
2524
+ }
2525
+ console.log(`
2526
+ Config: ~/.failproofai/policies-config.json`);
2527
+ const scopeNames = installedScopes.join(", ");
2528
+ console.log();
2529
+ console.log(`\x1B[33m⚠ Hooks in multiple scopes (${scopeNames}).\x1B[0m`);
2530
+ console.log(` Consider keeping one. Remove with: failproofai policies --uninstall --scope <scope>
2531
+ `);
2532
+ }
2533
+ if (config.policyParams) {
2534
+ for (const key of Object.keys(config.policyParams)) {
2535
+ if (!builtinPolicyNames.has(key)) {
2536
+ console.log(` \x1B[33mWarning: unknown policyParams key "${key}" — possible typo\x1B[0m`);
2537
+ }
2538
+ }
2539
+ }
2540
+ if (config.customPoliciesPath) {
2541
+ console.log(`
2542
+ ── Custom Policies (${config.customPoliciesPath}) ───────────────────────`);
2543
+ if (!existsSync5(config.customPoliciesPath)) {
2544
+ console.log(` \x1B[31m✗ File not found: ${config.customPoliciesPath}\x1B[0m`);
2545
+ } else {
2546
+ const hooks = await loadCustomHooks(config.customPoliciesPath);
2547
+ if (hooks.length === 0) {
2548
+ console.log(` \x1B[31m✗ ERR failed to load (check ~/.failproofai/logs/hooks.log)\x1B[0m`);
2549
+ } else {
2550
+ const descColWidth = nameColWidth;
2551
+ for (const hook of hooks) {
2552
+ console.log(` \x1B[32m✓\x1B[0m ${hook.name.padEnd(descColWidth)}${hook.description ?? ""}`);
2553
+ }
2554
+ }
2555
+ }
2556
+ console.log();
2557
+ }
2558
+ }
2559
+ var VALID_POLICY_NAMES;
2560
+ var init_manager = __esm(() => {
2561
+ init_types();
2562
+ init_install_prompt();
2563
+ init_hooks_config();
2564
+ init_builtin_policies();
2565
+ init_custom_hooks_loader();
2566
+ init_hook_telemetry();
2567
+ init_telemetry_id();
2568
+ VALID_POLICY_NAMES = new Set(BUILTIN_POLICIES.map((p) => p.name));
2569
+ });
2570
+
2571
+ // lib/paths.ts
2572
+ import { homedir as homedir6 } from "os";
2573
+ import { join as join4 } from "path";
2574
+ function getDefaultClaudeProjectsPath() {
2575
+ return join4(homedir6(), ".claude", "projects");
2576
+ }
2577
+ var init_paths = () => {};
2578
+
2579
+ // scripts/parse-script-args.ts
2580
+ import { resolve as resolve6 } from "path";
2581
+ function parseStringFlag(flagName, errorLabel, inlineValue, args, index, options) {
2582
+ const raw = inlineValue ?? args[index + 1];
2583
+ if (raw === undefined || inlineValue === null && raw.startsWith("-")) {
2584
+ console.error(`Error: ${flagName} requires ${errorLabel}`);
2585
+ process.exit(1);
2586
+ }
2587
+ const value = options?.resolve ? resolve6(raw) : raw;
2588
+ return { value, spliceCount: inlineValue !== null ? 1 : 2 };
2589
+ }
2590
+ function parseScriptArgs(argv) {
2591
+ const args = [...argv];
2592
+ let claudeProjectsPath;
2593
+ let loggingLevel;
2594
+ let disableTelemetry = false;
2595
+ let allowedDevOrigins;
2596
+ for (let i = 0;i < args.length; i++) {
2597
+ const arg = args[i];
2598
+ const eqIdx = arg.indexOf("=");
2599
+ const flag = eqIdx >= 0 ? arg.slice(0, eqIdx) : arg;
2600
+ const inlineValue = eqIdx >= 0 ? arg.slice(eqIdx + 1) : null;
2601
+ if (flag === "--projects-path" || flag === "-p") {
2602
+ const { value, spliceCount } = parseStringFlag(flag, "a path argument", inlineValue, args, i);
2603
+ claudeProjectsPath = value;
2604
+ args.splice(i, spliceCount);
2605
+ i--;
2606
+ continue;
2607
+ }
2608
+ if (flag === "--logging") {
2609
+ const raw = inlineValue ?? args[i + 1];
2610
+ if (raw === undefined || inlineValue === null && raw.startsWith("-")) {
2611
+ console.error("Error: --logging requires a level (info, warn, error)");
2612
+ process.exit(1);
2613
+ }
2614
+ const val = raw.toLowerCase();
2615
+ if (val !== "info" && val !== "warn" && val !== "error") {
2616
+ console.error("Error: --logging must be one of: info, warn, error");
2617
+ process.exit(1);
2618
+ }
2619
+ loggingLevel = val;
2620
+ args.splice(i, inlineValue !== null ? 1 : 2);
2621
+ i--;
2622
+ continue;
2623
+ }
2624
+ if (flag === "--disable-telemetry") {
2625
+ disableTelemetry = true;
2626
+ args.splice(i, 1);
2627
+ i--;
2628
+ continue;
2629
+ }
2630
+ if (flag === "--allowed-origins") {
2631
+ const { value, spliceCount } = parseStringFlag(flag, "a comma-separated list of origins", inlineValue, args, i);
2632
+ allowedDevOrigins = value.split(",").map((s) => s.trim()).filter(Boolean);
2633
+ args.splice(i, spliceCount);
2634
+ i--;
2635
+ continue;
2636
+ }
2637
+ }
2638
+ return { claudeProjectsPath, loggingLevel, disableTelemetry, allowedDevOrigins, remainingArgs: args };
2639
+ }
2640
+ var init_parse_script_args = () => {};
2641
+
2642
+ // scripts/launch.ts
2643
+ var exports_launch = {};
2644
+ __export(exports_launch, {
2645
+ launch: () => launch
2646
+ });
2647
+ import { spawn } from "child_process";
2648
+ import { realpathSync, existsSync as existsSync6 } from "node:fs";
2649
+ import { resolve as resolve7, dirname as dirname4 } from "node:path";
2650
+ import { fileURLToPath } from "node:url";
2651
+ function launch(mode) {
2652
+ const { claudeProjectsPath: parsedPath, loggingLevel, disableTelemetry, allowedDevOrigins, remainingArgs } = parseScriptArgs(process.argv.slice(2));
2653
+ console.log(`
2654
+ ______ _ __ ____ ___ ____
2655
+ / ____/___ _(_) /___ _________ ____ / __/ / | / _/
2656
+ / /_ / __ \`/ / / __ \\/ ___/ __ \\/ __ \\/ /_ / /| | / /
2657
+ / __/ / /_/ / / / /_/ / / / /_/ / /_/ / __/ / ___ |_/ /
2658
+ /_/ \\__,_/_/_/ .___/_/ \\____/\\____/_/ /_/ |_/___/
2659
+ /_/ v${version2}
2660
+ `);
2661
+ console.log(` ⭐ Star us: https://github.com/exospherehost/failproofai`);
2662
+ console.log(` \uD83D\uDCD6 Docs: https://befailproof.ai
2663
+ `);
2664
+ let claudeProjectsPath = parsedPath;
2665
+ if (!claudeProjectsPath) {
2666
+ claudeProjectsPath = getDefaultClaudeProjectsPath();
2667
+ console.log(`Using default .claude projects path: ${claudeProjectsPath}`);
2668
+ } else {
2669
+ console.log(`Using custom .claude projects path: ${claudeProjectsPath}`);
2670
+ }
2671
+ process.env.CLAUDE_PROJECTS_PATH = claudeProjectsPath;
2672
+ let cmd;
2673
+ let cmdArgs;
2674
+ if (mode === "start") {
2675
+ const portIdx = remainingArgs.indexOf("--port");
2676
+ const port = portIdx >= 0 ? remainingArgs[portIdx + 1] : "8020";
2677
+ process.env.PORT = port;
2678
+ process.env.HOSTNAME = "0.0.0.0";
2679
+ cmd = "node";
2680
+ const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT ?? resolve7(dirname4(realpathSync(fileURLToPath(import.meta.url))), "..");
2681
+ const serverJsPath = resolve7(packageRoot, ".next/standalone/server.js");
2682
+ if (!existsSync6(serverJsPath)) {
2683
+ console.error(`
2684
+ Error: Cannot find server.js at:
2685
+ ${serverJsPath}
2686
+
2687
+ ` + `The package may be missing its build output.
2688
+ ` + `Try reinstalling:
2689
+ npm install -g failproofai@latest
2690
+ `);
2691
+ process.exit(1);
2692
+ }
2693
+ cmdArgs = [serverJsPath];
2694
+ } else {
2695
+ cmd = "bunx";
2696
+ cmdArgs = ["--bun", "next", "dev", ...remainingArgs];
2697
+ }
2698
+ const nextProcess = spawn(cmd, cmdArgs, {
2699
+ stdio: "inherit",
2700
+ env: {
2701
+ ...process.env,
2702
+ CLAUDE_PROJECTS_PATH: claudeProjectsPath,
2703
+ ...loggingLevel ? { FAILPROOFAI_LOG_LEVEL: loggingLevel } : {},
2704
+ ...disableTelemetry ? { FAILPROOFAI_TELEMETRY_DISABLED: "1" } : {},
2705
+ ...allowedDevOrigins ? { FAILPROOFAI_ALLOWED_DEV_ORIGINS: allowedDevOrigins.join(",") } : {}
2706
+ }
2707
+ });
2708
+ nextProcess.on("error", (error) => {
2709
+ console.error("Error starting Next.js:", error);
2710
+ process.exit(1);
2711
+ });
2712
+ nextProcess.on("exit", (code) => {
2713
+ process.exit(code || 0);
2714
+ });
2715
+ }
2716
+ var init_launch = __esm(() => {
2717
+ init_paths();
2718
+ init_parse_script_args();
2719
+ init_package();
2720
+ });
2721
+
2722
+ // bin/failproofai.mjs
2723
+ import { realpathSync as realpathSync2 } from "fs";
2724
+ import { dirname as dirname5, resolve as resolve8 } from "path";
2725
+ import { fileURLToPath as fileURLToPath2 } from "url";
2726
+ // package.json
2727
+ var version = "0.0.2-beta.1";
2728
+
2729
+ // bin/failproofai.mjs
2730
+ if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
2731
+ process.env.FAILPROOFAI_PACKAGE_ROOT = resolve8(dirname5(realpathSync2(fileURLToPath2(import.meta.url))), "..");
2732
+ }
2733
+ if (!process.env.FAILPROOFAI_DIST_PATH) {
2734
+ process.env.FAILPROOFAI_DIST_PATH = resolve8(dirname5(realpathSync2(fileURLToPath2(import.meta.url))), "..", "dist");
2735
+ }
2736
+ var args = process.argv.slice(2);
2737
+ if (args[0] === "p")
2738
+ args[0] = "policies";
2739
+ var SUBCOMMANDS = ["policies"];
2740
+ if ((args.includes("--help") || args.includes("-h")) && !SUBCOMMANDS.includes(args[0])) {
2741
+ console.log(`
2742
+ failproofai v${version}
2743
+
2744
+ USAGE
2745
+ failproofai [command] [options]
2746
+
2747
+ COMMANDS
2748
+ (no args) Launch the policy dashboard
2749
+
2750
+ policies, p List all available policies and their status
2751
+ policies --install, -i Enable policies in Claude Code settings
2752
+ [names...] Specific policy names to enable
2753
+ --scope user|project|local Config scope to write to (default: user)
2754
+ --beta Include beta policies
2755
+ --custom, -c <path> Path to a JS file of custom policies
2756
+
2757
+ policies --uninstall, -u Disable policies or remove hooks
2758
+ [names...] Specific policy names to disable
2759
+ --scope user|project|local|all Config scope to remove from (default: user)
2760
+ --beta Remove only beta policies
2761
+ --custom, -c Clear the customPoliciesPath from config
2762
+
2763
+ policies --help, -h Show this help for the policies command
2764
+
2765
+ --version, -v Print version and exit
2766
+ --help, -h Show this help message
2767
+
2768
+ EXAMPLES
2769
+ failproofai policies
2770
+ failproofai policies --install
2771
+ failproofai policies --install block-sudo sanitize-api-keys --scope project
2772
+ failproofai policies --install --custom ./my-policies.js
2773
+ failproofai policies -i -c ./my-policies.js
2774
+ failproofai policies --uninstall block-sudo
2775
+ failproofai policies --uninstall --custom
2776
+
2777
+ LINKS
2778
+ \u2B50 Star us: https://github.com/exospherehost/failproofai
2779
+ \uD83D\uDCD6 Docs: https://befailproof.ai
2780
+ `.trimStart());
2781
+ process.exit(0);
2782
+ }
2783
+ if (args.includes("--version") || args.includes("-v")) {
2784
+ console.log(version);
2785
+ process.exit(0);
2786
+ }
2787
+ var hookIdx = args.indexOf("--hook");
2788
+ if (hookIdx >= 0 && args[hookIdx + 1]) {
2789
+ const { handleHookEvent: handleHookEvent2 } = await Promise.resolve().then(() => (init_handler(), exports_handler));
2790
+ const exitCode = await handleHookEvent2(args[hookIdx + 1]);
2791
+ process.exit(exitCode);
2792
+ }
2793
+ if (args[0] === "policies") {
2794
+ const subArgs = args.slice(1);
2795
+ const isInstall = subArgs.includes("--install") || subArgs.includes("-i");
2796
+ const isUninstall = subArgs.includes("--uninstall") || subArgs.includes("-u");
2797
+ const isHelp = subArgs.includes("--help") || subArgs.includes("-h");
2798
+ if (isHelp) {
2799
+ console.log(`
2800
+ failproofai policies \u2014 manage Failproof AI policies
2801
+
2802
+ USAGE
2803
+ failproofai policies List all policies and their status
2804
+ failproofai policies --install, -i Enable policies
2805
+ failproofai policies --uninstall, -u Disable policies or remove hooks
2806
+
2807
+ OPTIONS (install)
2808
+ [names...] Specific policy names to enable (omit for interactive)
2809
+ --scope user|project|local Config scope to write to (default: user)
2810
+ --beta Include beta policies
2811
+ --custom, -c <path> Path to a JS file of custom policies
2812
+ (skips interactive prompt; validates file first)
2813
+
2814
+ OPTIONS (uninstall)
2815
+ [names...] Specific policy names to disable (omit to remove hooks)
2816
+ --scope user|project|local|all Config scope to remove from (default: user)
2817
+ --beta Remove only beta policies
2818
+ --custom, -c Clear the customPoliciesPath from config
2819
+
2820
+ EXAMPLES
2821
+ failproofai policies
2822
+ failproofai policies --install
2823
+ failproofai policies --install block-sudo sanitize-api-keys
2824
+ failproofai policies --install --custom ./my-policies.js
2825
+ failproofai policies -i -c ./my-policies.js
2826
+ failproofai policies --uninstall block-sudo
2827
+ failproofai policies -u
2828
+ failproofai policies --uninstall --custom
2829
+ `.trimStart());
2830
+ process.exit(0);
2831
+ }
2832
+ if (isInstall) {
2833
+ const { installHooks: installHooks2 } = await Promise.resolve().then(() => (init_manager(), exports_manager));
2834
+ const scopeIdx = subArgs.indexOf("--scope");
2835
+ const scope = scopeIdx >= 0 ? subArgs[scopeIdx + 1] : "user";
2836
+ const customIdx = subArgs.includes("--custom") ? subArgs.indexOf("--custom") : subArgs.includes("-c") ? subArgs.indexOf("-c") : -1;
2837
+ const customPoliciesPath = customIdx >= 0 ? subArgs[customIdx + 1] : undefined;
2838
+ const includeBeta = subArgs.includes("--beta");
2839
+ const consumed = new Set([scope, customPoliciesPath].filter(Boolean));
2840
+ const flags = new Set(["--install", "-i", "--scope", "--beta", "--custom", "-c"]);
2841
+ const explicitPolicyNames = subArgs.filter((a) => !a.startsWith("-") && !flags.has(a) && !consumed.has(a));
2842
+ const policyNames = explicitPolicyNames.length > 0 ? explicitPolicyNames : customPoliciesPath !== undefined ? [] : undefined;
2843
+ await installHooks2(policyNames, scope, undefined, includeBeta, undefined, customPoliciesPath);
2844
+ process.exit(0);
2845
+ }
2846
+ if (isUninstall) {
2847
+ const { removeHooks: removeHooks2 } = await Promise.resolve().then(() => (init_manager(), exports_manager));
2848
+ const scopeIdx = subArgs.indexOf("--scope");
2849
+ const scope = scopeIdx >= 0 ? subArgs[scopeIdx + 1] : "user";
2850
+ const betaOnly = subArgs.includes("--beta");
2851
+ const removeCustomHooks = subArgs.includes("--custom") || subArgs.includes("-c");
2852
+ const consumed = new Set([scope].filter(Boolean));
2853
+ const flags = new Set(["--uninstall", "-u", "--scope", "--beta", "--custom", "-c"]);
2854
+ const policyNames = subArgs.filter((a) => !a.startsWith("-") && !flags.has(a) && !consumed.has(a));
2855
+ await removeHooks2(policyNames.length > 0 ? policyNames : undefined, scope, undefined, { betaOnly, removeCustomHooks });
2856
+ process.exit(0);
2857
+ }
2858
+ const { listHooks: listHooks2 } = await Promise.resolve().then(() => (init_manager(), exports_manager));
2859
+ await listHooks2();
2860
+ process.exit(0);
2861
+ }
2862
+ var knownFlags = ["--version", "-v", "--help", "-h", "--hook"];
2863
+ var unknownFlag = args.find((a) => a.startsWith("-") && !knownFlags.includes(a));
2864
+ if (unknownFlag) {
2865
+ let levenshtein = function(a, b) {
2866
+ const m = a.length, n = b.length;
2867
+ const dp = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0));
2868
+ for (let i = 1;i <= m; i++)
2869
+ for (let j = 1;j <= n; j++)
2870
+ dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
2871
+ return dp[m][n];
2872
+ };
2873
+ const primary = ["--version", "--help", "--hook", "policies"];
2874
+ const closest = primary.reduce((best, flag) => {
2875
+ const dist = levenshtein(unknownFlag, flag);
2876
+ return dist < best.dist ? { flag, dist } : best;
2877
+ }, { flag: primary[0], dist: Infinity });
2878
+ console.error(`Unknown flag: ${unknownFlag}`);
2879
+ console.error(`Did you mean: ${closest.flag}?`);
2880
+ console.error(`Run \`failproofai --help\` for usage details.`);
2881
+ process.exit(1);
2882
+ }
2883
+ var unknownSubcommand = args.find((a) => !a.startsWith("-") && a !== "policies");
2884
+ if (unknownSubcommand) {
2885
+ console.error(`Unknown command: ${unknownSubcommand}`);
2886
+ console.error(`Did you mean: failproofai policies?`);
2887
+ console.error(`Run \`failproofai --help\` for usage details.`);
2888
+ process.exit(1);
2889
+ }
2890
+ var { launch: launch2 } = await Promise.resolve().then(() => (init_launch(), exports_launch));
2891
+ launch2("start");