failproofai 0.0.6 → 0.0.8

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 (87) 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]__0t3ka1q._.js → [root-of-the-server]__0_rr1ty._.js} +2 -2
  54. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +2 -2
  55. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +2 -2
  56. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ow37ro._.js → [root-of-the-server]__0h3orxc._.js} +2 -2
  57. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0okos0k._.js +2 -2
  58. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +7 -7
  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/{061hxr2b-j.6q.js → 096~b1zwv69ph.js} +1 -1
  70. package/.next/standalone/.next/static/chunks/{0k5t-n0s8p2nr.js → 0eowehbf5egcz.js} +1 -1
  71. package/.next/standalone/.next/static/chunks/{0ubv3x~0zdd_w.js → 0lua3p__elu_..js} +1 -1
  72. package/.next/standalone/.next/static/chunks/{13cot7j99xkb~.js → 0mbc8hyeqe2c4.js} +1 -1
  73. package/.next/standalone/.next/static/chunks/0s_18.dox44e9.js +1 -0
  74. package/.next/standalone/.next/static/chunks/{0-igg2k65fzo_.js → 0t3euwspxi_zg.js} +1 -1
  75. package/.next/standalone/.next/static/chunks/{0lq8ary5l4s8t.js → 151bdxm9n-pry.js} +1 -1
  76. package/.next/standalone/.next/static/chunks/{0v23ca5xty1n~.js → 175-vim0.ztb2.js} +2 -2
  77. package/.next/standalone/package.json +1 -1
  78. package/.next/standalone/server.js +1 -1
  79. package/dist/cli.mjs +204 -62
  80. package/package.json +1 -1
  81. package/src/hooks/builtin-policies.ts +206 -71
  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/chunks/04iuhj_-h-21-.js +0 -1
  85. /package/.next/standalone/.next/static/{my01WPjry7ohRUHyTaYp4 → RYld7TSCDXm2_WhJq20rD}/_buildManifest.js +0 -0
  86. /package/.next/standalone/.next/static/{my01WPjry7ohRUHyTaYp4 → RYld7TSCDXm2_WhJq20rD}/_clientMiddlewareManifest.js +0 -0
  87. /package/.next/standalone/.next/static/{my01WPjry7ohRUHyTaYp4 → RYld7TSCDXm2_WhJq20rD}/_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;
@@ -1027,22 +1074,7 @@ function requirePrBeforeStop(ctx) {
1027
1074
  return allow(`PR #${pr.number} exists: ${pr.url}`);
1028
1075
  }
