gentyr 1.3.0

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 (599) hide show
  1. package/.claude/agents/antipattern-hunter.md +176 -0
  2. package/.claude/agents/code-reviewer.md +205 -0
  3. package/.claude/agents/code-writer.md +154 -0
  4. package/.claude/agents/deputy-cto.md +309 -0
  5. package/.claude/agents/feedback-agent.md +101 -0
  6. package/.claude/agents/investigator.md +136 -0
  7. package/.claude/agents/product-manager.md +97 -0
  8. package/.claude/agents/project-manager.md +116 -0
  9. package/.claude/agents/repo-hygiene-expert.md +626 -0
  10. package/.claude/agents/secret-manager.md +324 -0
  11. package/.claude/agents/test-writer.md +354 -0
  12. package/.claude/commands/configure-personas.md +144 -0
  13. package/.claude/commands/cto-report.md +36 -0
  14. package/.claude/commands/demo.md +89 -0
  15. package/.claude/commands/deputy-cto.md +345 -0
  16. package/.claude/commands/hotfix.md +31 -0
  17. package/.claude/commands/overdrive-gentyr.md +167 -0
  18. package/.claude/commands/product-manager.md +32 -0
  19. package/.claude/commands/push-migrations.md +86 -0
  20. package/.claude/commands/push-secrets.md +97 -0
  21. package/.claude/commands/services.json.example +30 -0
  22. package/.claude/commands/setup-gentyr.md +396 -0
  23. package/.claude/commands/show.md +42 -0
  24. package/.claude/commands/spawn-tasks.md +79 -0
  25. package/.claude/commands/toggle-automation-gentyr.md +75 -0
  26. package/.claude/commands/toggle-product-manager.md +19 -0
  27. package/.claude/commands/triage.md +69 -0
  28. package/.claude/hooks/README.md +686 -0
  29. package/.claude/hooks/__tests__/README.md +129 -0
  30. package/.claude/hooks/agent-tracker.js +434 -0
  31. package/.claude/hooks/antipattern-hunter-hook.js +401 -0
  32. package/.claude/hooks/api-key-watcher.js +289 -0
  33. package/.claude/hooks/block-no-verify.js +301 -0
  34. package/.claude/hooks/bypass-approval-hook.js +313 -0
  35. package/.claude/hooks/compliance-checker.js +1309 -0
  36. package/.claude/hooks/config-reader.js +143 -0
  37. package/.claude/hooks/credential-file-guard.js +1139 -0
  38. package/.claude/hooks/credential-health-check.js +168 -0
  39. package/.claude/hooks/credential-sync-hook.js +79 -0
  40. package/.claude/hooks/cto-notification-hook.js +656 -0
  41. package/.claude/hooks/feedback-launcher.js +424 -0
  42. package/.claude/hooks/feedback-orchestrator.js +367 -0
  43. package/.claude/hooks/gentyr-splash.js +47 -0
  44. package/.claude/hooks/gentyr-sync.js +389 -0
  45. package/.claude/hooks/hourly-automation.js +3340 -0
  46. package/.claude/hooks/key-sync.js +899 -0
  47. package/.claude/hooks/lib/approval-utils.js +731 -0
  48. package/.claude/hooks/lib/feature-branch-helper.js +102 -0
  49. package/.claude/hooks/lib/worktree-manager.js +330 -0
  50. package/.claude/hooks/mapping-validator.js +285 -0
  51. package/.claude/hooks/plan-executor.js +398 -0
  52. package/.claude/hooks/playwright-cli-guard.js +104 -0
  53. package/.claude/hooks/playwright-health-check.js +71 -0
  54. package/.claude/hooks/pre-commit-review.js +725 -0
  55. package/.claude/hooks/prompts/local-spec-enforcement.md +310 -0
  56. package/.claude/hooks/prompts/mapping-fix.md +92 -0
  57. package/.claude/hooks/prompts/mapping-review.md +140 -0
  58. package/.claude/hooks/prompts/schema-mapper.md +185 -0
  59. package/.claude/hooks/prompts/spec-enforcement.md +233 -0
  60. package/.claude/hooks/protected-action-approval-hook.js +336 -0
  61. package/.claude/hooks/protected-action-gate.js +562 -0
  62. package/.claude/hooks/protected-actions.json +208 -0
  63. package/.claude/hooks/protected-actions.json.template +122 -0
  64. package/.claude/hooks/quota-monitor.js +490 -0
  65. package/.claude/hooks/reporters/jest-failure-reporter.js +401 -0
  66. package/.claude/hooks/reporters/playwright-failure-reporter.js +446 -0
  67. package/.claude/hooks/reporters/vitest-failure-reporter.js +443 -0
  68. package/.claude/hooks/schema-mapper-hook.js +544 -0
  69. package/.claude/hooks/secret-leak-detector.js +216 -0
  70. package/.claude/hooks/session-reviver.js +514 -0
  71. package/.claude/hooks/slash-command-prefetch.js +1145 -0
  72. package/.claude/hooks/stale-work-detector.js +205 -0
  73. package/.claude/hooks/stop-continue-hook.js +414 -0
  74. package/.claude/hooks/todo-maintenance.js +522 -0
  75. package/.claude/hooks/todo-processing-prompt.md +75 -0
  76. package/.claude/hooks/usage-optimizer.js +791 -0
  77. package/.claude/mcp/README.md +246 -0
  78. package/.claude/settings.json.template +168 -0
  79. package/.mcp.json.template +207 -0
  80. package/CLAUDE.md +340 -0
  81. package/CLAUDE.md.gentyr-section +89 -0
  82. package/LICENSE +21 -0
  83. package/README.md +297 -0
  84. package/cli/commands/init.js +471 -0
  85. package/cli/commands/migrate.js +132 -0
  86. package/cli/commands/protect.js +271 -0
  87. package/cli/commands/scaffold.js +48 -0
  88. package/cli/commands/status.js +133 -0
  89. package/cli/commands/sync.js +101 -0
  90. package/cli/commands/uninstall.js +207 -0
  91. package/cli/index.js +111 -0
  92. package/cli/lib/config-gen.js +214 -0
  93. package/cli/lib/resolve-framework.js +97 -0
  94. package/cli/lib/state.js +140 -0
  95. package/cli/lib/symlinks.js +260 -0
  96. package/docs/AUTOMATION-SYSTEMS.md +484 -0
  97. package/docs/BINARY-PATCHING.md +212 -0
  98. package/docs/CHANGELOG.md +2830 -0
  99. package/docs/CREDENTIAL-DETECTION.md +151 -0
  100. package/docs/CTO-DASHBOARD.md +476 -0
  101. package/docs/DEPLOYMENT-FLOW.md +477 -0
  102. package/docs/DEVELOPER.md +116 -0
  103. package/docs/Executive.md +372 -0
  104. package/docs/SECRET-PATHS.md +77 -0
  105. package/docs/SETUP-GUIDE.md +419 -0
  106. package/docs/STACK.md +109 -0
  107. package/docs/TESTING.md +440 -0
  108. package/docs/assets/claude-logo.svg +3 -0
  109. package/docs/sessions/2026-01-24-spec-suite-implementation.md +190 -0
  110. package/docs/sessions/2026-02-15-feedback-e2e-audit.md +484 -0
  111. package/docs/sessions/2026-02-20-credential-rotation-experiments.md +340 -0
  112. package/docs/sessions/TEST-COVERAGE-REPORT-2026-02-20.md +168 -0
  113. package/docs/shared/EPHEMERAL-STATE-FILES.md +115 -0
  114. package/docs/shared/PROTECTION-SYSTEM.md +341 -0
  115. package/husky/post-commit +10 -0
  116. package/husky/pre-commit +40 -0
  117. package/husky/pre-push +94 -0
  118. package/package.json +43 -0
  119. package/packages/cto-dashboard/package-lock.json +3510 -0
  120. package/packages/cto-dashboard/package.json +41 -0
  121. package/packages/cto-dashboard/pnpm-lock.yaml +2168 -0
  122. package/packages/mcp-servers/dist/__testUtils__/fixtures.d.ts +220 -0
  123. package/packages/mcp-servers/dist/__testUtils__/fixtures.d.ts.map +1 -0
  124. package/packages/mcp-servers/dist/__testUtils__/fixtures.js +376 -0
  125. package/packages/mcp-servers/dist/__testUtils__/fixtures.js.map +1 -0
  126. package/packages/mcp-servers/dist/__testUtils__/index.d.ts +121 -0
  127. package/packages/mcp-servers/dist/__testUtils__/index.d.ts.map +1 -0
  128. package/packages/mcp-servers/dist/__testUtils__/index.js +180 -0
  129. package/packages/mcp-servers/dist/__testUtils__/index.js.map +1 -0
  130. package/packages/mcp-servers/dist/__testUtils__/schemas.d.ts +84 -0
  131. package/packages/mcp-servers/dist/__testUtils__/schemas.d.ts.map +1 -0
  132. package/packages/mcp-servers/dist/__testUtils__/schemas.js +309 -0
  133. package/packages/mcp-servers/dist/__testUtils__/schemas.js.map +1 -0
  134. package/packages/mcp-servers/dist/agent-reports/index.d.ts +7 -0
  135. package/packages/mcp-servers/dist/agent-reports/index.d.ts.map +1 -0
  136. package/packages/mcp-servers/dist/agent-reports/index.js +8 -0
  137. package/packages/mcp-servers/dist/agent-reports/index.js.map +1 -0
  138. package/packages/mcp-servers/dist/agent-reports/server.d.ts +22 -0
  139. package/packages/mcp-servers/dist/agent-reports/server.d.ts.map +1 -0
  140. package/packages/mcp-servers/dist/agent-reports/server.js +535 -0
  141. package/packages/mcp-servers/dist/agent-reports/server.js.map +1 -0
  142. package/packages/mcp-servers/dist/agent-reports/types.d.ts +258 -0
  143. package/packages/mcp-servers/dist/agent-reports/types.d.ts.map +1 -0
  144. package/packages/mcp-servers/dist/agent-reports/types.js +81 -0
  145. package/packages/mcp-servers/dist/agent-reports/types.js.map +1 -0
  146. package/packages/mcp-servers/dist/agent-tracker/index.d.ts +5 -0
  147. package/packages/mcp-servers/dist/agent-tracker/index.d.ts.map +1 -0
  148. package/packages/mcp-servers/dist/agent-tracker/index.js +5 -0
  149. package/packages/mcp-servers/dist/agent-tracker/index.js.map +1 -0
  150. package/packages/mcp-servers/dist/agent-tracker/server.d.ts +12 -0
  151. package/packages/mcp-servers/dist/agent-tracker/server.d.ts.map +1 -0
  152. package/packages/mcp-servers/dist/agent-tracker/server.js +919 -0
  153. package/packages/mcp-servers/dist/agent-tracker/server.js.map +1 -0
  154. package/packages/mcp-servers/dist/agent-tracker/types.d.ts +328 -0
  155. package/packages/mcp-servers/dist/agent-tracker/types.d.ts.map +1 -0
  156. package/packages/mcp-servers/dist/agent-tracker/types.js +128 -0
  157. package/packages/mcp-servers/dist/agent-tracker/types.js.map +1 -0
  158. package/packages/mcp-servers/dist/chrome-bridge/browser-tips.d.ts +27 -0
  159. package/packages/mcp-servers/dist/chrome-bridge/browser-tips.d.ts.map +1 -0
  160. package/packages/mcp-servers/dist/chrome-bridge/browser-tips.js +167 -0
  161. package/packages/mcp-servers/dist/chrome-bridge/browser-tips.js.map +1 -0
  162. package/packages/mcp-servers/dist/chrome-bridge/index.d.ts +6 -0
  163. package/packages/mcp-servers/dist/chrome-bridge/index.d.ts.map +1 -0
  164. package/packages/mcp-servers/dist/chrome-bridge/index.js +6 -0
  165. package/packages/mcp-servers/dist/chrome-bridge/index.js.map +1 -0
  166. package/packages/mcp-servers/dist/chrome-bridge/server.d.ts +13 -0
  167. package/packages/mcp-servers/dist/chrome-bridge/server.d.ts.map +1 -0
  168. package/packages/mcp-servers/dist/chrome-bridge/server.js +959 -0
  169. package/packages/mcp-servers/dist/chrome-bridge/server.js.map +1 -0
  170. package/packages/mcp-servers/dist/chrome-bridge/types.d.ts +41 -0
  171. package/packages/mcp-servers/dist/chrome-bridge/types.d.ts.map +1 -0
  172. package/packages/mcp-servers/dist/chrome-bridge/types.js +8 -0
  173. package/packages/mcp-servers/dist/chrome-bridge/types.js.map +1 -0
  174. package/packages/mcp-servers/dist/cloudflare/index.d.ts +8 -0
  175. package/packages/mcp-servers/dist/cloudflare/index.d.ts.map +1 -0
  176. package/packages/mcp-servers/dist/cloudflare/index.js +8 -0
  177. package/packages/mcp-servers/dist/cloudflare/index.js.map +1 -0
  178. package/packages/mcp-servers/dist/cloudflare/server.d.ts +16 -0
  179. package/packages/mcp-servers/dist/cloudflare/server.d.ts.map +1 -0
  180. package/packages/mcp-servers/dist/cloudflare/server.js +253 -0
  181. package/packages/mcp-servers/dist/cloudflare/server.js.map +1 -0
  182. package/packages/mcp-servers/dist/cloudflare/types.d.ts +141 -0
  183. package/packages/mcp-servers/dist/cloudflare/types.d.ts.map +1 -0
  184. package/packages/mcp-servers/dist/cloudflare/types.js +53 -0
  185. package/packages/mcp-servers/dist/cloudflare/types.js.map +1 -0
  186. package/packages/mcp-servers/dist/codecov/index.d.ts +7 -0
  187. package/packages/mcp-servers/dist/codecov/index.d.ts.map +1 -0
  188. package/packages/mcp-servers/dist/codecov/index.js +7 -0
  189. package/packages/mcp-servers/dist/codecov/index.js.map +1 -0
  190. package/packages/mcp-servers/dist/codecov/server.d.ts +21 -0
  191. package/packages/mcp-servers/dist/codecov/server.d.ts.map +1 -0
  192. package/packages/mcp-servers/dist/codecov/server.js +376 -0
  193. package/packages/mcp-servers/dist/codecov/server.js.map +1 -0
  194. package/packages/mcp-servers/dist/codecov/types.d.ts +269 -0
  195. package/packages/mcp-servers/dist/codecov/types.d.ts.map +1 -0
  196. package/packages/mcp-servers/dist/codecov/types.js +128 -0
  197. package/packages/mcp-servers/dist/codecov/types.js.map +1 -0
  198. package/packages/mcp-servers/dist/cto-report/index.d.ts +9 -0
  199. package/packages/mcp-servers/dist/cto-report/index.d.ts.map +1 -0
  200. package/packages/mcp-servers/dist/cto-report/index.js +9 -0
  201. package/packages/mcp-servers/dist/cto-report/index.js.map +1 -0
  202. package/packages/mcp-servers/dist/cto-report/server.d.ts +14 -0
  203. package/packages/mcp-servers/dist/cto-report/server.d.ts.map +1 -0
  204. package/packages/mcp-servers/dist/cto-report/server.js +859 -0
  205. package/packages/mcp-servers/dist/cto-report/server.js.map +1 -0
  206. package/packages/mcp-servers/dist/cto-report/types.d.ts +213 -0
  207. package/packages/mcp-servers/dist/cto-report/types.d.ts.map +1 -0
  208. package/packages/mcp-servers/dist/cto-report/types.js +29 -0
  209. package/packages/mcp-servers/dist/cto-report/types.js.map +1 -0
  210. package/packages/mcp-servers/dist/cto-reports/index.d.ts +7 -0
  211. package/packages/mcp-servers/dist/cto-reports/index.d.ts.map +1 -0
  212. package/packages/mcp-servers/dist/cto-reports/index.js +8 -0
  213. package/packages/mcp-servers/dist/cto-reports/index.js.map +1 -0
  214. package/packages/mcp-servers/dist/cto-reports/server.d.ts +20 -0
  215. package/packages/mcp-servers/dist/cto-reports/server.d.ts.map +1 -0
  216. package/packages/mcp-servers/dist/cto-reports/server.js +538 -0
  217. package/packages/mcp-servers/dist/cto-reports/server.js.map +1 -0
  218. package/packages/mcp-servers/dist/cto-reports/types.d.ts +236 -0
  219. package/packages/mcp-servers/dist/cto-reports/types.d.ts.map +1 -0
  220. package/packages/mcp-servers/dist/cto-reports/types.js +77 -0
  221. package/packages/mcp-servers/dist/cto-reports/types.js.map +1 -0
  222. package/packages/mcp-servers/dist/deputy-cto/index.d.ts +7 -0
  223. package/packages/mcp-servers/dist/deputy-cto/index.d.ts.map +1 -0
  224. package/packages/mcp-servers/dist/deputy-cto/index.js +8 -0
  225. package/packages/mcp-servers/dist/deputy-cto/index.js.map +1 -0
  226. package/packages/mcp-servers/dist/deputy-cto/server.d.ts +23 -0
  227. package/packages/mcp-servers/dist/deputy-cto/server.d.ts.map +1 -0
  228. package/packages/mcp-servers/dist/deputy-cto/server.js +1700 -0
  229. package/packages/mcp-servers/dist/deputy-cto/server.js.map +1 -0
  230. package/packages/mcp-servers/dist/deputy-cto/types.d.ts +439 -0
  231. package/packages/mcp-servers/dist/deputy-cto/types.d.ts.map +1 -0
  232. package/packages/mcp-servers/dist/deputy-cto/types.js +102 -0
  233. package/packages/mcp-servers/dist/deputy-cto/types.js.map +1 -0
  234. package/packages/mcp-servers/dist/elastic-logs/index.d.ts +5 -0
  235. package/packages/mcp-servers/dist/elastic-logs/index.d.ts.map +1 -0
  236. package/packages/mcp-servers/dist/elastic-logs/index.js +5 -0
  237. package/packages/mcp-servers/dist/elastic-logs/index.js.map +1 -0
  238. package/packages/mcp-servers/dist/elastic-logs/server.d.ts +18 -0
  239. package/packages/mcp-servers/dist/elastic-logs/server.d.ts.map +1 -0
  240. package/packages/mcp-servers/dist/elastic-logs/server.js +259 -0
  241. package/packages/mcp-servers/dist/elastic-logs/server.js.map +1 -0
  242. package/packages/mcp-servers/dist/elastic-logs/types.d.ts +107 -0
  243. package/packages/mcp-servers/dist/elastic-logs/types.d.ts.map +1 -0
  244. package/packages/mcp-servers/dist/elastic-logs/types.js +31 -0
  245. package/packages/mcp-servers/dist/elastic-logs/types.js.map +1 -0
  246. package/packages/mcp-servers/dist/feedback-explorer/index.d.ts +2 -0
  247. package/packages/mcp-servers/dist/feedback-explorer/index.d.ts.map +1 -0
  248. package/packages/mcp-servers/dist/feedback-explorer/index.js +2 -0
  249. package/packages/mcp-servers/dist/feedback-explorer/index.js.map +1 -0
  250. package/packages/mcp-servers/dist/feedback-explorer/server.d.ts +21 -0
  251. package/packages/mcp-servers/dist/feedback-explorer/server.d.ts.map +1 -0
  252. package/packages/mcp-servers/dist/feedback-explorer/server.js +580 -0
  253. package/packages/mcp-servers/dist/feedback-explorer/server.js.map +1 -0
  254. package/packages/mcp-servers/dist/feedback-explorer/types.d.ts +331 -0
  255. package/packages/mcp-servers/dist/feedback-explorer/types.d.ts.map +1 -0
  256. package/packages/mcp-servers/dist/feedback-explorer/types.js +40 -0
  257. package/packages/mcp-servers/dist/feedback-explorer/types.js.map +1 -0
  258. package/packages/mcp-servers/dist/feedback-reporter/index.d.ts +9 -0
  259. package/packages/mcp-servers/dist/feedback-reporter/index.d.ts.map +1 -0
  260. package/packages/mcp-servers/dist/feedback-reporter/index.js +9 -0
  261. package/packages/mcp-servers/dist/feedback-reporter/index.js.map +1 -0
  262. package/packages/mcp-servers/dist/feedback-reporter/server.d.ts +36 -0
  263. package/packages/mcp-servers/dist/feedback-reporter/server.d.ts.map +1 -0
  264. package/packages/mcp-servers/dist/feedback-reporter/server.js +392 -0
  265. package/packages/mcp-servers/dist/feedback-reporter/server.js.map +1 -0
  266. package/packages/mcp-servers/dist/feedback-reporter/types.d.ts +152 -0
  267. package/packages/mcp-servers/dist/feedback-reporter/types.d.ts.map +1 -0
  268. package/packages/mcp-servers/dist/feedback-reporter/types.js +67 -0
  269. package/packages/mcp-servers/dist/feedback-reporter/types.js.map +1 -0
  270. package/packages/mcp-servers/dist/github/index.d.ts +7 -0
  271. package/packages/mcp-servers/dist/github/index.d.ts.map +1 -0
  272. package/packages/mcp-servers/dist/github/index.js +7 -0
  273. package/packages/mcp-servers/dist/github/index.js.map +1 -0
  274. package/packages/mcp-servers/dist/github/server.d.ts +15 -0
  275. package/packages/mcp-servers/dist/github/server.d.ts.map +1 -0
  276. package/packages/mcp-servers/dist/github/server.js +686 -0
  277. package/packages/mcp-servers/dist/github/server.js.map +1 -0
  278. package/packages/mcp-servers/dist/github/types.d.ts +660 -0
  279. package/packages/mcp-servers/dist/github/types.d.ts.map +1 -0
  280. package/packages/mcp-servers/dist/github/types.js +209 -0
  281. package/packages/mcp-servers/dist/github/types.js.map +1 -0
  282. package/packages/mcp-servers/dist/index.d.ts +30 -0
  283. package/packages/mcp-servers/dist/index.d.ts.map +1 -0
  284. package/packages/mcp-servers/dist/index.js +32 -0
  285. package/packages/mcp-servers/dist/index.js.map +1 -0
  286. package/packages/mcp-servers/dist/makerkit-docs/index.d.ts +5 -0
  287. package/packages/mcp-servers/dist/makerkit-docs/index.d.ts.map +1 -0
  288. package/packages/mcp-servers/dist/makerkit-docs/index.js +5 -0
  289. package/packages/mcp-servers/dist/makerkit-docs/index.js.map +1 -0
  290. package/packages/mcp-servers/dist/makerkit-docs/server.d.ts +15 -0
  291. package/packages/mcp-servers/dist/makerkit-docs/server.d.ts.map +1 -0
  292. package/packages/mcp-servers/dist/makerkit-docs/server.js +252 -0
  293. package/packages/mcp-servers/dist/makerkit-docs/server.js.map +1 -0
  294. package/packages/mcp-servers/dist/makerkit-docs/types.d.ts +74 -0
  295. package/packages/mcp-servers/dist/makerkit-docs/types.d.ts.map +1 -0
  296. package/packages/mcp-servers/dist/makerkit-docs/types.js +20 -0
  297. package/packages/mcp-servers/dist/makerkit-docs/types.js.map +1 -0
  298. package/packages/mcp-servers/dist/onepassword/index.d.ts +2 -0
  299. package/packages/mcp-servers/dist/onepassword/index.d.ts.map +1 -0
  300. package/packages/mcp-servers/dist/onepassword/index.js +2 -0
  301. package/packages/mcp-servers/dist/onepassword/index.js.map +1 -0
  302. package/packages/mcp-servers/dist/onepassword/server.d.ts +2 -0
  303. package/packages/mcp-servers/dist/onepassword/server.d.ts.map +1 -0
  304. package/packages/mcp-servers/dist/onepassword/server.js +159 -0
  305. package/packages/mcp-servers/dist/onepassword/server.js.map +1 -0
  306. package/packages/mcp-servers/dist/onepassword/types.d.ts +55 -0
  307. package/packages/mcp-servers/dist/onepassword/types.d.ts.map +1 -0
  308. package/packages/mcp-servers/dist/onepassword/types.js +22 -0
  309. package/packages/mcp-servers/dist/onepassword/types.js.map +1 -0
  310. package/packages/mcp-servers/dist/playwright/helpers.d.ts +20 -0
  311. package/packages/mcp-servers/dist/playwright/helpers.d.ts.map +1 -0
  312. package/packages/mcp-servers/dist/playwright/helpers.js +31 -0
  313. package/packages/mcp-servers/dist/playwright/helpers.js.map +1 -0
  314. package/packages/mcp-servers/dist/playwright/index.d.ts +5 -0
  315. package/packages/mcp-servers/dist/playwright/index.d.ts.map +1 -0
  316. package/packages/mcp-servers/dist/playwright/index.js +5 -0
  317. package/packages/mcp-servers/dist/playwright/index.js.map +1 -0
  318. package/packages/mcp-servers/dist/playwright/server.d.ts +13 -0
  319. package/packages/mcp-servers/dist/playwright/server.d.ts.map +1 -0
  320. package/packages/mcp-servers/dist/playwright/server.js +1201 -0
  321. package/packages/mcp-servers/dist/playwright/server.js.map +1 -0
  322. package/packages/mcp-servers/dist/playwright/types.d.ts +216 -0
  323. package/packages/mcp-servers/dist/playwright/types.d.ts.map +1 -0
  324. package/packages/mcp-servers/dist/playwright/types.js +172 -0
  325. package/packages/mcp-servers/dist/playwright/types.js.map +1 -0
  326. package/packages/mcp-servers/dist/playwright-feedback/browser-manager.d.ts +39 -0
  327. package/packages/mcp-servers/dist/playwright-feedback/browser-manager.d.ts.map +1 -0
  328. package/packages/mcp-servers/dist/playwright-feedback/browser-manager.js +71 -0
  329. package/packages/mcp-servers/dist/playwright-feedback/browser-manager.js.map +1 -0
  330. package/packages/mcp-servers/dist/playwright-feedback/index.d.ts +5 -0
  331. package/packages/mcp-servers/dist/playwright-feedback/index.d.ts.map +1 -0
  332. package/packages/mcp-servers/dist/playwright-feedback/index.js +5 -0
  333. package/packages/mcp-servers/dist/playwright-feedback/index.js.map +1 -0
  334. package/packages/mcp-servers/dist/playwright-feedback/server.d.ts +34 -0
  335. package/packages/mcp-servers/dist/playwright-feedback/server.d.ts.map +1 -0
  336. package/packages/mcp-servers/dist/playwright-feedback/server.js +538 -0
  337. package/packages/mcp-servers/dist/playwright-feedback/server.js.map +1 -0
  338. package/packages/mcp-servers/dist/playwright-feedback/types.d.ts +305 -0
  339. package/packages/mcp-servers/dist/playwright-feedback/types.d.ts.map +1 -0
  340. package/packages/mcp-servers/dist/playwright-feedback/types.js +123 -0
  341. package/packages/mcp-servers/dist/playwright-feedback/types.js.map +1 -0
  342. package/packages/mcp-servers/dist/product-manager/server.d.ts +17 -0
  343. package/packages/mcp-servers/dist/product-manager/server.d.ts.map +1 -0
  344. package/packages/mcp-servers/dist/product-manager/server.js +690 -0
  345. package/packages/mcp-servers/dist/product-manager/server.js.map +1 -0
  346. package/packages/mcp-servers/dist/product-manager/types.d.ts +286 -0
  347. package/packages/mcp-servers/dist/product-manager/types.d.ts.map +1 -0
  348. package/packages/mcp-servers/dist/product-manager/types.js +99 -0
  349. package/packages/mcp-servers/dist/product-manager/types.js.map +1 -0
  350. package/packages/mcp-servers/dist/programmatic-feedback/index.d.ts +7 -0
  351. package/packages/mcp-servers/dist/programmatic-feedback/index.d.ts.map +1 -0
  352. package/packages/mcp-servers/dist/programmatic-feedback/index.js +7 -0
  353. package/packages/mcp-servers/dist/programmatic-feedback/index.js.map +1 -0
  354. package/packages/mcp-servers/dist/programmatic-feedback/sandbox.d.ts +19 -0
  355. package/packages/mcp-servers/dist/programmatic-feedback/sandbox.d.ts.map +1 -0
  356. package/packages/mcp-servers/dist/programmatic-feedback/sandbox.js +174 -0
  357. package/packages/mcp-servers/dist/programmatic-feedback/sandbox.js.map +1 -0
  358. package/packages/mcp-servers/dist/programmatic-feedback/server.d.ts +35 -0
  359. package/packages/mcp-servers/dist/programmatic-feedback/server.d.ts.map +1 -0
  360. package/packages/mcp-servers/dist/programmatic-feedback/server.js +465 -0
  361. package/packages/mcp-servers/dist/programmatic-feedback/server.js.map +1 -0
  362. package/packages/mcp-servers/dist/programmatic-feedback/types.d.ts +127 -0
  363. package/packages/mcp-servers/dist/programmatic-feedback/types.d.ts.map +1 -0
  364. package/packages/mcp-servers/dist/programmatic-feedback/types.js +80 -0
  365. package/packages/mcp-servers/dist/programmatic-feedback/types.js.map +1 -0
  366. package/packages/mcp-servers/dist/render/index.d.ts +8 -0
  367. package/packages/mcp-servers/dist/render/index.d.ts.map +1 -0
  368. package/packages/mcp-servers/dist/render/index.js +8 -0
  369. package/packages/mcp-servers/dist/render/index.js.map +1 -0
  370. package/packages/mcp-servers/dist/render/server.d.ts +15 -0
  371. package/packages/mcp-servers/dist/render/server.d.ts.map +1 -0
  372. package/packages/mcp-servers/dist/render/server.js +428 -0
  373. package/packages/mcp-servers/dist/render/server.js.map +1 -0
  374. package/packages/mcp-servers/dist/render/types.d.ts +273 -0
  375. package/packages/mcp-servers/dist/render/types.d.ts.map +1 -0
  376. package/packages/mcp-servers/dist/render/types.js +102 -0
  377. package/packages/mcp-servers/dist/render/types.js.map +1 -0
  378. package/packages/mcp-servers/dist/resend/index.d.ts +7 -0
  379. package/packages/mcp-servers/dist/resend/index.d.ts.map +1 -0
  380. package/packages/mcp-servers/dist/resend/index.js +7 -0
  381. package/packages/mcp-servers/dist/resend/index.js.map +1 -0
  382. package/packages/mcp-servers/dist/resend/server.d.ts +15 -0
  383. package/packages/mcp-servers/dist/resend/server.d.ts.map +1 -0
  384. package/packages/mcp-servers/dist/resend/server.js +298 -0
  385. package/packages/mcp-servers/dist/resend/server.js.map +1 -0
  386. package/packages/mcp-servers/dist/resend/types.d.ts +222 -0
  387. package/packages/mcp-servers/dist/resend/types.d.ts.map +1 -0
  388. package/packages/mcp-servers/dist/resend/types.js +58 -0
  389. package/packages/mcp-servers/dist/resend/types.js.map +1 -0
  390. package/packages/mcp-servers/dist/review-queue/index.d.ts +6 -0
  391. package/packages/mcp-servers/dist/review-queue/index.d.ts.map +1 -0
  392. package/packages/mcp-servers/dist/review-queue/index.js +6 -0
  393. package/packages/mcp-servers/dist/review-queue/index.js.map +1 -0
  394. package/packages/mcp-servers/dist/review-queue/server.d.ts +17 -0
  395. package/packages/mcp-servers/dist/review-queue/server.d.ts.map +1 -0
  396. package/packages/mcp-servers/dist/review-queue/server.js +348 -0
  397. package/packages/mcp-servers/dist/review-queue/server.js.map +1 -0
  398. package/packages/mcp-servers/dist/review-queue/types.d.ts +162 -0
  399. package/packages/mcp-servers/dist/review-queue/types.d.ts.map +1 -0
  400. package/packages/mcp-servers/dist/review-queue/types.js +56 -0
  401. package/packages/mcp-servers/dist/review-queue/types.js.map +1 -0
  402. package/packages/mcp-servers/dist/secret-sync/server.d.ts +19 -0
  403. package/packages/mcp-servers/dist/secret-sync/server.d.ts.map +1 -0
  404. package/packages/mcp-servers/dist/secret-sync/server.js +1139 -0
  405. package/packages/mcp-servers/dist/secret-sync/server.js.map +1 -0
  406. package/packages/mcp-servers/dist/secret-sync/types.d.ts +442 -0
  407. package/packages/mcp-servers/dist/secret-sync/types.d.ts.map +1 -0
  408. package/packages/mcp-servers/dist/secret-sync/types.js +113 -0
  409. package/packages/mcp-servers/dist/secret-sync/types.js.map +1 -0
  410. package/packages/mcp-servers/dist/session-events/index.d.ts +5 -0
  411. package/packages/mcp-servers/dist/session-events/index.d.ts.map +1 -0
  412. package/packages/mcp-servers/dist/session-events/index.js +5 -0
  413. package/packages/mcp-servers/dist/session-events/index.js.map +1 -0
  414. package/packages/mcp-servers/dist/session-events/server.d.ts +11 -0
  415. package/packages/mcp-servers/dist/session-events/server.d.ts.map +1 -0
  416. package/packages/mcp-servers/dist/session-events/server.js +290 -0
  417. package/packages/mcp-servers/dist/session-events/server.js.map +1 -0
  418. package/packages/mcp-servers/dist/session-events/types.d.ts +213 -0
  419. package/packages/mcp-servers/dist/session-events/types.d.ts.map +1 -0
  420. package/packages/mcp-servers/dist/session-events/types.js +69 -0
  421. package/packages/mcp-servers/dist/session-events/types.js.map +1 -0
  422. package/packages/mcp-servers/dist/session-restart/index.d.ts +9 -0
  423. package/packages/mcp-servers/dist/session-restart/index.d.ts.map +1 -0
  424. package/packages/mcp-servers/dist/session-restart/index.js +9 -0
  425. package/packages/mcp-servers/dist/session-restart/index.js.map +1 -0
  426. package/packages/mcp-servers/dist/session-restart/server.d.ts +20 -0
  427. package/packages/mcp-servers/dist/session-restart/server.d.ts.map +1 -0
  428. package/packages/mcp-servers/dist/session-restart/server.js +411 -0
  429. package/packages/mcp-servers/dist/session-restart/server.js.map +1 -0
  430. package/packages/mcp-servers/dist/session-restart/types.d.ts +26 -0
  431. package/packages/mcp-servers/dist/session-restart/types.d.ts.map +1 -0
  432. package/packages/mcp-servers/dist/session-restart/types.js +16 -0
  433. package/packages/mcp-servers/dist/session-restart/types.js.map +1 -0
  434. package/packages/mcp-servers/dist/setup-helper/index.d.ts +5 -0
  435. package/packages/mcp-servers/dist/setup-helper/index.d.ts.map +1 -0
  436. package/packages/mcp-servers/dist/setup-helper/index.js +5 -0
  437. package/packages/mcp-servers/dist/setup-helper/index.js.map +1 -0
  438. package/packages/mcp-servers/dist/setup-helper/server.d.ts +14 -0
  439. package/packages/mcp-servers/dist/setup-helper/server.d.ts.map +1 -0
  440. package/packages/mcp-servers/dist/setup-helper/server.js +454 -0
  441. package/packages/mcp-servers/dist/setup-helper/server.js.map +1 -0
  442. package/packages/mcp-servers/dist/setup-helper/types.d.ts +81 -0
  443. package/packages/mcp-servers/dist/setup-helper/types.d.ts.map +1 -0
  444. package/packages/mcp-servers/dist/setup-helper/types.js +41 -0
  445. package/packages/mcp-servers/dist/setup-helper/types.js.map +1 -0
  446. package/packages/mcp-servers/dist/shared/audited-server.d.ts +31 -0
  447. package/packages/mcp-servers/dist/shared/audited-server.d.ts.map +1 -0
  448. package/packages/mcp-servers/dist/shared/audited-server.js +126 -0
  449. package/packages/mcp-servers/dist/shared/audited-server.js.map +1 -0
  450. package/packages/mcp-servers/dist/shared/constants.d.ts +26 -0
  451. package/packages/mcp-servers/dist/shared/constants.d.ts.map +1 -0
  452. package/packages/mcp-servers/dist/shared/constants.js +41 -0
  453. package/packages/mcp-servers/dist/shared/constants.js.map +1 -0
  454. package/packages/mcp-servers/dist/shared/index.d.ts +6 -0
  455. package/packages/mcp-servers/dist/shared/index.d.ts.map +1 -0
  456. package/packages/mcp-servers/dist/shared/index.js +6 -0
  457. package/packages/mcp-servers/dist/shared/index.js.map +1 -0
  458. package/packages/mcp-servers/dist/shared/readonly-db.d.ts +11 -0
  459. package/packages/mcp-servers/dist/shared/readonly-db.d.ts.map +1 -0
  460. package/packages/mcp-servers/dist/shared/readonly-db.js +47 -0
  461. package/packages/mcp-servers/dist/shared/readonly-db.js.map +1 -0
  462. package/packages/mcp-servers/dist/shared/resolve-framework.d.ts +20 -0
  463. package/packages/mcp-servers/dist/shared/resolve-framework.d.ts.map +1 -0
  464. package/packages/mcp-servers/dist/shared/resolve-framework.js +65 -0
  465. package/packages/mcp-servers/dist/shared/resolve-framework.js.map +1 -0
  466. package/packages/mcp-servers/dist/shared/server.d.ts +86 -0
  467. package/packages/mcp-servers/dist/shared/server.d.ts.map +1 -0
  468. package/packages/mcp-servers/dist/shared/server.js +291 -0
  469. package/packages/mcp-servers/dist/shared/server.js.map +1 -0
  470. package/packages/mcp-servers/dist/shared/types.d.ts +113 -0
  471. package/packages/mcp-servers/dist/shared/types.d.ts.map +1 -0
  472. package/packages/mcp-servers/dist/shared/types.js +36 -0
  473. package/packages/mcp-servers/dist/shared/types.js.map +1 -0
  474. package/packages/mcp-servers/dist/show/server.d.ts +12 -0
  475. package/packages/mcp-servers/dist/show/server.d.ts.map +1 -0
  476. package/packages/mcp-servers/dist/show/server.js +97 -0
  477. package/packages/mcp-servers/dist/show/server.js.map +1 -0
  478. package/packages/mcp-servers/dist/show/types.d.ts +19 -0
  479. package/packages/mcp-servers/dist/show/types.d.ts.map +1 -0
  480. package/packages/mcp-servers/dist/show/types.js +32 -0
  481. package/packages/mcp-servers/dist/show/types.js.map +1 -0
  482. package/packages/mcp-servers/dist/specs-browser/index.d.ts +5 -0
  483. package/packages/mcp-servers/dist/specs-browser/index.d.ts.map +1 -0
  484. package/packages/mcp-servers/dist/specs-browser/index.js +5 -0
  485. package/packages/mcp-servers/dist/specs-browser/index.js.map +1 -0
  486. package/packages/mcp-servers/dist/specs-browser/server.d.ts +13 -0
  487. package/packages/mcp-servers/dist/specs-browser/server.d.ts.map +1 -0
  488. package/packages/mcp-servers/dist/specs-browser/server.js +692 -0
  489. package/packages/mcp-servers/dist/specs-browser/server.js.map +1 -0
  490. package/packages/mcp-servers/dist/specs-browser/types.d.ts +337 -0
  491. package/packages/mcp-servers/dist/specs-browser/types.d.ts.map +1 -0
  492. package/packages/mcp-servers/dist/specs-browser/types.js +134 -0
  493. package/packages/mcp-servers/dist/specs-browser/types.js.map +1 -0
  494. package/packages/mcp-servers/dist/supabase/index.d.ts +10 -0
  495. package/packages/mcp-servers/dist/supabase/index.d.ts.map +1 -0
  496. package/packages/mcp-servers/dist/supabase/index.js +10 -0
  497. package/packages/mcp-servers/dist/supabase/index.js.map +1 -0
  498. package/packages/mcp-servers/dist/supabase/server.d.ts +20 -0
  499. package/packages/mcp-servers/dist/supabase/server.d.ts.map +1 -0
  500. package/packages/mcp-servers/dist/supabase/server.js +451 -0
  501. package/packages/mcp-servers/dist/supabase/server.js.map +1 -0
  502. package/packages/mcp-servers/dist/supabase/types.d.ts +196 -0
  503. package/packages/mcp-servers/dist/supabase/types.d.ts.map +1 -0
  504. package/packages/mcp-servers/dist/supabase/types.js +76 -0
  505. package/packages/mcp-servers/dist/supabase/types.js.map +1 -0
  506. package/packages/mcp-servers/dist/todo-db/index.d.ts +5 -0
  507. package/packages/mcp-servers/dist/todo-db/index.d.ts.map +1 -0
  508. package/packages/mcp-servers/dist/todo-db/index.js +5 -0
  509. package/packages/mcp-servers/dist/todo-db/index.js.map +1 -0
  510. package/packages/mcp-servers/dist/todo-db/server.d.ts +13 -0
  511. package/packages/mcp-servers/dist/todo-db/server.d.ts.map +1 -0
  512. package/packages/mcp-servers/dist/todo-db/server.js +649 -0
  513. package/packages/mcp-servers/dist/todo-db/server.js.map +1 -0
  514. package/packages/mcp-servers/dist/todo-db/types.d.ts +225 -0
  515. package/packages/mcp-servers/dist/todo-db/types.d.ts.map +1 -0
  516. package/packages/mcp-servers/dist/todo-db/types.js +69 -0
  517. package/packages/mcp-servers/dist/todo-db/types.js.map +1 -0
  518. package/packages/mcp-servers/dist/user-feedback/index.d.ts +7 -0
  519. package/packages/mcp-servers/dist/user-feedback/index.d.ts.map +1 -0
  520. package/packages/mcp-servers/dist/user-feedback/index.js +8 -0
  521. package/packages/mcp-servers/dist/user-feedback/index.js.map +1 -0
  522. package/packages/mcp-servers/dist/user-feedback/server.d.ts +25 -0
  523. package/packages/mcp-servers/dist/user-feedback/server.d.ts.map +1 -0
  524. package/packages/mcp-servers/dist/user-feedback/server.js +914 -0
  525. package/packages/mcp-servers/dist/user-feedback/server.js.map +1 -0
  526. package/packages/mcp-servers/dist/user-feedback/types.d.ts +415 -0
  527. package/packages/mcp-servers/dist/user-feedback/types.d.ts.map +1 -0
  528. package/packages/mcp-servers/dist/user-feedback/types.js +132 -0
  529. package/packages/mcp-servers/dist/user-feedback/types.js.map +1 -0
  530. package/packages/mcp-servers/dist/vercel/index.d.ts +9 -0
  531. package/packages/mcp-servers/dist/vercel/index.d.ts.map +1 -0
  532. package/packages/mcp-servers/dist/vercel/index.js +9 -0
  533. package/packages/mcp-servers/dist/vercel/index.js.map +1 -0
  534. package/packages/mcp-servers/dist/vercel/server.d.ts +17 -0
  535. package/packages/mcp-servers/dist/vercel/server.d.ts.map +1 -0
  536. package/packages/mcp-servers/dist/vercel/server.js +265 -0
  537. package/packages/mcp-servers/dist/vercel/server.js.map +1 -0
  538. package/packages/mcp-servers/dist/vercel/types.d.ts +189 -0
  539. package/packages/mcp-servers/dist/vercel/types.d.ts.map +1 -0
  540. package/packages/mcp-servers/dist/vercel/types.js +65 -0
  541. package/packages/mcp-servers/dist/vercel/types.js.map +1 -0
  542. package/packages/mcp-servers/package-lock.json +3765 -0
  543. package/packages/mcp-servers/package.json +64 -0
  544. package/packages/mcp-servers/test/reporters/test-failure-reporter.ts +372 -0
  545. package/packages/mcp-servers/vitest.config.ts +27 -0
  546. package/scripts/__tests__/README.md +163 -0
  547. package/scripts/apply-credential-hardening.sh +271 -0
  548. package/scripts/credential-providers/manual.js +56 -0
  549. package/scripts/credential-providers/onepassword.js +85 -0
  550. package/scripts/credential-providers/provider-interface.js +104 -0
  551. package/scripts/encrypt-credential.js +337 -0
  552. package/scripts/feedback-launcher.js +338 -0
  553. package/scripts/feedback-orchestrator.js +373 -0
  554. package/scripts/fix-mcp-launcher-issues.sh +97 -0
  555. package/scripts/force-spawn-tasks.js +651 -0
  556. package/scripts/force-triage-reports.js +560 -0
  557. package/scripts/generate-protected-actions-spec.js +142 -0
  558. package/scripts/generate-proxy-certs.sh +158 -0
  559. package/scripts/grant-chrome-ext-permissions.sh +242 -0
  560. package/scripts/mcp-launcher.js +125 -0
  561. package/scripts/merge-settings.cjs +167 -0
  562. package/scripts/patch-clawd.py +844 -0
  563. package/scripts/patch-credential-cache.py +313 -0
  564. package/scripts/patches/credential-file-guard-patched.mjs +573 -0
  565. package/scripts/patches/credential-file-guard.js.patched +573 -0
  566. package/scripts/patches/verify-tokenizer.mjs +132 -0
  567. package/scripts/protect-framework.sh +478 -0
  568. package/scripts/readme-chrome.template +12 -0
  569. package/scripts/reap-completed-agents.js +439 -0
  570. package/scripts/reinstall.sh +86 -0
  571. package/scripts/resign-node.sh +185 -0
  572. package/scripts/rotation-proxy.js +656 -0
  573. package/scripts/rotation-stress-monitor.mjs +862 -0
  574. package/scripts/setup-automation-service.sh +648 -0
  575. package/scripts/setup-check.js +251 -0
  576. package/scripts/watch-claude-version.js +142 -0
  577. package/specs/framework/CORE-INVARIANTS.md +161 -0
  578. package/specs/patterns/AGENT-PATTERNS.md +223 -0
  579. package/specs/patterns/HOOK-PATTERNS.md +242 -0
  580. package/specs/patterns/MCP-SERVER-PATTERNS.md +144 -0
  581. package/templates/config/gitignore.template +14 -0
  582. package/templates/config/merge-chain-check.yml.template +51 -0
  583. package/templates/config/package.json.template +18 -0
  584. package/templates/config/pnpm-workspace.yaml +5 -0
  585. package/templates/config/services.json.template +18 -0
  586. package/templates/config/tsconfig.base.json +17 -0
  587. package/templates/scaffold/integrations/_template/.gitkeep +0 -0
  588. package/templates/scaffold/packages/logger/package.json +17 -0
  589. package/templates/scaffold/packages/logger/src/logger.ts +44 -0
  590. package/templates/scaffold/packages/shared/package.json +17 -0
  591. package/templates/scaffold/packages/shared/src/errors.ts +43 -0
  592. package/templates/scaffold/products/_product/apps/backend/package.json +21 -0
  593. package/templates/scaffold/products/_product/apps/backend/src/index.ts +17 -0
  594. package/templates/scaffold/products/_product/apps/extension/.gitkeep +0 -0
  595. package/templates/scaffold/products/_product/apps/web/.gitkeep +0 -0
  596. package/templates/scaffold/specs/global/.gitkeep +0 -0
  597. package/templates/scaffold/specs/local/.gitkeep +0 -0
  598. package/templates/scaffold/specs/reference/.gitkeep +0 -0
  599. package/version.json +15 -0
