failproofai 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) 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]__0g72weg._.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]__0g.lg8b._.js +2 -2
  54. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +2 -2
  55. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0okos0k._.js +2 -2
  56. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0t3ka1q._.js → [root-of-the-server]__0tjjyb9._.js} +2 -2
  57. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +7 -7
  58. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ow37ro._.js → [root-of-the-server]__0zn7uo6._.js} +2 -2
  59. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +2 -2
  60. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +2 -2
  61. package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
  62. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +1 -1
  64. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  65. package/.next/standalone/.next/server/pages/404.html +2 -2
  66. package/.next/standalone/.next/server/pages/500.html +1 -1
  67. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  68. package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
  69. package/.next/standalone/.next/static/chunks/{0lq8ary5l4s8t.js → 01l2mh88iy.ga.js} +1 -1
  70. package/.next/standalone/.next/static/chunks/{04iuhj_-h-21-.js → 0388wpenm9-a4.js} +1 -1
  71. package/.next/standalone/.next/static/chunks/{0ubv3x~0zdd_w.js → 0a0lh_a4f_xs-.js} +1 -1
  72. package/.next/standalone/.next/static/chunks/{13cot7j99xkb~.js → 0f_9854du76y2.js} +1 -1
  73. package/.next/standalone/.next/static/chunks/{0-igg2k65fzo_.js → 0j2o20pqkib~d.js} +1 -1
  74. package/.next/standalone/.next/static/chunks/{0k5t-n0s8p2nr.js → 0kkzzoo.s-t3p.js} +1 -1
  75. package/.next/standalone/.next/static/chunks/{0v23ca5xty1n~.js → 0x0o8~u4jsatb.js} +2 -2
  76. package/.next/standalone/.next/static/chunks/{061hxr2b-j.6q.js → 12wu.28cbx4dl.js} +1 -1
  77. package/.next/standalone/package.json +1 -1
  78. package/.next/standalone/server.js +1 -1
  79. package/dist/cli.mjs +203 -46
  80. package/package.json +1 -1
  81. package/src/hooks/builtin-policies.ts +196 -44
  82. package/src/hooks/custom-hooks-loader.ts +6 -3
  83. package/src/hooks/hooks-config.ts +31 -5
  84. /package/.next/standalone/.next/static/{my01WPjry7ohRUHyTaYp4 → 9FNjQiktocMN-qDiGqDL5}/_buildManifest.js +0 -0
  85. /package/.next/standalone/.next/static/{my01WPjry7ohRUHyTaYp4 → 9FNjQiktocMN-qDiGqDL5}/_clientMiddlewareManifest.js +0 -0
  86. /package/.next/standalone/.next/static/{my01WPjry7ohRUHyTaYp4 → 9FNjQiktocMN-qDiGqDL5}/_ssgManifest.js +0 -0
package/dist/cli.mjs CHANGED
@@ -103,7 +103,7 @@ var init_hook_logger = __esm(() => {
103
103
  });
104
104
 
105
105
  // src/hooks/hooks-config.ts