1029
1076
  if (pr.state === "MERGED") {
1030
- try {
1031
- execFileSync("git", ["fetch", "origin", `+refs/heads/${baseBranch}:refs/remotes/origin/${baseBranch}`], {
1032
- cwd,
1033
- encoding: "utf8",
1034
- stdio: ["pipe", "pipe", "pipe"],
1035
- timeout: 1e4
1036
- });
1037
- const freshAhead = execFileSync("git", ["log", `origin/${baseBranch}..HEAD`, "--oneline"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
1038
- if (!freshAhead) {
1039
- return allow(`PR #${pr.number} was merged; branch is up to date with ${baseBranch}.`);
1040
- }
1041
- const freshDiff = execFileSync("git", ["diff", "--stat", `origin/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
1042
- if (!freshDiff) {
1043
- return allow(`PR #${pr.number} was merged; no file changes vs ${baseBranch}.`);
1044
- }
1045
- } catch {}
1077
+ return allow(`PR #${pr.number} was merged: ${pr.url}. ` + `Switch off this branch (e.g. 'git checkout ${baseBranch} && git pull') before stopping again.`);
1046
1078
  }
1047
1079
  return deny(`Pull request for branch "${branch}" is ${pr.state.toLowerCase()}. Run now: gh pr create`);
1048
1080
  } catch {
@@ -1060,7 +1092,31 @@ function requireNoConflictsBeforeStop(ctx) {
1060
1092
  if (branch === baseBranch) {
1061
1093
  return allow(`On base branch "${baseBranch}", skipping conflict check.`);
1062
1094
  }
1063
- let localSkipped = false;
1095
+ try {
1096
+ execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
1097
+ } catch {
1098
+ return allow("gh CLI not installed, skipping conflict check.");
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("No pull request found for branch, skipping conflict check.");
1110
+ }
1111
+ let pr;
1112
+ try {
1113
+ pr = JSON.parse(prJson);
1114
+ } catch {
1115
+ return allow("Could not parse gh pr view output, skipping conflict check.");
1116
+ }
1117
+ if (pr.state !== "OPEN") {
1118
+ return allow(`PR #${pr.number} is ${pr.state.toLowerCase()}; skipping conflict check.`);
1119
+ }
1064
1120
  try {
1065
1121
  execFileSync("git", ["rev-parse", "--verify", `origin/${baseBranch}`], {
1066
1122
  cwd,
@@ -1069,9 +1125,7 @@ function requireNoConflictsBeforeStop(ctx) {
1069
1125
  timeout: 3000
1070
1126
  });
1071
1127
  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 {
1128
+ if (ahead) {
1075
1129
  execFileSync("git", ["merge-tree", "--write-tree", "--name-only", `origin/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 1e4 });
1076
1130
  }
1077
1131
  } catch (err) {
@@ -1090,32 +1144,6 @@ function requireNoConflictsBeforeStop(ctx) {
1090
1144
  const fileList = files.length ? files.join(", ") : "one or more files";
1091
1145
  return deny(`Branch "${branch}" has merge conflicts with ${baseBranch} in: ${fileList}. ` + `Rebase or merge origin/${baseBranch} now and resolve the conflicts.`);
1092
1146
  }
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
1147
  }
1120
1148
  if (pr.mergeable === "CONFLICTING") {
1121
1149
  return deny(`PR #${pr.number} has merge conflicts per GitHub (${pr.url}). ` + `Rebase or merge origin/${baseBranch} now and resolve the conflicts.`);
@@ -1178,7 +1206,7 @@ function registerBuiltinPolicies(enabledNames) {
1178
1206
  }
1179
1207
  }
1180
1208
  }
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;
1209
+ 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
1210
  var init_builtin_policies = __esm(() => {
1183
1211
  init_hook_logger();
1184
1212
  SHELL_OPERATORS = new Set(["&&", "||", "|", ";"]);
@@ -1251,6 +1279,13 @@ var init_builtin_policies = __esm(() => {
1251
1279
  TMUX_DETACH_RE = /\btmux\s+(?:new-session|new)\b[^|&;]*-d\b/;
1252
1280
  DISOWN_RE = /\bdisown\b/;
1253
1281
  BACKGROUND_AMPERSAND_RE = /(?<![&|])\s?&\s*(?:$|#|;)/;
1282
+ KUBECTL_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*kubectl(?:\s|$)/;
1283
+ TERRAFORM_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*(?:terraform|tofu)(?:\s|$)/;
1284
+ AWS_CLI_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*aws(?:\s|$)/;
1285
+ GCLOUD_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*gcloud(?:\s|$)/;
1286
+ AZ_CLI_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*az(?:\s|$)/;
1287
+ HELM_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*helm(?:\s|$)/;
1288
+ 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
1289
  gitBranchCache = new Map;
1255
1290
  READ_LIKE_CMDS = /(?:^|;|&&|\|\||\|)\s*(?:ls|find|cat|head|tail|less|more|wc|file|stat|tree|du)\s/;
1256
1291
  SEGMENT_SPLIT_RE = /\s*(?:&&|\|\||\||;)\s*/;
@@ -1379,6 +1414,111 @@ var init_builtin_policies = __esm(() => {
1379
1414
  defaultEnabled: true,
1380
1415
  category: "Dangerous Commands"
1381
1416
  },
1417
+ {
1418
+ name: "block-kubectl",
1419
+ description: "Block kubectl commands (Kubernetes cluster mutations)",
1420
+ fn: blockKubectl,
1421
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1422
+ defaultEnabled: false,
1423
+ category: "Infra Commands",
1424
+ params: {
1425
+ allowPatterns: {
1426
+ type: "string[]",
1427
+ description: "kubectl command patterns to allow, matched token-by-token (e.g. 'kubectl get *', 'kubectl describe *')",
1428
+ default: []
1429
+ }
1430
+ }
1431
+ },
1432
+ {
1433
+ name: "block-terraform",
1434
+ description: "Block terraform and tofu (OpenTofu) commands",
1435
+ fn: blockTerraform,
1436
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1437
+ defaultEnabled: false,
1438
+ category: "Infra Commands",
1439
+ params: {
1440
+ allowPatterns: {
1441
+ type: "string[]",
1442
+ description: "terraform/tofu command patterns to allow (e.g. 'terraform plan', 'terraform validate')",
1443
+ default: []
1444
+ }
1445
+ }
1446
+ },
1447
+ {
1448
+ name: "block-aws-cli",
1449
+ description: "Block aws CLI commands",
1450
+ fn: blockAwsCli,
1451
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1452
+ defaultEnabled: false,
1453
+ category: "Infra Commands",
1454
+ params: {
1455
+ allowPatterns: {
1456
+ type: "string[]",
1457
+ description: "aws CLI command patterns to allow (e.g. 'aws s3 ls *', 'aws sts get-caller-identity')",
1458
+ default: []
1459
+ }
1460
+ }
1461
+ },
1462
+ {
1463
+ name: "block-gcloud",
1464
+ description: "Block gcloud (Google Cloud) CLI commands",
1465
+ fn: blockGcloud,
1466
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1467
+ defaultEnabled: false,
1468
+ category: "Infra Commands",
1469
+ params: {
1470
+ allowPatterns: {
1471
+ type: "string[]",
1472
+ description: "gcloud command patterns to allow (e.g. 'gcloud auth list', 'gcloud config list')",
1473
+ default: []
1474
+ }
1475
+ }
1476
+ },
1477
+ {
1478
+ name: "block-az-cli",
1479
+ description: "Block az (Azure) CLI commands",
1480
+ fn: blockAzCli,
1481
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1482
+ defaultEnabled: false,
1483
+ category: "Infra Commands",
1484
+ params: {
1485
+ allowPatterns: {
1486
+ type: "string[]",
1487
+ description: "az CLI command patterns to allow (e.g. 'az account show', 'az group list')",
1488
+ default: []
1489
+ }
1490
+ }
1491
+ },
1492
+ {
1493
+ name: "block-helm",
1494
+ description: "Block helm commands",
1495
+ fn: blockHelm,
1496
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1497
+ defaultEnabled: false,
1498
+ category: "Infra Commands",
1499
+ params: {
1500
+ allowPatterns: {
1501
+ type: "string[]",
1502
+ description: "helm command patterns to allow (e.g. 'helm list', 'helm status *')",
1503
+ default: []
1504
+ }
1505
+ }
1506
+ },
1507
+ {
1508
+ name: "block-gh-pipeline",
1509
+ description: "Block gh CLI pipeline-trigger subcommands (workflow run, run rerun/cancel, pr merge, release create/delete, cache delete, secret set/delete)",
1510
+ fn: blockGhPipeline,
1511
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1512
+ defaultEnabled: false,
1513
+ category: "Infra Commands",
1514
+ params: {
1515
+ allowPatterns: {
1516
+ type: "string[]",
1517
+ 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",
1518
+ default: []
1519
+ }
1520
+ }
1521
+ },
1382
1522
  {
1383
1523
  name: "block-secrets-write",
1384
1524
  description: "Block writing secret key files",
@@ -1981,8 +2121,9 @@ async function loadCustomHooks(customPoliciesPath, opts) {
1981
2121
  async function loadAllCustomHooks(customPoliciesPath, opts) {
1982
2122
  clearCustomHooks();
1983
2123
  const conventionSources = [];
2124
+ const projectRoot = findProjectConfigDir(opts?.sessionCwd ?? process.cwd());
1984
2125
  if (customPoliciesPath) {
1985
- const absPath = isAbsolute(customPoliciesPath) ? customPoliciesPath : resolve4(opts?.sessionCwd ?? process.cwd(), customPoliciesPath);
2126
+ const absPath = isAbsolute(customPoliciesPath) ? customPoliciesPath : resolve4(projectRoot, customPoliciesPath);
1986
2127
  if (existsSync3(absPath)) {
1987
2128
  await loadSingleFile(absPath);
1988
2129
  } else {
@@ -1990,7 +2131,7 @@ async function loadAllCustomHooks(customPoliciesPath, opts) {
1990
2131
  }
1991
2132
  }
1992
2133
  const hooksBeforeConvention = getCustomHooks().length;
1993
- const projectDir = resolve4(opts?.sessionCwd ?? process.cwd(), ".failproofai", "policies");
2134
+ const projectDir = resolve4(projectRoot, ".failproofai", "policies");
1994
2135
  const projectFiles = discoverPolicyFiles(projectDir);
1995
2136
  for (const file of projectFiles) {
1996
2137
  const hooksBefore = getCustomHooks().length;
@@ -2045,6 +2186,7 @@ var init_custom_hooks_loader = __esm(() => {
2045
2186
  init_hook_logger();
2046
2187
  init_custom_hooks_registry();
2047
2188
  init_loader_utils();
2189
+ init_hooks_config();
2048
2190
  CONVENTION_FILE_RE = /policies\.(js|mjs|ts)$/;
2049
2191
  });
2050
2192
 
@@ -2057,7 +2199,7 @@ import {
2057
2199
  readdirSync as readdirSync2,
2058
2200
  mkdirSync as mkdirSync3,
2059
2201
  existsSync as existsSync4,
2060
- statSync as statSync2,
2202
+ statSync as statSync3,
2061
2203
  unlinkSync
2062
2204
  } from "node:fs";
2063
2205
  import { join as join3 } from "node:path";
@@ -2079,7 +2221,7 @@ function acquireLock() {
2079
2221
  if (e.code !== "EEXIST")
2080
2222
  return;
2081
2223
  try {
2082
- const s = statSync2(lockPath);
2224
+ const s = statSync3(lockPath);
2083
2225
  if (Date.now() - s.mtimeMs > LOCK_STALE_MS) {
2084
2226
  writeFileSync2(lockPath, String(process.pid), "utf-8");
2085
2227
  return;
@@ -2171,7 +2313,7 @@ var init_hook_activity_store = __esm(() => {
2171
2313
  });
2172
2314
 
2173
2315
  // package.json
2174
- var version2 = "0.0.6";
2316
+ var version2 = "0.0.8";
2175
2317
  var init_package = () => {};
2176
2318
 
2177
2319
  // src/posthog-key.ts
@@ -2349,7 +2491,7 @@ import {
2349
2491
  mkdirSync as mkdirSync5,
2350
2492
  existsSync as existsSync6,
2351
2493
  readFileSync as readFileSync4,
2352
- statSync as statSync3,
2494
+ statSync as statSync4,
2353
2495
  renameSync as renameSync4,
2354
2496
  unlinkSync as unlinkSync3,
2355
2497
  readdirSync as readdirSync3,
@@ -2395,7 +2537,7 @@ function appendToServerQueue(entry) {
2395
2537
  return;
2396
2538
  ensureDir2();
2397
2539
  try {
2398
- if (existsSync6(PENDING_FILE) && statSync3(PENDING_FILE).size > MAX_QUEUE_BYTES) {
2540
+ if (existsSync6(PENDING_FILE) && statSync4(PENDING_FILE).size > MAX_QUEUE_BYTES) {
2399
2541
  return;
2400
2542
  }
2401
2543
  } catch {}
@@ -2408,7 +2550,7 @@ function appendToServerQueue(entry) {
2408
2550
  }
2409
2551
  function queueSizeBytes() {
2410
2552
  try {
2411
- return statSync3(PENDING_FILE).size;
2553
+ return statSync4(PENDING_FILE).size;
2412
2554
  } catch {
2413
2555
  return 0;
2414
2556
  }
@@ -2417,7 +2559,7 @@ function claimPendingBatch() {
2417
2559
  if (!existsSync6(PENDING_FILE))
2418
2560
  return null;
2419
2561
  try {
2420
- const size = statSync3(PENDING_FILE).size;
2562
+ const size = statSync4(PENDING_FILE).size;
2421
2563
  if (size === 0)
2422
2564
  return null;
2423
2565
  } catch {
@@ -4484,7 +4626,7 @@ import { realpathSync as realpathSync2 } from "fs";
4484
4626
  import { dirname as dirname7, resolve as resolve8 } from "path";
4485
4627
  import { fileURLToPath as fileURLToPath2 } from "url";
4486
4628
  // package.json
4487
- var version = "0.0.6";
4629
+ var version = "0.0.8";
4488
4630
 
4489
4631
  // bin/failproofai.mjs
4490
4632
  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.8",
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"