@@ -0,0 +1,1139 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PreToolUse Hook: Credential File Guard
4
+ *
5
+ * Intercepts Read, Write, Edit, Grep, Glob, and Bash tool calls and blocks
6
+ * access to files containing credentials, secrets, or sensitive configuration.
7
+ *
8
+ * For Bash commands, also detects:
9
+ * - ANY command argument referencing protected files (not just known read commands)
10
+ * - Output redirection targets (>, >>)
11
+ * - Embedded file path references in code strings (raw command scan)
12
+ * - References to protected credential environment variables ($TOKEN, etc.)
13
+ * - Environment dump commands (env, printenv, export -p)
14
+ *
15
+ * NOTE: Bash detection is defense-in-depth only. Primary defenses are:
16
+ * - Root-ownership of credential files (OS-level, unbypassable)
17
+ * - Credentials only in .mcp.json env blocks, not in shell env (architectural)
18
+ *
19
+ * Uses Claude Code's permissionDecision JSON output for hard blocking.
20
+ *
21
+ * Input: JSON on stdin from Claude Code PreToolUse event
22
+ * Output: JSON on stdout with permissionDecision (deny/allow)
23
+ *
24
+ * SECURITY: This file should be root-owned via protect-framework.sh
25
+ *
26
+ * @version 3.0.0
27
+ */
28
+
29
+ import fs from 'node:fs';
30
+ import path from 'node:path';
31
+ import { createRequest, checkApproval, loadProtectedActions } from './lib/approval-utils.js';
32
+
33
+ // ============================================================================
34
+ // Protected File Patterns
35
+ // ============================================================================
36
+
37
+ /**
38
+ * File basenames that are always blocked regardless of path
39
+ */
40
+ const BLOCKED_BASENAMES = new Set([
41
+ '.env',
42
+ '.env.local',
43
+ '.env.production',
44
+ '.env.staging',
45
+ '.env.development',
46
+ '.env.test',
47
+ '.credentials.json',
48
+ ]);
49
+
50
+ /**
51
+ * Path suffixes that are blocked (matched against the end of the resolved path)
52
+ * These are relative to the project directory
53
+ */
54
+ const BLOCKED_PATH_SUFFIXES = [
55
+ '.claude/protection-key',
56
+ '.claude/api-key-rotation.json',
57
+ '.claude/bypass-approval-token.json',
58
+ '.claude/commit-approval-token.json',
59
+ '.claude/credential-provider.json',
60
+ '.claude/protected-action-approvals.json',
61
+ '.claude/vault-mappings.json',
62
+ '.claude/config/services.json',
63
+ '.mcp.json',
64
+ ];
65
+ // Note: The suffix '.claude/api-key-rotation.json' also blocks the user-level
66
+ // path at ~/.claude/api-key-rotation.json since both end with the same suffix.
67
+
68
+ /**
69
+ * Path suffixes that are ALWAYS hard-blocked with no approval escape hatch.
70
+ * These files control the approval system itself — granting access would
71
+ * compromise the security model.
72
+ */
73
+ const ALWAYS_BLOCKED_SUFFIXES = new Set([
74
+ '.claude/protection-key',
75
+ '.claude/protected-action-approvals.json',
76
+ '.claude/bypass-approval-token.json',
77
+ '.claude/commit-approval-token.json',
78
+ ]);
79
+
80
+ /**
81
+ * Basenames that are always hard-blocked with no approval escape hatch.
82
+ * Raw secrets files — no legitimate agent access.
83
+ */
84
+ const ALWAYS_BLOCKED_BASENAMES = new Set([
85
+ '.env',
86
+ '.env.local',
87
+ '.env.production',
88
+ '.env.staging',
89
+ '.env.development',
90
+ '.env.test',
91
+ '.credentials.json',
92
+ ]);
93
+
94
+ /**
95
+ * Patterns matched against the full path
96
+ */
97
+ const BLOCKED_PATH_PATTERNS = [
98
+ /\.env(\.[a-z]+)?$/i, // Any .env or .env.* file
99
+ ];
100
+
101
+ // ============================================================================
102
+ // Bash Command Analysis
103
+ // ============================================================================
104
+
105
+ /**
106
+ * Commands that do NOT access files via their arguments, so their arguments
107
+ * should NOT be checked as file paths. This prevents false positives like
108
+ * "echo .env" (which just prints the text ".env", not reading any file).
109
+ *
110
+ * All other commands have their arguments checked against protected paths.
111
+ */
112
+ const NON_FILE_COMMANDS = new Set([
113
+ 'echo', 'printf', // Output text, don't access files
114
+ 'mkdir', 'rmdir', // Create/remove directories
115
+ 'cd', 'pushd', 'popd', // Change directory
116
+ 'touch', // Create/update timestamps (not reading)
117
+ 'chmod', 'chown', 'chgrp', // Change permissions (not reading contents)
118
+ 'ln', // Create links
119
+ 'alias', 'unalias', // Shell aliases
120
+ 'export', 'set', 'unset', // Shell variables
121
+ 'type', 'which', 'whereis', 'command', // Command location
122
+ 'hash', 'history', // Shell builtins
123
+ 'true', 'false', // No-ops
124
+ 'test', '[', // Conditionals
125
+ 'kill', 'killall', // Process signals
126
+ 'sleep', 'wait', // Timing
127
+ 'exit', 'return', // Flow control
128
+ 'npm', 'npx', 'pnpm', 'yarn', 'bun', // Package managers (install commands)
129
+ 'pip', 'pip3', // Python package manager
130
+ 'gem', // Ruby package manager
131
+ 'cargo', // Rust package manager
132
+ 'go', // Go toolchain
133
+ 'git', // Git (has its own security checks)
134
+ 'docker', 'docker-compose', // Container tools
135
+ 'brew', 'apt', 'yum', // System package managers
136
+ ]);
137
+
138
+ /**
139
+ * Commands that dump all environment variables.
140
+ * Requires whitespace or start-of-string before command name to avoid
141
+ * matching filenames like ".env" (where \b would falsely match).
142
+ */
143
+ const ENV_DUMP_COMMANDS = /(?:^|\s)(env|printenv|export\s+-p)(?:\s|$|\|)/;
144
+
145
+ /**
146
+ * Load protected credential key names from protected-actions.json
147
+ * @param {string} projectDir
148
+ * @returns {Set<string>}
149
+ */
150
+ function loadCredentialKeys(projectDir) {
151
+ const keys = new Set();
152
+ try {
153
+ const configPath = path.join(projectDir, '.claude', 'hooks', 'protected-actions.json');
154
+ if (!fs.existsSync(configPath)) {
155
+ return keys;
156
+ }
157
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
158
+ if (config && config.servers) {
159
+ for (const server of Object.values(config.servers)) {
160
+ if (Array.isArray(server.credentialKeys)) {
161
+ for (const key of server.credentialKeys) {
162
+ keys.add(key);
163
+ }
164
+ }
165
+ }
166
+ }
167
+ } catch (err) {
168
+ // Fail open for credential key loading - the architectural defense
169
+ // (creds not in env) is the primary protection
170
+ console.error(`[credential-file-guard] Warning: Could not load credential keys: ${err.message}`);
171
+ }
172
+ return keys;
173
+ }
174
+
175
+ /**
176
+ * Simple shell tokenizer that respects single and double quotes.
177
+ * @param {string} str
178
+ * @returns {string[]}
179
+ */
180
+ function tokenize(str) {
181
+ const tokens = [];
182
+ let current = '';
183
+ let inSingle = false;
184
+ let inDouble = false;
185
+ let escaped = false;
186
+
187
+ for (const ch of str) {
188
+ if (escaped) {
189
+ current += ch;
190
+ escaped = false;
191
+ continue;
192
+ }
193
+ if (ch === '\\' && !inSingle) {
194
+ escaped = true;
195
+ continue;
196
+ }
197
+ if (ch === "'" && !inDouble) {
198
+ inSingle = !inSingle;
199
+ continue;
200
+ }
201
+ if (ch === '"' && !inSingle) {
202
+ inDouble = !inDouble;
203
+ continue;
204
+ }
205
+ if ((ch === ' ' || ch === '\t') && !inSingle && !inDouble) {
206
+ if (current) {
207
+ tokens.push(current);
208
+ current = '';
209
+ }
210
+ continue;
211
+ }
212
+ current += ch;
213
+ }
214
+ if (current) {
215
+ tokens.push(current);
216
+ }
217
+ return tokens;
218
+ }
219
+
220
+ /**
221
+ * Split a command string on shell operators (|, ||, &&, ;) while respecting
222
+ * single and double quotes. This prevents mangling paths like 'path;with;semicolons/.env'.
223
+ *
224
+ * @param {string} command
225
+ * @returns {string[]} Array of sub-command strings
226
+ */
227
+ function splitOnShellOperators(command) {
228
+ const parts = [];
229
+ let current = '';
230
+ let inSingle = false;
231
+ let inDouble = false;
232
+ let escaped = false;
233
+ let i = 0;
234
+
235
+ while (i < command.length) {
236
+ const ch = command[i];
237
+
238
+ if (escaped) {
239
+ current += ch;
240
+ escaped = false;
241
+ i++;
242
+ continue;
243
+ }
244
+ if (ch === '\\' && !inSingle) {
245
+ escaped = true;
246
+ current += ch;
247
+ i++;
248
+ continue;
249
+ }
250
+ if (ch === "'" && !inDouble) {
251
+ inSingle = !inSingle;
252
+ current += ch;
253
+ i++;
254
+ continue;
255
+ }
256
+ if (ch === '"' && !inSingle) {
257
+ inDouble = !inDouble;
258
+ current += ch;
259
+ i++;
260
+ continue;
261
+ }
262
+
263
+ // Only split on operators when outside quotes
264
+ if (!inSingle && !inDouble) {
265
+ // Check for && or ||
266
+ if ((ch === '&' || ch === '|') && i + 1 < command.length && command[i + 1] === ch) {
267
+ if (current.trim()) parts.push(current.trim());
268
+ current = '';
269
+ i += 2;
270
+ continue;
271
+ }
272
+ // Check for single | (not ||)
273
+ if (ch === '|') {
274
+ if (current.trim()) parts.push(current.trim());
275
+ current = '';
276
+ i++;
277
+ continue;
278
+ }
279
+ // Check for ;
280
+ if (ch === ';') {
281
+ if (current.trim()) parts.push(current.trim());
282
+ current = '';
283
+ i++;
284
+ continue;
285
+ }
286
+ }
287
+
288
+ current += ch;
289
+ i++;
290
+ }
291
+ if (current.trim()) parts.push(current.trim());
292
+ return parts;
293
+ }
294
+
295
+ /**
296
+ * Extract file paths from a bash command that may access protected files.
297
+ * Splits on pipes, semicolons, && and || to process individual sub-commands,
298
+ * respecting shell quoting to avoid mangling quoted paths.
299
+ *
300
+ * SECURITY FIX (C2): Uses universal argument scanning for ALL commands except
301
+ * those in NON_FILE_COMMANDS (echo, printf, mkdir, etc.). Previously only
302
+ * checked 14 specific file-reading commands, allowing bypass via grep, awk,
303
+ * python3, node, sort, diff, or any other command.
304
+ *
305
+ * SECURITY FIX (M1): Checks output redirection targets (>, >>) in addition
306
+ * to input redirection (<). This prevents writing to protected files via
307
+ * echo/printf redirection.
308
+ *
309
+ * @param {string} command
310
+ * @returns {string[]} Array of file paths found
311
+ */
312
+ function extractFilePathsFromCommand(command) {
313
+ const paths = [];
314
+
315
+ // Split on pipe, semicolons, && and || respecting quotes
316
+ const subCommands = splitOnShellOperators(command);
317
+
318
+ for (const sub of subCommands) {
319
+ const trimmed = sub.trim();
320
+ if (!trimmed) continue;
321
+
322
+ // Tokenize: split on whitespace but respect quotes
323
+ const tokens = tokenize(trimmed);
324
+ if (tokens.length === 0) continue;
325
+
326
+ const cmd = path.basename(tokens[0]); // Handle /usr/bin/cat etc.
327
+
328
+ // Check arguments as file paths for ALL commands EXCEPT known non-file commands.
329
+ // This is safer than maintaining a blocklist of file-reading commands (C2 fix).
330
+ if (!NON_FILE_COMMANDS.has(cmd)) {
331
+ for (let i = 1; i < tokens.length; i++) {
332
+ const token = tokens[i];
333
+ // Skip flags (but not paths starting with ./ or ../)
334
+ if (token.startsWith('-') && !token.startsWith('./') && !token.startsWith('../')) {
335
+ continue;
336
+ }
337
+ // Skip redirection operators (targets handled below)
338
+ if (token === '>' || token === '>>' || token === '<' || token === '2>' || token === '2>>') {
339
+ i++; // skip the target
340
+ continue;
341
+ }
342
+ // Skip variable references (checked separately)
343
+ if (token.startsWith('$')) {
344
+ continue;
345
+ }
346
+ // This looks like a potential file path argument
347
+ if (token) {
348
+ paths.push(token);
349
+ }
350
+ }
351
+ }
352
+
353
+ // Check ALL output and input redirection targets regardless of command (M1 fix).
354
+ // This catches: echo '{}' > .claude/bypass-approval-token.json
355
+ // and: grep secret < .env
356
+ const redirectMatches = trimmed.matchAll(/(?:^|[^<>])(>>?|2>>?|<)\s*(\S+)/g);
357
+ for (const match of redirectMatches) {
358
+ const target = match[2];
359
+ if (target && !target.startsWith('$') && !target.startsWith('&')) {
360
+ paths.push(target);
361
+ }
362
+ }
363
+ }
364
+
365
+ return paths;
366
+ }
367
+
368
+ /**
369
+ * Scan the raw command string for embedded references to protected file paths.
370
+ * This catches cases where file paths are embedded inside code strings
371
+ * (e.g., python3 -c "open('.mcp.json').read()") that token-based extraction misses.
372
+ *
373
+ * Only checks BLOCKED_PATH_SUFFIXES (longer, more specific paths like .mcp.json,
374
+ * .claude/protection-key). Does NOT check BLOCKED_BASENAMES (short names like .env)
375
+ * to avoid false positives with commands like "echo .env" or "npm install .env-parser".
376
+ * Basename detection is handled by the universal argument scanning in extractFilePathsFromCommand().
377
+ *
378
+ * SECURITY FIX (C2): Provides a second layer of defense against bypass via
379
+ * scripting language interpreters.
380
+ *
381
+ * @param {string} command
382
+ * @returns {{ blocked: boolean, reason: string, matchedSuffix?: string }}
383
+ */
384
+ function scanRawCommandForProtectedPaths(command) {
385
+ // Check for blocked path suffixes as substrings in the command.
386
+ // These are specific enough to not cause false positives.
387
+ for (const suffix of BLOCKED_PATH_SUFFIXES) {
388
+ if (command.includes(suffix)) {
389
+ return {
390
+ blocked: true,
391
+ reason: `Command references protected path "${suffix}"`,
392
+ matchedSuffix: suffix,
393
+ };
394
+ }
395
+ }
396
+
397
+ return { blocked: false, reason: '' };
398
+ }
399
+
400
+ /**
401
+ * Escape special regex characters in a string
402
+ * @param {string} str
403
+ * @returns {string}
404
+ */
405
+ function escapeRegExp(str) {
406
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
407
+ }
408
+
409
+ /**
410
+ * Check if a bash command references protected credential env vars.
411
+ * @param {string} command
412
+ * @param {Set<string>} credentialKeys
413
+ * @returns {{ blocked: boolean, reason: string }}
414
+ */
415
+ function checkBashEnvAccess(command, credentialKeys) {
416
+ // 1. Block full environment dump commands
417
+ if (ENV_DUMP_COMMANDS.test(command)) {
418
+ return {
419
+ blocked: true,
420
+ reason: 'Environment dump commands are blocked to prevent credential exposure',
421
+ };
422
+ }
423
+
424
+ // 2. Check for direct references to credential env vars
425
+ if (credentialKeys.size > 0) {
426
+ for (const key of credentialKeys) {
427
+ // Match $KEY or ${KEY}
428
+ const varPattern = new RegExp('\\$\\{?' + escapeRegExp(key) + '\\}?\\b');
429
+ if (varPattern.test(command)) {
430
+ return {
431
+ blocked: true,
432
+ reason: `Command references protected credential variable: ${key}`,
433
+ };
434
+ }
435
+
436
+ // Also check printenv KEY
437
+ const printenvPattern = new RegExp('\\bprintenv\\s+' + escapeRegExp(key) + '\\b');
438
+ if (printenvPattern.test(command)) {
439
+ return {
440
+ blocked: true,
441
+ reason: `Command reads protected credential variable: ${key}`,
442
+ };
443
+ }
444
+ }
445
+ }
446
+
447
+ return { blocked: false, reason: '' };
448
+ }
449
+
450
+ // ============================================================================
451
+ // File Approval Logic
452
+ // ============================================================================
453
+
454
+ /**
455
+ * Check if a normalized file path is always-blocked (no approval possible).
456
+ * @param {string} normalizedPath - Resolved, normalized file path
457
+ * @returns {boolean}
458
+ */
459
+ function isAlwaysBlocked(normalizedPath) {
460
+ const basename = path.basename(normalizedPath);
461
+ const normalizedForSuffix = normalizedPath.replace(/\\/g, '/');
462
+
463
+ // Check always-blocked basenames (.env, .credentials.json, etc.)
464
+ if (ALWAYS_BLOCKED_BASENAMES.has(basename)) {
465
+ return true;
466
+ }
467
+
468
+ // Check always-blocked suffixes (protection-key, approval tokens)
469
+ for (const suffix of ALWAYS_BLOCKED_SUFFIXES) {
470
+ if (normalizedForSuffix.endsWith(suffix)) {
471
+ return true;
472
+ }
473
+ }
474
+
475
+ // Check .env.* pattern (any .env variant is always-blocked)
476
+ for (const pattern of BLOCKED_PATH_PATTERNS) {
477
+ if (pattern.test(normalizedPath)) {
478
+ return true;
479
+ }
480
+ }
481
+
482
+ return false;
483
+ }
484
+
485
+ /**
486
+ * Check if a path suffix is in the always-blocked set.
487
+ * @param {string} suffix - A BLOCKED_PATH_SUFFIXES entry
488
+ * @returns {boolean}
489
+ */
490
+ function isAlwaysBlockedSuffix(suffix) {
491
+ return ALWAYS_BLOCKED_SUFFIXES.has(suffix);
492
+ }
493
+
494
+ /**
495
+ * Find file protection by suffix string (for raw command scan matches).
496
+ * @param {string} suffix - The matched suffix from BLOCKED_PATH_SUFFIXES
497
+ * @param {string} projectDir - The project directory
498
+ * @returns {{ key: string, config: object } | null}
499
+ */
500
+ function findFileProtectionBySuffix(suffix, projectDir) {
501
+ try {
502
+ const config = loadProtectedActions();
503
+ if (!config || !config.files) {
504
+ return null;
505
+ }
506
+
507
+ // Check if suffix matches any file config key (require path-component boundary)
508
+ for (const [key, fileConfig] of Object.entries(config.files)) {
509
+ if (suffix === key) {
510
+ return { key, config: fileConfig };
511
+ }
512
+ // Match at path separator boundary: suffix ends with /key or key ends with /suffix
513
+ if (suffix.endsWith('/' + key) || suffix.endsWith(key)) {
514
+ return { key, config: fileConfig };
515
+ }
516
+ }
517
+
518
+ return null;
519
+ } catch (err) {
520
+ console.error(`[credential-file-guard] Warning: Could not load file protection config: ${err.message}`);
521
+ return null;
522
+ }
523
+ }
524
+
525
+ /**
526
+ * Find file protection configuration from protected-actions.json.
527
+ * Returns the matching file config if the file is in the approvable tier.
528
+ *
529
+ * @param {string} filePath - The file path being accessed
530
+ * @param {string} projectDir - The project directory
531
+ * @returns {{ key: string, config: object } | null}
532
+ */
533
+ function findFileProtection(filePath, projectDir) {
534
+ try {
535
+ const config = loadProtectedActions();
536
+ if (!config || !config.files) {
537
+ return null;
538
+ }
539
+
540
+ const normalizedPath = path.resolve(filePath).replace(/\\/g, '/');
541
+
542
+ // Don't allow approval for always-blocked files
543
+ if (isAlwaysBlocked(normalizedPath)) {
544
+ return null;
545
+ }
546
+
547
+ for (const [key, fileConfig] of Object.entries(config.files)) {
548
+ if (normalizedPath.endsWith(key)) {
549
+ return { key, config: fileConfig };
550
+ }
551
+ }
552
+
553
+ return null;
554
+ } catch (err) {
555
+ console.error(`[credential-file-guard] Warning: Could not load file protection config: ${err.message}`);
556
+ return null;
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Block a file operation but include the approval code and instructions.
562
+ * Used for approvable files (not always-blocked).
563
+ *
564
+ * @param {string} filePath - The file path
565
+ * @param {string} reason - Why it's blocked
566
+ * @param {object} request - The approval request from createRequest()
567
+ * @param {object} protection - The file protection config
568
+ */
569
+ function blockWithApprovalRequest(filePath, reason, request, protection) {
570
+ const lines = [
571
+ 'BLOCKED: Protected File Access (Approval Required)',
572
+ '',
573
+ `Why: ${reason}`,
574
+ '',
575
+ `Path: ${filePath}`,
576
+ '',
577
+ ];
578
+
579
+ if (protection.config.protection === 'deputy-cto-approval') {
580
+ lines.push(
581
+ `This file requires deputy-CTO approval. Submit a report to deputy-CTO with code: ${request.code}`,
582
+ `Or CTO can type: ${request.phrase} ${request.code}`,
583
+ );
584
+ } else {
585
+ lines.push(
586
+ `CTO must type: ${request.phrase} ${request.code}`,
587
+ );
588
+ }
589
+
590
+ lines.push(
591
+ '',
592
+ `This approval expires in ${request.expires_in_minutes} minutes and can only be used once.`,
593
+ );
594
+
595
+ const fullReason = lines.join('\n');
596
+
597
+ console.log(JSON.stringify({
598
+ hookSpecificOutput: {
599
+ hookEventName: 'PreToolUse',
600
+ permissionDecision: 'deny',
601
+ permissionDecisionReason: fullReason,
602
+ },
603
+ }));
604
+
605
+ console.error('');
606
+ console.error('══════════════════════════════════════════════════════════════');
607
+ console.error(' BLOCKED: Protected File (Approval Required)');
608
+ console.error('══════════════════════════════════════════════════════════════');
609
+ console.error('');
610
+ console.error(` Why: ${reason}`);
611
+ console.error(` Path: ${filePath}`);
612
+ console.error('');
613
+ if (protection.config.protection === 'deputy-cto-approval') {
614
+ console.error(` Submit report to deputy-CTO with code: ${request.code}`);
615
+ console.error(` Or CTO type: ${request.phrase} ${request.code}`);
616
+ } else {
617
+ console.error(` CTO type: ${request.phrase} ${request.code}`);
618
+ }
619
+ console.error('');
620
+ console.error('══════════════════════════════════════════════════════════════');
621
+ console.error('');
622
+
623
+ process.exit(0);
624
+ }
625
+
626
+ /**
627
+ * Block a Bash command but include the approval code and instructions.
628
+ * Used for commands that reference approvable files.
629
+ *
630
+ * @param {string} command - The bash command
631
+ * @param {string} reason - Why it's blocked
632
+ * @param {object[]} approvalRequests - Array of { request, protection, filePath } for each approvable file
633
+ */
634
+ function blockBashWithApprovalRequest(command, reason, approvalRequests) {
635
+ const truncatedCmd = command.length > 100 ? command.substring(0, 100) + '...' : command;
636
+ const lines = [
637
+ 'BLOCKED: Credential Access via Bash (Approval Required)',
638
+ '',
639
+ `Why: ${reason}`,
640
+ '',
641
+ `Command: ${truncatedCmd}`,
642
+ '',
643
+ ];
644
+
645
+ for (const { request, protection } of approvalRequests) {
646
+ if (protection.config.protection === 'deputy-cto-approval') {
647
+ lines.push(`Submit report to deputy-CTO with code: ${request.code} or CTO type: ${request.phrase} ${request.code}`);
648
+ } else {
649
+ lines.push(`CTO must type: ${request.phrase} ${request.code}`);
650
+ }
651
+ }
652
+
653
+ lines.push('', `Approvals expire in ${approvalRequests[0].request.expires_in_minutes} minutes and can only be used once.`);
654
+
655
+ const fullReason = lines.join('\n');
656
+
657
+ console.log(JSON.stringify({
658
+ hookSpecificOutput: {
659
+ hookEventName: 'PreToolUse',
660
+ permissionDecision: 'deny',
661
+ permissionDecisionReason: fullReason,
662
+ },
663
+ }));
664
+
665
+ console.error('');
666
+ console.error('══════════════════════════════════════════════════════════════');
667
+ console.error(' BASH BLOCKED: Protected File (Approval Required)');
668
+ console.error('══════════════════════════════════════════════════════════════');
669
+ console.error('');
670
+ console.error(` Why: ${reason}`);
671
+ console.error(` Command: ${truncatedCmd}`);
672
+ console.error('');
673
+ for (const { request, protection } of approvalRequests) {
674
+ if (protection.config.protection === 'deputy-cto-approval') {
675
+ console.error(` Submit to deputy-CTO with code: ${request.code} or CTO type: ${request.phrase} ${request.code}`);
676
+ } else {
677
+ console.error(` CTO type: ${request.phrase} ${request.code}`);
678
+ }
679
+ }
680
+ console.error('');
681
+ console.error('══════════════════════════════════════════════════════════════');
682
+ console.error('');
683
+
684
+ process.exit(0);
685
+ }
686
+
687
+ // ============================================================================
688
+ // Guard Logic
689
+ // ============================================================================
690
+
691
+ /**
692
+ * Check if a file path should be blocked
693
+ * @param {string} filePath - The file path being read
694
+ * @param {string} projectDir - The project directory
695
+ * @returns {{ blocked: boolean, reason: string }}
696
+ */
697
+ function checkFilePath(filePath, projectDir) {
698
+ if (!filePath) {
699
+ return { blocked: false, reason: '' };
700
+ }
701
+
702
+ // Normalize the path
703
+ const normalizedPath = path.resolve(filePath);
704
+ const basename = path.basename(normalizedPath);
705
+
706
+ // Check blocked basenames
707
+ if (BLOCKED_BASENAMES.has(basename)) {
708
+ return {
709
+ blocked: true,
710
+ reason: `File "${basename}" contains credentials or secrets`,
711
+ };
712
+ }
713
+
714
+ // Check blocked path suffixes
715
+ const normalizedForSuffix = normalizedPath.replace(/\\/g, '/');
716
+ for (const suffix of BLOCKED_PATH_SUFFIXES) {
717
+ if (normalizedForSuffix.endsWith(suffix)) {
718
+ return {
719
+ blocked: true,
720
+ reason: `File "${suffix}" contains sensitive configuration`,
721
+ };
722
+ }
723
+ }
724
+
725
+ // Check blocked patterns
726
+ for (const pattern of BLOCKED_PATH_PATTERNS) {
727
+ if (pattern.test(normalizedPath)) {
728
+ return {
729
+ blocked: true,
730
+ reason: `File matches protected credential pattern: ${basename}`,
731
+ };
732
+ }
733
+ }
734
+
735
+ return { blocked: false, reason: '' };
736
+ }
737
+
738
+ /**
739
+ * Tools that access files and should be blocked for credential files.
740
+ *
741
+ * SECURITY FIX (C1): Added Grep and Glob which can also access file contents.
742
+ * Grep with output_mode="content" returns matching lines from files.
743
+ * Glob returns file paths (not contents) but is blocked for defense-in-depth.
744
+ */
745
+ const FILE_ACCESS_TOOLS = new Set(['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob']);
746
+
747
+ // ============================================================================
748
+ // Blocking Functions
749
+ // ============================================================================
750
+
751
+ /**
752
+ * Block a file operation using Claude Code's permissionDecision system
753
+ */
754
+ function blockRead(filePath, reason) {
755
+ const fullReason = [
756
+ 'BLOCKED: Credential File Access',
757
+ '',
758
+ `Why: ${reason}`,
759
+ '',
760
+ `Path: ${filePath}`,
761
+ '',
762
+ 'This file is protected by GENTYR to prevent credential exposure.',
763
+ 'If you need access to this file, request CTO approval.',
764
+ ].join('\n');
765
+
766
+ // Output JSON to stdout for Claude Code's permission system (hard deny)
767
+ console.log(JSON.stringify({
768
+ hookSpecificOutput: {
769
+ hookEventName: 'PreToolUse',
770
+ permissionDecision: 'deny',
771
+ permissionDecisionReason: fullReason,
772
+ },
773
+ }));
774
+
775
+ // Also output to stderr for visibility
776
+ console.error('');
777
+ console.error('══════════════════════════════════════════════════════════════');
778
+ console.error(' READ BLOCKED: Credential File Protection');
779
+ console.error('══════════════════════════════════════════════════════════════');
780
+ console.error('');
781
+ console.error(` Why: ${reason}`);
782
+ console.error('');
783
+ console.error(` Path: ${filePath}`);
784
+ console.error('');
785
+ console.error('══════════════════════════════════════════════════════════════');
786
+ console.error('');
787
+
788
+ process.exit(0); // Exit 0 - the JSON output handles the deny
789
+ }
790
+
791
+ /**
792
+ * Block a Bash command using Claude Code's permissionDecision system
793
+ */
794
+ function blockBash(command, reason) {
795
+ const truncatedCmd = command.length > 100 ? command.substring(0, 100) + '...' : command;
796
+ const fullReason = [
797
+ 'BLOCKED: Credential Access via Bash',
798
+ '',
799
+ `Why: ${reason}`,
800
+ '',
801
+ `Command: ${truncatedCmd}`,
802
+ '',
803
+ 'This command is blocked by GENTYR to prevent credential exposure.',
804
+ 'Credentials should only be accessed through approved MCP server tools.',
805
+ 'If you need access, request CTO approval.',
806
+ ].join('\n');
807
+
808
+ // Output JSON to stdout for Claude Code's permission system (hard deny)
809
+ console.log(JSON.stringify({
810
+ hookSpecificOutput: {
811
+ hookEventName: 'PreToolUse',
812
+ permissionDecision: 'deny',
813
+ permissionDecisionReason: fullReason,
814
+ },
815
+ }));
816
+
817
+ // Also output to stderr for visibility
818
+ console.error('');
819
+ console.error('══════════════════════════════════════════════════════════════');
820
+ console.error(' BASH BLOCKED: Credential Protection');
821
+ console.error('══════════════════════════════════════════════════════════════');
822
+ console.error('');
823
+ console.error(` Why: ${reason}`);
824
+ console.error('');
825
+ console.error(` Command: ${truncatedCmd}`);
826
+ console.error('');
827
+ console.error('══════════════════════════════════════════════════════════════');
828
+ console.error('');
829
+
830
+ process.exit(0); // Exit 0 - the JSON output handles the deny
831
+ }
832
+
833
+ // ============================================================================
834
+ // Main
835
+ // ============================================================================
836
+
837
+ let input = '';
838
+
839
+ process.stdin.on('data', (chunk) => {
840
+ input += chunk.toString();
841
+ });
842
+
843
+ process.stdin.on('end', () => {
844
+ try {
845
+ const hookInput = JSON.parse(input);
846
+
847
+ const toolName = hookInput.tool_name;
848
+ const toolInput = hookInput.tool_input || {};
849
+ const projectDir = hookInput.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
850
+
851
+ // Only check tools that access files or credentials
852
+ if (!FILE_ACCESS_TOOLS.has(toolName)) {
853
+ process.exit(0);
854
+ }
855
+
856
+ // --- Bash tool: check command for file paths and env var references ---
857
+ if (toolName === 'Bash') {
858
+ const command = toolInput.command || '';
859
+ if (!command) {
860
+ process.exit(0);
861
+ }
862
+
863
+ // Check 1: File paths extracted from command tokens
864
+ const filePaths = extractFilePathsFromCommand(command);
865
+ const blockedApprovable = [];
866
+ let hasAlwaysBlocked = false;
867
+ let firstBlockReason = '';
868
+
869
+ for (const fp of filePaths) {
870
+ const result = checkFilePath(fp, projectDir);
871
+ if (result.blocked) {
872
+ const normalizedPath = path.resolve(fp);
873
+ if (!firstBlockReason) firstBlockReason = result.reason;
874
+
875
+ if (isAlwaysBlocked(normalizedPath)) {
876
+ hasAlwaysBlocked = true;
877
+ break; // Any always-blocked file → hard block the whole command
878
+ }
879
+
880
+ const protection = findFileProtection(fp, projectDir);
881
+ if (protection) {
882
+ blockedApprovable.push({ filePath: fp, reason: result.reason, protection });
883
+ } else {
884
+ // Blocked but not in approvable config → hard block
885
+ hasAlwaysBlocked = true;
886
+ break;
887
+ }
888
+ }
889
+ }
890
+
891
+ if (hasAlwaysBlocked) {
892
+ blockBash(command, firstBlockReason);
893
+ return;
894
+ }
895
+
896
+ // Track which file config keys were approved (to skip raw scan for the same paths)
897
+ const approvedFileKeys = new Set();
898
+
899
+ if (blockedApprovable.length > 0) {
900
+ // Check if all approvable files have approvals
901
+ let allApproved = true;
902
+ const approvalRequests = [];
903
+
904
+ for (const { filePath: fp, reason, protection } of blockedApprovable) {
905
+ const approval = checkApproval('__file__', protection.key);
906
+ if (approval) {
907
+ approvedFileKeys.add(protection.key);
908
+ } else {
909
+ allApproved = false;
910
+ const approvalMode = protection.config.protection === 'deputy-cto-approval' ? 'deputy-cto' : 'cto';
911
+ const request = createRequest('__file__', protection.key, {}, protection.config.phrase, { approvalMode });
912
+ approvalRequests.push({ request, protection, filePath: fp });
913
+ }
914
+ }
915
+
916
+ if (allApproved) {
917
+ // All files approved — fall through to remaining checks
918
+ } else {
919
+ blockBashWithApprovalRequest(command, blockedApprovable[0].reason, approvalRequests);
920
+ return;
921
+ }
922
+ }
923
+
924
+ // Check 2: Raw command scan for embedded protected path references
925
+ // Catches: python3 -c "open('.mcp.json')", node -e "fs.readFileSync('.env')", etc.
926
+ const rawScanResult = scanRawCommandForProtectedPaths(command);
927
+ if (rawScanResult.blocked) {
928
+ const matchedSuffix = rawScanResult.matchedSuffix;
929
+
930
+ // Skip if this path was already approved in Check 1 (token extraction)
931
+ const alreadyApproved = matchedSuffix && !isAlwaysBlockedSuffix(matchedSuffix) &&
932
+ [...approvedFileKeys].some(key => matchedSuffix.endsWith(key) || key.endsWith(matchedSuffix) || matchedSuffix === key);
933
+
934
+ if (!alreadyApproved) {
935
+ // Check if the matched suffix is approvable (not always-blocked)
936
+ if (matchedSuffix && !isAlwaysBlockedSuffix(matchedSuffix)) {
937
+ const protection = findFileProtectionBySuffix(matchedSuffix, projectDir);
938
+ if (protection) {
939
+ const approval = checkApproval('__file__', protection.key);
940
+ if (approval) {
941
+ // Approved — fall through to remaining checks
942
+ } else {
943
+ const approvalMode = protection.config.protection === 'deputy-cto-approval' ? 'deputy-cto' : 'cto';
944
+ const request = createRequest('__file__', protection.key, {}, protection.config.phrase, { approvalMode });
945
+ blockBashWithApprovalRequest(command, rawScanResult.reason, [{ request, protection, filePath: matchedSuffix }]);
946
+ return;
947
+ }
948
+ } else {
949
+ blockBash(command, rawScanResult.reason);
950
+ return;
951
+ }
952
+ } else {
953
+ blockBash(command, rawScanResult.reason);
954
+ return;
955
+ }
956
+ }
957
+ }
958
+
959
+ // Check 3: Credential env var references
960
+ const credentialKeys = loadCredentialKeys(projectDir);
961
+ const envResult = checkBashEnvAccess(command, credentialKeys);
962
+ if (envResult.blocked) {
963
+ blockBash(command, envResult.reason);
964
+ return;
965
+ }
966
+
967
+ // Bash command is allowed
968
+ process.exit(0);
969
+ }
970
+
971
+ // --- Grep tool: check path parameter ---
972
+ if (toolName === 'Grep') {
973
+ const grepPath = toolInput.path || '';
974
+ if (grepPath) {
975
+ const result = checkFilePath(grepPath, projectDir);
976
+ if (result.blocked) {
977
+ const normalizedGrepPath = path.resolve(grepPath);
978
+
979
+ // Always-blocked files: hard deny
980
+ if (isAlwaysBlocked(normalizedGrepPath)) {
981
+ blockRead(grepPath, result.reason);
982
+ return;
983
+ }
984
+
985
+ // Check if file is in the approvable tier
986
+ const protection = findFileProtection(grepPath, projectDir);
987
+ if (protection) {
988
+ const approval = checkApproval('__file__', protection.key);
989
+ if (approval) {
990
+ // Approved — fall through to remaining checks
991
+ } else {
992
+ const approvalMode = protection.config.protection === 'deputy-cto-approval' ? 'deputy-cto' : 'cto';
993
+ const request = createRequest('__file__', protection.key, {}, protection.config.phrase, { approvalMode });
994
+ blockWithApprovalRequest(grepPath, result.reason, request, protection);
995
+ return;
996
+ }
997
+ } else {
998
+ blockRead(grepPath, result.reason);
999
+ return;
1000
+ }
1001
+ }
1002
+ }
1003
+ // Also check glob parameter for protected file patterns
1004
+ const grepGlob = toolInput.glob || '';
1005
+ if (grepGlob) {
1006
+ for (const basename of BLOCKED_BASENAMES) {
1007
+ if (grepGlob.includes(basename)) {
1008
+ blockRead(grepGlob, `Grep glob pattern targets protected file "${basename}"`);
1009
+ return;
1010
+ }
1011
+ }
1012
+ for (const suffix of BLOCKED_PATH_SUFFIXES) {
1013
+ if (grepGlob.includes(suffix)) {
1014
+ blockRead(grepGlob, `Grep glob pattern targets protected path "${suffix}"`);
1015
+ return;
1016
+ }
1017
+ }
1018
+ }
1019
+ // When no path, no glob, and no type filter, Grep searches the entire
1020
+ // directory tree which includes protected credential files.
1021
+ // A type filter (e.g., type: "js") restricts to specific file extensions,
1022
+ // which won't match .env or .mcp.json, so those are safe to allow.
1023
+ const grepType = toolInput.type || '';
1024
+ if (!grepPath && !grepGlob && !grepType) {
1025
+ blockRead('(recursive search)',
1026
+ 'Grep without path, glob, or type would search all files including protected credential files. ' +
1027
+ 'Specify a path (e.g., path: "src/"), glob (e.g., glob: "*.ts"), or type (e.g., type: "js") to restrict the search.');
1028
+ return;
1029
+ }
1030
+ process.exit(0);
1031
+ }
1032
+
1033
+ // --- Glob tool: check path parameter ---
1034
+ if (toolName === 'Glob') {
1035
+ const globPath = toolInput.path || '';
1036
+ if (globPath) {
1037
+ const result = checkFilePath(globPath, projectDir);
1038
+ if (result.blocked) {
1039
+ const normalizedGlobPath = path.resolve(globPath);
1040
+
1041
+ // Always-blocked files: hard deny
1042
+ if (isAlwaysBlocked(normalizedGlobPath)) {
1043
+ blockRead(globPath, result.reason);
1044
+ return;
1045
+ }
1046
+
1047
+ // Check if file is in the approvable tier
1048
+ const protection = findFileProtection(globPath, projectDir);
1049
+ if (protection) {
1050
+ const approval = checkApproval('__file__', protection.key);
1051
+ if (approval) {
1052
+ // Approved — fall through to remaining checks
1053
+ } else {
1054
+ const approvalMode = protection.config.protection === 'deputy-cto-approval' ? 'deputy-cto' : 'cto';
1055
+ const request = createRequest('__file__', protection.key, {}, protection.config.phrase, { approvalMode });
1056
+ blockWithApprovalRequest(globPath, result.reason, request, protection);
1057
+ return;
1058
+ }
1059
+ } else {
1060
+ blockRead(globPath, result.reason);
1061
+ return;
1062
+ }
1063
+ }
1064
+ }
1065
+ // Check pattern for protected file names
1066
+ const globPattern = toolInput.pattern || '';
1067
+ if (globPattern) {
1068
+ for (const basename of BLOCKED_BASENAMES) {
1069
+ if (globPattern.includes(basename)) {
1070
+ blockRead(globPattern, `Glob pattern targets protected file "${basename}"`);
1071
+ return;
1072
+ }
1073
+ }
1074
+ for (const suffix of BLOCKED_PATH_SUFFIXES) {
1075
+ if (globPattern.includes(suffix)) {
1076
+ blockRead(globPattern, `Glob pattern targets protected path "${suffix}"`);
1077
+ return;
1078
+ }
1079
+ }
1080
+ }
1081
+ process.exit(0);
1082
+ }
1083
+
1084
+ // --- Read/Write/Edit tools: check file_path ---
1085
+ const filePath = toolInput.file_path || '';
1086
+
1087
+ if (!filePath) {
1088
+ process.exit(0);
1089
+ }
1090
+
1091
+ // Check if this file is protected
1092
+ const result = checkFilePath(filePath, projectDir);
1093
+
1094
+ if (result.blocked) {
1095
+ const normalizedPath = path.resolve(filePath);
1096
+
1097
+ // Always-blocked files: hard deny, no approval possible
1098
+ if (isAlwaysBlocked(normalizedPath)) {
1099
+ blockRead(filePath, result.reason);
1100
+ return;
1101
+ }
1102
+
1103
+ // Check if file is in the approvable tier
1104
+ const protection = findFileProtection(filePath, projectDir);
1105
+ if (protection) {
1106
+ // Check for existing valid approval
1107
+ const approval = checkApproval('__file__', protection.key);
1108
+ if (approval) {
1109
+ // Approved — allow access
1110
+ process.exit(0);
1111
+ }
1112
+
1113
+ // No approval — create request and block with instructions
1114
+ const approvalMode = protection.config.protection === 'deputy-cto-approval' ? 'deputy-cto' : 'cto';
1115
+ const request = createRequest('__file__', protection.key, {}, protection.config.phrase, { approvalMode });
1116
+ blockWithApprovalRequest(filePath, result.reason, request, protection);
1117
+ return;
1118
+ }
1119
+
1120
+ // Not in approvable config — hard block
1121
+ blockRead(filePath, result.reason);
1122
+ return;
1123
+ }
1124
+
1125
+ // File is allowed
1126
+ process.exit(0);
1127
+ } catch (err) {
1128
+ // G001: fail-closed on parse errors - block the operation
1129
+ console.error(`[credential-file-guard] G001 FAIL-CLOSED: Error parsing input: ${err.message}`);
1130
+ console.log(JSON.stringify({
1131
+ hookSpecificOutput: {
1132
+ hookEventName: 'PreToolUse',
1133
+ permissionDecision: 'deny',
1134
+ permissionDecisionReason: `G001 FAIL-CLOSED: Hook error - ${err.message}`,
1135
+ },
1136
+ }));
1137
+ process.exit(0);
1138
+ }
1139
+ });