106
- import { readFileSync, writeFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
106
+ import { readFileSync, writeFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, statSync as statSync2 } from "node:fs";
107
107
  import { resolve, dirname } from "node:path";
108
108
  import { homedir as homedir2 } from "node:os";
109
109
  function readConfigAt(path) {
@@ -117,8 +117,24 @@ function readConfigAt(path) {
117
117
  return {};
118
118
  }
119
119
  }
120
+ function findProjectConfigDir(start) {
121
+ const home = homedir2();
122
+ let dir = resolve(start);
123
+ while (dir !== home) {
124
+ const marker = resolve(dir, ".failproofai");
125
+ try {
126
+ if (statSync2(marker).isDirectory())
127
+ return dir;
128
+ } catch {}
129
+ const parent = dirname(dir);
130
+ if (parent === dir)
131
+ break;
132
+ dir = parent;
133
+ }
134
+ return resolve(start);
135
+ }
120
136
  function readMergedHooksConfig(cwd) {
121
- const base = cwd ? resolve(cwd) : process.cwd();
137
+ const base = findProjectConfigDir(cwd ?? process.cwd());
122
138
  const projectPath = resolve(base, ".failproofai", "policies-config.json");
123
139
  const localPath = resolve(base, ".failproofai", "policies-config.local.json");
124
140
  const globalPath = resolve(homedir2(), ".failproofai", "policies-config.json");
@@ -150,14 +166,13 @@ function readMergedHooksConfig(cwd) {
150
166
  };
151
167
  }
152
168
  function getConfigPathForScope(scope, cwd) {
153
- const base = cwd ? resolve(cwd) : process.cwd();
154
169
  switch (scope) {
155
170
  case "user":
156
171
  return resolve(homedir2(), ".failproofai", "policies-config.json");
157
172
  case "project":
158
- return resolve(base, ".failproofai", "policies-config.json");
173
+ return resolve(findProjectConfigDir(cwd ?? process.cwd()), ".failproofai", "policies-config.json");
159
174
  case "local":
160
- return resolve(base, ".failproofai", "policies-config.local.json");
175
+ return resolve(findProjectConfigDir(cwd ?? process.cwd()), ".failproofai", "policies-config.local.json");
161
176
  }
162
177
  }
163
178
  function readScopedHooksConfig(scope, cwd) {
@@ -768,6 +783,38 @@ function blockFailproofaiCommands(ctx) {
768
783
  }
769
784
  return allow();
770
785
  }
786
+ function blockInfraCli(ctx, re, denyMsg) {
787
+ if (ctx.toolName !== "Bash")
788
+ return allow();
789
+ const cmd = getCommand(ctx);
790
+ if (!re.test(cmd))
791
+ return allow();
792
+ const allowPatterns = ctx.params?.allowPatterns ?? [];
793
+ if (allowPatterns.some((p) => matchesAllowedPattern(cmd, p)))
794
+ return allow();
795
+ return deny(denyMsg);
796
+ }
797
+ function blockKubectl(ctx) {
798
+ return blockInfraCli(ctx, KUBECTL_RE, "kubectl commands are blocked");
799
+ }
800
+ function blockTerraform(ctx) {
801
+ return blockInfraCli(ctx, TERRAFORM_RE, "terraform/tofu commands are blocked");
802
+ }
803
+ function blockAwsCli(ctx) {
804
+ return blockInfraCli(ctx, AWS_CLI_RE, "aws CLI commands are blocked");
805
+ }
806
+ function blockGcloud(ctx) {
807
+ return blockInfraCli(ctx, GCLOUD_RE, "gcloud commands are blocked");
808
+ }
809
+ function blockAzCli(ctx) {
810
+ return blockInfraCli(ctx, AZ_CLI_RE, "az (Azure) CLI commands are blocked");
811
+ }
812
+ function blockHelm(ctx) {
813
+ return blockInfraCli(ctx, HELM_RE, "helm commands are blocked");
814
+ }
815
+ function blockGhPipeline(ctx) {
816
+ return blockInfraCli(ctx, GH_PIPELINE_RE, "gh pipeline-trigger commands are blocked");
817
+ }
771
818
  async function warnRepeatedToolCalls(ctx) {
772
819
  const THRESHOLD = 3;
773
820
  const transcriptPath = ctx.session?.transcriptPath;
@@ -1060,7 +1107,31 @@ function requireNoConflictsBeforeStop(ctx) {
1060
1107
  if (branch === baseBranch) {
1061
1108
  return allow(`On base branch "${baseBranch}", skipping conflict check.`);
1062
1109
  }
1063
- let localSkipped = false;
1110
+ try {
1111
+ execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
1112
+ } catch {
1113
+ return allow("gh CLI not installed, skipping conflict check.");
1114
+ }
1115
+ let prJson;
1116
+ try {
1117
+ prJson = execSync("gh pr view --json mergeable,number,url,state", {
1118
+ cwd,
1119
+ encoding: "utf8",
1120
+ stdio: ["pipe", "pipe", "pipe"],
1121
+ timeout: 15000
1122
+ }).trim();
1123
+ } catch {
1124
+ return allow("No pull request found for branch, skipping conflict check.");
1125
+ }
1126
+ let pr;
1127
+ try {
1128
+ pr = JSON.parse(prJson);
1129
+ } catch {
1130
+ return allow("Could not parse gh pr view output, skipping conflict check.");
1131
+ }
1132
+ if (pr.state !== "OPEN") {
1133
+ return allow(`PR #${pr.number} is ${pr.state.toLowerCase()}; skipping conflict check.`);
1134
+ }
1064
1135
  try {
1065
1136
  execFileSync("git", ["rev-parse", "--verify", `origin/${baseBranch}`], {
1066
1137
  cwd,
@@ -1069,9 +1140,7 @@ function requireNoConflictsBeforeStop(ctx) {
1069
1140
  timeout: 3000
1070
1141
  });
1071
1142
  const ahead = execFileSync("git", ["log", `origin/${baseBranch}..HEAD`, "--oneline"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
1072
- if (!ahead) {
1073
- localSkipped = true;
1074
- } else {
1143
+ if (ahead) {
1075
1144
  execFileSync("git", ["merge-tree", "--write-tree", "--name-only", `origin/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 1e4 });
1076
1145
  }
1077
1146
  } catch (err) {
@@ -1090,32 +1159,6 @@ function requireNoConflictsBeforeStop(ctx) {
1090
1159
  const fileList = files.length ? files.join(", ") : "one or more files";
1091
1160
  return deny(`Branch "${branch}" has merge conflicts with ${baseBranch} in: ${fileList}. ` + `Rebase or merge origin/${baseBranch} now and resolve the conflicts.`);
1092
1161
  }
1093
- localSkipped = true;
1094
- }
1095
- try {
1096
- execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
1097
- } catch {
1098
- return allow(localSkipped ? "Local conflict check skipped and gh CLI not installed, skipping conflict check." : `Branch "${branch}" merges cleanly with ${baseBranch} locally (gh CLI not installed, PR mergeability not verified).`);
1099
- }
1100
- let prJson;
1101
- try {
1102
- prJson = execSync("gh pr view --json mergeable,number,url,state", {
1103
- cwd,
1104
- encoding: "utf8",
1105
- stdio: ["pipe", "pipe", "pipe"],
1106
- timeout: 15000
1107
- }).trim();
1108
- } catch {
1109
- return allow(localSkipped ? "No pull request found for branch, skipping conflict check." : `Branch "${branch}" merges cleanly with ${baseBranch} locally (no PR to verify against).`);
1110
- }
1111
- let pr;
1112
- try {
1113
- pr = JSON.parse(prJson);
1114
- } catch {
1115
- return allow("Could not parse gh pr view output, skipping PR mergeability check.");
1116
- }
1117
- if (pr.state !== "OPEN") {
1118
- return allow(`PR #${pr.number} is ${pr.state.toLowerCase()}; skipping conflict check.`);
1119
1162
  }
1120
1163
  if (pr.mergeable === "CONFLICTING") {
1121
1164
  return deny(`PR #${pr.number} has merge conflicts per GitHub (${pr.url}). ` + `Rebase or merge origin/${baseBranch} now and resolve the conflicts.`);
@@ -1178,7 +1221,7 @@ function registerBuiltinPolicies(enabledNames) {
1178
1221
  }
1179
1222
  }
1180
1223
  }
1181
- 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, PKG_MANAGER_DETECTORS, NOHUP_RE, SCREEN_DETACH_RE, TMUX_DETACH_RE, DISOWN_RE, BACKGROUND_AMPERSAND_RE, gitBranchCache, READ_LIKE_CMDS, TOOL_CALL_TRACKER_MAX_BYTES = 65536, SEGMENT_SPLIT_RE, BUILTIN_POLICIES;
1224
+ 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, PKG_MANAGER_DETECTORS, NOHUP_RE, SCREEN_DETACH_RE, TMUX_DETACH_RE, DISOWN_RE, BACKGROUND_AMPERSAND_RE, KUBECTL_RE, TERRAFORM_RE, AWS_CLI_RE, GCLOUD_RE, AZ_CLI_RE, HELM_RE, GH_PIPELINE_RE, gitBranchCache, READ_LIKE_CMDS, TOOL_CALL_TRACKER_MAX_BYTES = 65536, SEGMENT_SPLIT_RE, BUILTIN_POLICIES;
1182
1225
  var init_builtin_policies = __esm(() => {
1183
1226
  init_hook_logger();
1184
1227
  SHELL_OPERATORS = new Set(["&&", "||", "|", ";"]);
@@ -1251,6 +1294,13 @@ var init_builtin_policies = __esm(() => {
1251
1294
  TMUX_DETACH_RE = /\btmux\s+(?:new-session|new)\b[^|&;]*-d\b/;
1252
1295
  DISOWN_RE = /\bdisown\b/;
1253
1296
  BACKGROUND_AMPERSAND_RE = /(?<![&|])\s?&\s*(?:$|#|;)/;
1297
+ KUBECTL_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*kubectl(?:\s|$)/;
1298
+ TERRAFORM_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*(?:terraform|tofu)(?:\s|$)/;
1299
+ AWS_CLI_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*aws(?:\s|$)/;
1300
+ GCLOUD_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*gcloud(?:\s|$)/;
1301
+ AZ_CLI_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*az(?:\s|$)/;
1302
+ HELM_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*helm(?:\s|$)/;
1303
+ GH_PIPELINE_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*gh\s+(?:workflow\s+(?:run|enable|disable)|run\s+(?:rerun|cancel)|pr\s+merge|release\s+(?:create|delete)|cache\s+delete|secret\s+(?:set|delete))\b/;
1254
1304
  gitBranchCache = new Map;
1255
1305
  READ_LIKE_CMDS = /(?:^|;|&&|\|\||\|)\s*(?:ls|find|cat|head|tail|less|more|wc|file|stat|tree|du)\s/;
1256
1306
  SEGMENT_SPLIT_RE = /\s*(?:&&|\|\||\||;)\s*/;
@@ -1379,6 +1429,111 @@ var init_builtin_policies = __esm(() => {
1379
1429
  defaultEnabled: true,
1380
1430
  category: "Dangerous Commands"
1381
1431
  },
1432
+ {
1433
+ name: "block-kubectl",
1434
+ description: "Block kubectl commands (Kubernetes cluster mutations)",
1435
+ fn: blockKubectl,
1436
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1437
+ defaultEnabled: false,
1438
+ category: "Infra Commands",
1439
+ params: {
1440
+ allowPatterns: {
1441
+ type: "string[]",
1442
+ description: "kubectl command patterns to allow, matched token-by-token (e.g. 'kubectl get *', 'kubectl describe *')",
1443
+ default: []
1444
+ }
1445
+ }
1446
+ },
1447
+ {
1448
+ name: "block-terraform",
1449
+ description: "Block terraform and tofu (OpenTofu) commands",
1450
+ fn: blockTerraform,
1451
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1452
+ defaultEnabled: false,
1453
+ category: "Infra Commands",
1454
+ params: {
1455
+ allowPatterns: {
1456
+ type: "string[]",
1457
+ description: "terraform/tofu command patterns to allow (e.g. 'terraform plan', 'terraform validate')",
1458
+ default: []
1459
+ }
1460
+ }
1461
+ },
1462
+ {
1463
+ name: "block-aws-cli",
1464
+ description: "Block aws CLI commands",
1465
+ fn: blockAwsCli,
1466
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1467
+ defaultEnabled: false,
1468
+ category: "Infra Commands",
1469
+ params: {
1470
+ allowPatterns: {
1471
+ type: "string[]",
1472
+ description: "aws CLI command patterns to allow (e.g. 'aws s3 ls *', 'aws sts get-caller-identity')",
1473
+ default: []
1474
+ }
1475
+ }
1476
+ },
1477
+ {
1478
+ name: "block-gcloud",
1479
+ description: "Block gcloud (Google Cloud) CLI commands",
1480
+ fn: blockGcloud,
1481
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1482
+ defaultEnabled: false,
1483
+ category: "Infra Commands",
1484
+ params: {
1485
+ allowPatterns: {
1486
+ type: "string[]",
1487
+ description: "gcloud command patterns to allow (e.g. 'gcloud auth list', 'gcloud config list')",
1488
+ default: []
1489
+ }
1490
+ }
1491
+ },
1492
+ {
1493
+ name: "block-az-cli",
1494
+ description: "Block az (Azure) CLI commands",
1495
+ fn: blockAzCli,
1496
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1497
+ defaultEnabled: false,
1498
+ category: "Infra Commands",
1499
+ params: {
1500
+ allowPatterns: {
1501
+ type: "string[]",
1502
+ description: "az CLI command patterns to allow (e.g. 'az account show', 'az group list')",
1503
+ default: []
1504
+ }
1505
+ }
1506
+ },
1507
+ {
1508
+ name: "block-helm",
1509
+ description: "Block helm commands",
1510
+ fn: blockHelm,
1511
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1512
+ defaultEnabled: false,
1513
+ category: "Infra Commands",
1514
+ params: {
1515
+ allowPatterns: {
1516
+ type: "string[]",
1517
+ description: "helm command patterns to allow (e.g. 'helm list', 'helm status *')",
1518
+ default: []
1519
+ }
1520
+ }
1521
+ },
1522
+ {
1523
+ name: "block-gh-pipeline",
1524
+ description: "Block gh CLI pipeline-trigger subcommands (workflow run, run rerun/cancel, pr merge, release create/delete, cache delete, secret set/delete)",
1525
+ fn: blockGhPipeline,
1526
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1527
+ defaultEnabled: false,
1528
+ category: "Infra Commands",
1529
+ params: {
1530
+ allowPatterns: {
1531
+ type: "string[]",
1532
+ description: "gh pipeline command patterns to allow (e.g. specific scripted invocations); read-only gh subcommands like 'gh pr view' and 'gh run list' are not matched by this policy",
1533
+ default: []
1534
+ }
1535
+ }
1536
+ },
1382
1537
  {
1383
1538
  name: "block-secrets-write",
1384
1539
  description: "Block writing secret key files",
@@ -1981,8 +2136,9 @@ async function loadCustomHooks(customPoliciesPath, opts) {
1981
2136
  async function loadAllCustomHooks(customPoliciesPath, opts) {
1982
2137
  clearCustomHooks();
1983
2138
  const conventionSources = [];
2139
+ const projectRoot = findProjectConfigDir(opts?.sessionCwd ?? process.cwd());
1984
2140
  if (customPoliciesPath) {
1985
- const absPath = isAbsolute(customPoliciesPath) ? customPoliciesPath : resolve4(opts?.sessionCwd ?? process.cwd(), customPoliciesPath);
2141
+ const absPath = isAbsolute(customPoliciesPath) ? customPoliciesPath : resolve4(projectRoot, customPoliciesPath);
1986
2142
  if (existsSync3(absPath)) {
1987
2143
  await loadSingleFile(absPath);
1988
2144
  } else {
@@ -1990,7 +2146,7 @@ async function loadAllCustomHooks(customPoliciesPath, opts) {
1990
2146
  }
1991
2147
  }
1992
2148
  const hooksBeforeConvention = getCustomHooks().length;
1993
- const projectDir = resolve4(opts?.sessionCwd ?? process.cwd(), ".failproofai", "policies");
2149
+ const projectDir = resolve4(projectRoot, ".failproofai", "policies");
1994
2150
  const projectFiles = discoverPolicyFiles(projectDir);
1995
2151
  for (const file of projectFiles) {
1996
2152
  const hooksBefore = getCustomHooks().length;
@@ -2045,6 +2201,7 @@ var init_custom_hooks_loader = __esm(() => {
2045
2201
  init_hook_logger();
2046
2202
  init_custom_hooks_registry();
2047
2203
  init_loader_utils();
2204
+ init_hooks_config();
2048
2205
  CONVENTION_FILE_RE = /policies\.(js|mjs|ts)$/;
2049
2206
  });
2050
2207
 
@@ -2057,7 +2214,7 @@ import {
2057
2214
  readdirSync as readdirSync2,
2058
2215
  mkdirSync as mkdirSync3,
2059
2216
  existsSync as existsSync4,
2060
- statSync as statSync2,
2217
+ statSync as statSync3,
2061
2218
  unlinkSync
2062
2219
  } from "node:fs";
2063
2220
  import { join as join3 } from "node:path";
@@ -2079,7 +2236,7 @@ function acquireLock() {
2079
2236
  if (e.code !== "EEXIST")
2080
2237
  return;
2081
2238
  try {
2082
- const s = statSync2(lockPath);
2239
+ const s = statSync3(lockPath);
2083
2240
  if (Date.now() - s.mtimeMs > LOCK_STALE_MS) {
2084
2241
  writeFileSync2(lockPath, String(process.pid), "utf-8");
2085
2242
  return;
@@ -2171,7 +2328,7 @@ var init_hook_activity_store = __esm(() => {
2171
2328
  });
2172
2329
 
2173
2330
  // package.json
2174
- var version2 = "0.0.6";
2331
+ var version2 = "0.0.7";
2175
2332
  var init_package = () => {};
2176
2333
 
2177
2334
  // src/posthog-key.ts
@@ -2349,7 +2506,7 @@ import {
2349
2506
  mkdirSync as mkdirSync5,
2350
2507
  existsSync as existsSync6,
2351
2508
  readFileSync as readFileSync4,
2352
- statSync as statSync3,
2509
+ statSync as statSync4,
2353
2510
  renameSync as renameSync4,
2354
2511
  unlinkSync as unlinkSync3,
2355
2512
  readdirSync as readdirSync3,
@@ -2395,7 +2552,7 @@ function appendToServerQueue(entry) {
2395
2552
  return;
2396
2553
  ensureDir2();
2397
2554
  try {
2398
- if (existsSync6(PENDING_FILE) && statSync3(PENDING_FILE).size > MAX_QUEUE_BYTES) {
2555
+ if (existsSync6(PENDING_FILE) && statSync4(PENDING_FILE).size > MAX_QUEUE_BYTES) {
2399
2556
  return;
2400
2557
  }
2401
2558
  } catch {}
@@ -2408,7 +2565,7 @@ function appendToServerQueue(entry) {
2408
2565
  }
2409
2566
  function queueSizeBytes() {
2410
2567
  try {
2411
- return statSync3(PENDING_FILE).size;
2568
+ return statSync4(PENDING_FILE).size;
2412
2569
  } catch {
2413
2570
  return 0;
2414
2571
  }
@@ -2417,7 +2574,7 @@ function claimPendingBatch() {
2417
2574
  if (!existsSync6(PENDING_FILE))
2418
2575
  return null;
2419
2576
  try {
2420
- const size = statSync3(PENDING_FILE).size;
2577
+ const size = statSync4(PENDING_FILE).size;
2421
2578
  if (size === 0)
2422
2579
  return null;
2423
2580
  } catch {
@@ -4484,7 +4641,7 @@ import { realpathSync as realpathSync2 } from "fs";
4484
4641
  import { dirname as dirname7, resolve as resolve8 } from "path";
4485
4642
  import { fileURLToPath as fileURLToPath2 } from "url";
4486
4643
  // package.json
4487
- var version = "0.0.6";
4644
+ var version = "0.0.7";
4488
4645
 
4489
4646
  // bin/failproofai.mjs
4490
4647
  if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs"