@haposoft/cafekit 0.16.0-rc.8 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (323) hide show
  1. package/README.md +117 -28
  2. package/bin/__tests__/codex-hooks-ownership.test.js +239 -0
  3. package/bin/__tests__/codex-hooks.test.js +175 -0
  4. package/bin/__tests__/codex-native.test.js +1882 -70
  5. package/bin/__tests__/develop-contract.test.js +442 -23
  6. package/bin/__tests__/grok-envelope.test.js +271 -0
  7. package/bin/__tests__/grok-platform.test.js +133 -0
  8. package/bin/__tests__/hook-payload.test.js +194 -0
  9. package/bin/__tests__/installer-safety.test.js +3 -3
  10. package/bin/__tests__/omp-bridge.test.js +168 -0
  11. package/bin/__tests__/omp-hooks.test.js +178 -0
  12. package/bin/__tests__/omp-runtime.test.js +150 -0
  13. package/bin/__tests__/optional-skill-inventory.test.js +249 -0
  14. package/bin/__tests__/output-styles.test.js +214 -0
  15. package/bin/__tests__/package-inventory.test.js +1735 -98
  16. package/bin/__tests__/runtime-dir.test.js +158 -0
  17. package/bin/__tests__/runtime-schema.test.js +113 -0
  18. package/bin/__tests__/secret-output-guardrail.test.js +122 -0
  19. package/bin/__tests__/skill-routing-source.test.js +172 -0
  20. package/bin/__tests__/specs-v2-execution-closeout.test.js +28 -1
  21. package/bin/install.js +15 -5
  22. package/bin/lib/codex-install.js +170 -5
  23. package/bin/lib/context.js +64 -3
  24. package/bin/lib/i18n.js +15 -36
  25. package/bin/lib/skill-deps.js +5 -5
  26. package/bin/phases/claude-runtime.js +23 -0
  27. package/bin/phases/claude-settings.js +8 -0
  28. package/bin/phases/codex-hooks.js +156 -0
  29. package/bin/phases/codex-runtime.js +29 -38
  30. package/bin/phases/copy-payload.js +15 -2
  31. package/bin/phases/omp-runtime.js +140 -0
  32. package/bin/phases/root-config.js +3 -0
  33. package/bin/phases/select-platform.js +22 -1
  34. package/bin/phases/select-skill-bundles.js +96 -0
  35. package/bin/phases/skill-inventory.js +71 -0
  36. package/bin/phases/skills-setup.js +9 -2
  37. package/bin/phases/summary.js +0 -4
  38. package/bin/phases/write-metadata.js +8 -5
  39. package/package.json +8 -2
  40. package/src/claude/CLAUDE.md +3 -3
  41. package/src/claude/agents/brainstormer.md +52 -69
  42. package/src/claude/agents/code-auditor.md +32 -14
  43. package/src/claude/agents/debugger.md +35 -20
  44. package/src/claude/agents/deployer.md +12 -5
  45. package/src/claude/agents/docs-keeper.md +11 -1
  46. package/src/claude/agents/implementer.md +25 -13
  47. package/src/claude/agents/inspector.md +6 -3
  48. package/src/claude/agents/project-manager.md +1 -1
  49. package/src/claude/agents/researcher.md +26 -55
  50. package/src/claude/agents/spec-maker.md +12 -7
  51. package/src/claude/agents/strategist.md +52 -0
  52. package/src/claude/agents/test-runner.md +117 -132
  53. package/src/claude/agents/ui-ux-designer.md +1 -1
  54. package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -0
  55. package/src/claude/hooks/__tests__/precompact.test.js +158 -0
  56. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +2 -0
  57. package/src/claude/hooks/__tests__/spec-gate.test.js +348 -3
  58. package/src/claude/hooks/__tests__/state.test.js +7 -0
  59. package/src/claude/hooks/__tests__/statusline.test.js +196 -0
  60. package/src/claude/hooks/agent.cjs +13 -8
  61. package/src/claude/hooks/completion-authority.cjs +10 -4
  62. package/src/claude/hooks/docs-sync.cjs +14 -11
  63. package/src/claude/hooks/inspect-block.cjs +28 -8
  64. package/src/claude/hooks/lib/config.cjs +8 -2
  65. package/src/claude/hooks/lib/counter.cjs +7 -5
  66. package/src/claude/hooks/lib/hook-payload.cjs +160 -0
  67. package/src/claude/hooks/lib/hook-state-dir.cjs +39 -0
  68. package/src/claude/hooks/lib/runtime-dir.cjs +49 -0
  69. package/src/claude/hooks/lib/secret-keywords.cjs +42 -0
  70. package/src/claude/hooks/precompact.cjs +106 -0
  71. package/src/claude/hooks/privacy-block.cjs +4 -2
  72. package/src/claude/hooks/rules.cjs +5 -3
  73. package/src/claude/hooks/secret-output-guardrail.cjs +44 -0
  74. package/src/claude/hooks/semantic-review-authority.cjs +6 -4
  75. package/src/claude/hooks/session.cjs +37 -9
  76. package/src/claude/hooks/spec-gate.cjs +21 -11
  77. package/src/claude/hooks/spec-state.cjs +11 -7
  78. package/src/claude/hooks/state.cjs +7 -5
  79. package/src/claude/hooks/task-scaffold-guard.cjs +30 -9
  80. package/src/claude/hooks/usage.cjs +6 -5
  81. package/src/claude/migration-manifest.json +31 -15
  82. package/src/claude/output-styles/coding-level-0-eli5.md +103 -0
  83. package/src/claude/output-styles/coding-level-1-junior.md +124 -0
  84. package/src/claude/output-styles/coding-level-2-mid.md +146 -0
  85. package/src/claude/output-styles/coding-level-3-senior.md +148 -0
  86. package/src/claude/output-styles/coding-level-4-lead.md +159 -0
  87. package/src/claude/output-styles/coding-level-5-god.md +91 -0
  88. package/src/claude/references/debugger/core-philosophy.md +5 -3
  89. package/src/claude/references/debugger/frontend-verification.md +2 -2
  90. package/src/claude/references/debugger/log-ci-analysis.md +13 -1
  91. package/src/claude/references/debugger/parallel-agent-hydration.md +19 -34
  92. package/src/claude/references/debugger/performance-diagnostics.md +2 -2
  93. package/src/claude/references/debugger/root-cause-tracing.md +37 -6
  94. package/src/claude/references/debugger/side-effect-gate.md +3 -1
  95. package/src/claude/references/debugger/verification-protocol.md +13 -12
  96. package/src/claude/rules/orchestrator.md +7 -1
  97. package/src/claude/rules/process-management.md +39 -0
  98. package/src/claude/rules/review-audit-self-decision.md +48 -0
  99. package/src/claude/rules/skill-domain-routing.md +26 -24
  100. package/src/claude/rules/skill-workflow-routing.md +22 -14
  101. package/src/claude/rules/state-sync.md +7 -1
  102. package/src/claude/rules/workflow.md +5 -3
  103. package/src/claude/runtime.json +1 -0
  104. package/src/claude/runtime.schema.json +205 -0
  105. package/src/claude/scripts/generate-skill-catalog.cjs +156 -122
  106. package/src/claude/scripts/provenance.cjs +29 -9
  107. package/src/claude/scripts/spec-receipt.cjs +65 -4
  108. package/src/claude/scripts/workflow-policy.cjs +1 -1
  109. package/src/claude/settings/settings.json +16 -0
  110. package/src/claude/skills/agent-browser/SKILL.md +1 -1
  111. package/src/claude/skills/ai-multimodal/SKILL.md +1 -1
  112. package/src/claude/skills/brainstorm/SKILL.md +193 -172
  113. package/src/claude/skills/brainstorm/references/question-framework.md +105 -167
  114. package/src/claude/skills/chrome-devtools/SKILL.md +1 -1
  115. package/src/claude/skills/code-review/SKILL.md +43 -25
  116. package/src/claude/skills/code-review/references/spec-compliance-review.md +3 -4
  117. package/src/claude/skills/code-review/references/verification-gate.md +1 -1
  118. package/src/claude/skills/debug/SKILL.md +48 -19
  119. package/src/claude/skills/delegate/SKILL.md +1 -1
  120. package/src/claude/skills/develop/SKILL.md +92 -65
  121. package/src/claude/skills/develop/references/parallel-waves.md +36 -63
  122. package/src/claude/skills/develop/references/quality-gate.md +31 -4
  123. package/src/claude/skills/develop/references/subagent-patterns.md +21 -7
  124. package/src/claude/skills/docs/SKILL.md +42 -23
  125. package/src/claude/skills/docs/references/init-workflow.md +4 -4
  126. package/src/claude/skills/docs/references/reconstruct-workflow.md +6 -6
  127. package/src/claude/skills/docs/references/standard-docs-workflow.md +6 -2
  128. package/src/claude/skills/docs/references/summarize-workflow.md +2 -2
  129. package/src/claude/skills/docs/references/update-workflow.md +9 -7
  130. package/src/claude/skills/docs/templates/requirements-as-is.md +1 -1
  131. package/src/claude/skills/docx/SKILL.md +1 -1
  132. package/src/claude/skills/git/SKILL.md +1 -1
  133. package/src/claude/skills/git/references/commit-protocols.md +1 -1
  134. package/src/claude/skills/git/references/finish-branch.md +1 -1
  135. package/src/claude/skills/hotfix/SKILL.md +159 -170
  136. package/src/claude/skills/hotfix/references/diagnosis-protocol.md +14 -9
  137. package/src/claude/skills/hotfix/references/parallel-patterns.md +27 -13
  138. package/src/claude/skills/hotfix/references/prevention-gate.md +5 -0
  139. package/src/claude/skills/hotfix/references/review-cycle.md +21 -15
  140. package/src/claude/skills/hotfix/references/workflow-specialized.md +53 -21
  141. package/src/claude/skills/inspect/SKILL.md +55 -46
  142. package/src/claude/skills/inspect/references/internal-inspection.md +21 -5
  143. package/src/claude/skills/loop/SKILL.md +114 -0
  144. package/src/claude/skills/loop/references/bounded-loop-protocol.md +104 -0
  145. package/src/claude/skills/loop/references/metric-and-guard-contract.md +76 -0
  146. package/src/claude/skills/pdf/SKILL.md +1 -1
  147. package/src/claude/skills/pptx/SKILL.md +1 -1
  148. package/src/claude/skills/question/SKILL.md +16 -13
  149. package/src/claude/skills/research/SKILL.md +70 -43
  150. package/src/claude/skills/route/SKILL.md +94 -0
  151. package/src/claude/skills/route/references/agent-timing.md +46 -0
  152. package/src/claude/skills/route/references/chaining-patterns.md +53 -0
  153. package/src/claude/skills/route/references/task-taxonomy.md +48 -0
  154. package/src/claude/skills/specs/SKILL.md +51 -71
  155. package/src/claude/skills/specs/references/review.md +11 -8
  156. package/src/claude/skills/specs/references/templates.md +34 -18
  157. package/src/claude/skills/specs/templates/requirements-init.md +1 -1
  158. package/src/claude/skills/specs/templates/research.md +2 -2
  159. package/src/claude/skills/sync/SKILL.md +6 -6
  160. package/src/claude/skills/sync/references/sync-protocols.md +1 -1
  161. package/src/claude/skills/test/SKILL.md +120 -120
  162. package/src/claude/skills/test/references/execution-strategy.md +208 -361
  163. package/src/claude/skills/test/references/failure-triage.md +55 -121
  164. package/src/claude/skills/test/references/test-memory.md +20 -34
  165. package/src/claude/skills/ui-ux-pro-max/SKILL.md +1 -1
  166. package/src/claude/skills/web-testing/SKILL.md +1 -1
  167. package/src/claude/skills/xlsx/SKILL.md +1 -1
  168. package/src/claude/status.cjs +135 -33
  169. package/src/codex/AGENTS.md +14 -4
  170. package/src/codex/hooks/docs-sync.cjs +8 -3
  171. package/src/codex/hooks/lib/hook-context.cjs +18 -4
  172. package/src/codex/hooks/rules.cjs +29 -1
  173. package/src/codex/hooks/secret-output-guardrail.cjs +30 -0
  174. package/src/codex/hooks/spec-gate.cjs +3 -8
  175. package/src/codex/hooks/spec-state.cjs +3 -2
  176. package/src/codex/hooks.json +5 -0
  177. package/src/codex/rules/state-sync.md +7 -1
  178. package/src/codex/runtime.json +2 -0
  179. package/src/common/.env.example +1 -1
  180. package/src/omp/extensions/cafekit-bridge.mjs +233 -0
  181. package/src/omp/hooks/privacy-block.cjs +250 -0
  182. package/src/omp/runtime.json +33 -0
  183. package/bin/lib/rtk-run.js +0 -28
  184. package/bin/phases/setup-rtk.js +0 -159
  185. package/src/claude/skills/backend-development/SKILL.md +0 -101
  186. package/src/claude/skills/backend-development/references/backend-api-design.md +0 -495
  187. package/src/claude/skills/backend-development/references/backend-architecture.md +0 -454
  188. package/src/claude/skills/backend-development/references/backend-authentication.md +0 -338
  189. package/src/claude/skills/backend-development/references/backend-code-quality.md +0 -659
  190. package/src/claude/skills/backend-development/references/backend-debugging.md +0 -904
  191. package/src/claude/skills/backend-development/references/backend-devops.md +0 -491
  192. package/src/claude/skills/backend-development/references/backend-mindset.md +0 -387
  193. package/src/claude/skills/backend-development/references/backend-performance.md +0 -397
  194. package/src/claude/skills/backend-development/references/backend-security.md +0 -342
  195. package/src/claude/skills/backend-development/references/backend-technologies.md +0 -256
  196. package/src/claude/skills/backend-development/references/backend-testing.md +0 -429
  197. package/src/claude/skills/devops/.env.example +0 -76
  198. package/src/claude/skills/devops/SKILL.md +0 -102
  199. package/src/claude/skills/devops/references/browser-rendering.md +0 -305
  200. package/src/claude/skills/devops/references/cloudflare-d1-kv.md +0 -123
  201. package/src/claude/skills/devops/references/cloudflare-platform.md +0 -271
  202. package/src/claude/skills/devops/references/cloudflare-r2-storage.md +0 -280
  203. package/src/claude/skills/devops/references/cloudflare-workers-advanced.md +0 -312
  204. package/src/claude/skills/devops/references/cloudflare-workers-apis.md +0 -309
  205. package/src/claude/skills/devops/references/cloudflare-workers-basics.md +0 -418
  206. package/src/claude/skills/devops/references/docker-basics.md +0 -297
  207. package/src/claude/skills/devops/references/docker-compose.md +0 -292
  208. package/src/claude/skills/devops/references/gcloud-platform.md +0 -297
  209. package/src/claude/skills/devops/references/gcloud-services.md +0 -304
  210. package/src/claude/skills/devops/references/kubernetes-basics.md +0 -95
  211. package/src/claude/skills/devops/references/kubernetes-helm-advanced.md +0 -70
  212. package/src/claude/skills/devops/references/kubernetes-helm.md +0 -81
  213. package/src/claude/skills/devops/references/kubernetes-kubectl.md +0 -74
  214. package/src/claude/skills/devops/references/kubernetes-security-advanced.md +0 -94
  215. package/src/claude/skills/devops/references/kubernetes-security.md +0 -89
  216. package/src/claude/skills/devops/references/kubernetes-troubleshooting-advanced.md +0 -74
  217. package/src/claude/skills/devops/references/kubernetes-troubleshooting.md +0 -49
  218. package/src/claude/skills/devops/references/kubernetes-workflows-advanced.md +0 -75
  219. package/src/claude/skills/devops/references/kubernetes-workflows.md +0 -76
  220. package/src/claude/skills/devops/scripts/cloudflare_deploy.py +0 -269
  221. package/src/claude/skills/devops/scripts/docker_optimize.py +0 -332
  222. package/src/claude/skills/devops/scripts/requirements.txt +0 -20
  223. package/src/claude/skills/devops/scripts/tests/requirements.txt +0 -3
  224. package/src/claude/skills/devops/scripts/tests/test_cloudflare_deploy.py +0 -285
  225. package/src/claude/skills/devops/scripts/tests/test_docker_optimize.py +0 -436
  226. package/src/claude/skills/frontend-design/SKILL.md +0 -135
  227. package/src/claude/skills/frontend-design/references/ai-multimodal-overview.md +0 -165
  228. package/src/claude/skills/frontend-design/references/analysis-best-practices.md +0 -80
  229. package/src/claude/skills/frontend-design/references/analysis-prompts.md +0 -141
  230. package/src/claude/skills/frontend-design/references/analysis-techniques.md +0 -118
  231. package/src/claude/skills/frontend-design/references/animejs.md +0 -396
  232. package/src/claude/skills/frontend-design/references/anti-slop-rules.md +0 -103
  233. package/src/claude/skills/frontend-design/references/asset-generation.md +0 -337
  234. package/src/claude/skills/frontend-design/references/bento-motion-engine.md +0 -142
  235. package/src/claude/skills/frontend-design/references/design-extraction-overview.md +0 -71
  236. package/src/claude/skills/frontend-design/references/extraction-best-practices.md +0 -141
  237. package/src/claude/skills/frontend-design/references/extraction-output-templates.md +0 -162
  238. package/src/claude/skills/frontend-design/references/extraction-prompts.md +0 -127
  239. package/src/claude/skills/frontend-design/references/magicui-components.md +0 -129
  240. package/src/claude/skills/frontend-design/references/performance-guardrails.md +0 -169
  241. package/src/claude/skills/frontend-design/references/premium-design-patterns.md +0 -93
  242. package/src/claude/skills/frontend-design/references/redesign-audit-checklist.md +0 -114
  243. package/src/claude/skills/frontend-design/references/technical-accessibility.md +0 -119
  244. package/src/claude/skills/frontend-design/references/technical-best-practices.md +0 -97
  245. package/src/claude/skills/frontend-design/references/technical-optimization.md +0 -44
  246. package/src/claude/skills/frontend-design/references/technical-overview.md +0 -90
  247. package/src/claude/skills/frontend-design/references/technical-workflows.md +0 -150
  248. package/src/claude/skills/frontend-design/references/visual-analysis-overview.md +0 -95
  249. package/src/claude/skills/frontend-design/references/workflow-3d.md +0 -102
  250. package/src/claude/skills/frontend-design/references/workflow-describe.md +0 -87
  251. package/src/claude/skills/frontend-design/references/workflow-immersive.md +0 -87
  252. package/src/claude/skills/frontend-design/references/workflow-quick.md +0 -57
  253. package/src/claude/skills/frontend-design/references/workflow-screenshot.md +0 -63
  254. package/src/claude/skills/frontend-design/references/workflow-video.md +0 -74
  255. package/src/claude/skills/frontend-development/SKILL.md +0 -406
  256. package/src/claude/skills/frontend-development/resources/common-patterns.md +0 -331
  257. package/src/claude/skills/frontend-development/resources/complete-examples.md +0 -872
  258. package/src/claude/skills/frontend-development/resources/component-patterns.md +0 -502
  259. package/src/claude/skills/frontend-development/resources/data-fetching.md +0 -767
  260. package/src/claude/skills/frontend-development/resources/file-organization.md +0 -502
  261. package/src/claude/skills/frontend-development/resources/loading-and-error-states.md +0 -501
  262. package/src/claude/skills/frontend-development/resources/performance.md +0 -406
  263. package/src/claude/skills/frontend-development/resources/routing-guide.md +0 -364
  264. package/src/claude/skills/frontend-development/resources/styling-guide.md +0 -428
  265. package/src/claude/skills/frontend-development/resources/typescript-standards.md +0 -418
  266. package/src/claude/skills/mobile-development/SKILL.md +0 -218
  267. package/src/claude/skills/mobile-development/references/mobile-android.md +0 -604
  268. package/src/claude/skills/mobile-development/references/mobile-best-practices.md +0 -544
  269. package/src/claude/skills/mobile-development/references/mobile-debugging.md +0 -1089
  270. package/src/claude/skills/mobile-development/references/mobile-frameworks.md +0 -465
  271. package/src/claude/skills/mobile-development/references/mobile-ios.md +0 -496
  272. package/src/claude/skills/mobile-development/references/mobile-mindset.md +0 -542
  273. package/src/claude/skills/react-best-practices/AGENTS.md +0 -2249
  274. package/src/claude/skills/react-best-practices/README.md +0 -123
  275. package/src/claude/skills/react-best-practices/SKILL.md +0 -128
  276. package/src/claude/skills/react-best-practices/metadata.json +0 -15
  277. package/src/claude/skills/react-best-practices/rules/_sections.md +0 -46
  278. package/src/claude/skills/react-best-practices/rules/_template.md +0 -28
  279. package/src/claude/skills/react-best-practices/rules/advanced-event-handler-refs.md +0 -55
  280. package/src/claude/skills/react-best-practices/rules/advanced-use-latest.md +0 -49
  281. package/src/claude/skills/react-best-practices/rules/async-api-routes.md +0 -38
  282. package/src/claude/skills/react-best-practices/rules/async-defer-await.md +0 -80
  283. package/src/claude/skills/react-best-practices/rules/async-dependencies.md +0 -36
  284. package/src/claude/skills/react-best-practices/rules/async-parallel.md +0 -28
  285. package/src/claude/skills/react-best-practices/rules/async-suspense-boundaries.md +0 -99
  286. package/src/claude/skills/react-best-practices/rules/bundle-barrel-imports.md +0 -59
  287. package/src/claude/skills/react-best-practices/rules/bundle-conditional.md +0 -31
  288. package/src/claude/skills/react-best-practices/rules/bundle-defer-third-party.md +0 -49
  289. package/src/claude/skills/react-best-practices/rules/bundle-dynamic-imports.md +0 -35
  290. package/src/claude/skills/react-best-practices/rules/bundle-preload.md +0 -50
  291. package/src/claude/skills/react-best-practices/rules/client-event-listeners.md +0 -74
  292. package/src/claude/skills/react-best-practices/rules/client-swr-dedup.md +0 -56
  293. package/src/claude/skills/react-best-practices/rules/js-batch-dom-css.md +0 -82
  294. package/src/claude/skills/react-best-practices/rules/js-cache-function-results.md +0 -80
  295. package/src/claude/skills/react-best-practices/rules/js-cache-property-access.md +0 -28
  296. package/src/claude/skills/react-best-practices/rules/js-cache-storage.md +0 -70
  297. package/src/claude/skills/react-best-practices/rules/js-combine-iterations.md +0 -32
  298. package/src/claude/skills/react-best-practices/rules/js-early-exit.md +0 -50
  299. package/src/claude/skills/react-best-practices/rules/js-hoist-regexp.md +0 -45
  300. package/src/claude/skills/react-best-practices/rules/js-index-maps.md +0 -37
  301. package/src/claude/skills/react-best-practices/rules/js-length-check-first.md +0 -49
  302. package/src/claude/skills/react-best-practices/rules/js-min-max-loop.md +0 -82
  303. package/src/claude/skills/react-best-practices/rules/js-set-map-lookups.md +0 -24
  304. package/src/claude/skills/react-best-practices/rules/js-tosorted-immutable.md +0 -57
  305. package/src/claude/skills/react-best-practices/rules/rendering-activity.md +0 -26
  306. package/src/claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
  307. package/src/claude/skills/react-best-practices/rules/rendering-conditional-render.md +0 -40
  308. package/src/claude/skills/react-best-practices/rules/rendering-content-visibility.md +0 -38
  309. package/src/claude/skills/react-best-practices/rules/rendering-hoist-jsx.md +0 -46
  310. package/src/claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
  311. package/src/claude/skills/react-best-practices/rules/rendering-svg-precision.md +0 -28
  312. package/src/claude/skills/react-best-practices/rules/rerender-defer-reads.md +0 -39
  313. package/src/claude/skills/react-best-practices/rules/rerender-dependencies.md +0 -45
  314. package/src/claude/skills/react-best-practices/rules/rerender-derived-state.md +0 -29
  315. package/src/claude/skills/react-best-practices/rules/rerender-functional-setstate.md +0 -74
  316. package/src/claude/skills/react-best-practices/rules/rerender-lazy-state-init.md +0 -58
  317. package/src/claude/skills/react-best-practices/rules/rerender-memo.md +0 -44
  318. package/src/claude/skills/react-best-practices/rules/rerender-transitions.md +0 -40
  319. package/src/claude/skills/react-best-practices/rules/server-after-nonblocking.md +0 -73
  320. package/src/claude/skills/react-best-practices/rules/server-cache-lru.md +0 -41
  321. package/src/claude/skills/react-best-practices/rules/server-cache-react.md +0 -26
  322. package/src/claude/skills/react-best-practices/rules/server-parallel-fetching.md +0 -79
  323. package/src/claude/skills/react-best-practices/rules/server-serialization.md +0 -38
@@ -5,6 +5,7 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const {
7
7
  getHookContext,
8
+ hookStateDir,
8
9
  logCrash,
9
10
  readPayload,
10
11
  resolveProjectPath
@@ -15,7 +16,7 @@ function reservationFile(projectRoot, sessionId) {
15
16
  .update(String(sessionId || 'unknown'))
16
17
  .digest('hex')
17
18
  .slice(0, 16);
18
- return path.join(projectRoot, '.codex', 'hooks', '.logs', `rules-${key}.json`);
19
+ return path.join(hookStateDir(projectRoot), `rules-${key}.json`);
19
20
  }
20
21
 
21
22
  /** Reserve one injection slot per session. Returns null on reservation errors. */
@@ -35,6 +36,28 @@ function reserveSession(projectRoot, sessionId) {
35
36
  }
36
37
  }
37
38
 
39
+ /**
40
+ * Body of the output style for `codingLevel`, or null.
41
+ *
42
+ * Codex CLI has no output-style feature, so the style Claude Code would load natively is
43
+ * injected here instead. The rules hook reserves one injection per session, so this costs
44
+ * the same as a host-loaded style rather than repeating every turn.
45
+ */
46
+ function readCodingStyle(projectRoot, level) {
47
+ if (!Number.isInteger(level) || level < 0 || level > 5) return null;
48
+ try {
49
+ const dir = path.join(projectRoot, '.codex', 'output-styles');
50
+ const match = fs.readdirSync(dir).find((name) => name.startsWith(`coding-level-${level}-`));
51
+ if (!match) return null;
52
+ const raw = fs.readFileSync(path.join(dir, match), 'utf8');
53
+ // Strip the frontmatter Claude Code reads; Codex only needs the instructions.
54
+ const body = raw.replace(/^---\n[\s\S]*?\n---\n/, '').trim();
55
+ return body || null;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
38
61
  function readRuntime(projectRoot) {
39
62
  try {
40
63
  const file = path.join(projectRoot, '.codex', 'runtime.json');
@@ -74,6 +97,11 @@ try {
74
97
  `- docs.maxLoc: ${runtime.docs?.maxLoc || 800} lines per doc file`
75
98
  );
76
99
 
100
+ const style = readCodingStyle(projectRoot, runtime.codingLevel);
101
+ if (style) {
102
+ lines.push('', '## Communication style', style);
103
+ }
104
+
77
105
  process.stdout.write(`${lines.join('\n')}\n`);
78
106
  } catch (error) {
79
107
  logCrash('rules', error);
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Copyright (c) 2026 Haposoft. MIT License.
6
+ *
7
+ * UserPromptSubmit hook — secret-output-guardrail.cjs (Codex)
8
+ *
9
+ * Mirror of the Claude hook. The privacy hook gates whether a sensitive file may be
10
+ * read; nothing gated whether an approved value may then be printed into the
11
+ * transcript, where it survives compaction and is replayed on every later turn.
12
+ *
13
+ * Reads the prompt and never echoes it: no prompt text and no matched value is
14
+ * written out, so the guardrail cannot itself become the leak. Fail-open.
15
+ */
16
+
17
+ const { logCrash, readPayload } = require('./lib/hook-context.cjs');
18
+ const { REMINDER, containsSecretKeyword } = require('./lib/secret-keywords.cjs');
19
+
20
+ try {
21
+ const payload = readPayload();
22
+ if (!payload) process.exit(0);
23
+
24
+ const prompt = String(payload.prompt || payload.user_prompt || '');
25
+ if (!containsSecretKeyword(prompt)) process.exit(0);
26
+
27
+ process.stdout.write(`${REMINDER}\n`);
28
+ } catch (error) {
29
+ logCrash('secret-output-guardrail', error);
30
+ }
@@ -6,6 +6,7 @@ const path = require('path');
6
6
  const {
7
7
  atomicWrite,
8
8
  getHookContext,
9
+ hookStateDir,
9
10
  logCrash,
10
11
  readPayload
11
12
  } = require('./lib/hook-context.cjs');
@@ -132,13 +133,7 @@ try {
132
133
  const currentStatuses = processWorkflow
133
134
  ? Object.fromEntries(Object.entries(registry).map(([taskPath, task]) => [taskPath, task?.status || 'pending']))
134
135
  : taskStatusMap(activeSpec);
135
- const cacheFile = path.join(
136
- projectRoot,
137
- '.codex',
138
- 'hooks',
139
- '.logs',
140
- 'spec-gate-last.json'
141
- );
136
+ const cacheFile = path.join(hookStateDir(projectRoot), 'spec-gate-last.json');
142
137
  const cacheExists = fs.existsSync(cacheFile);
143
138
  let cache = {};
144
139
  if (cacheExists) {
@@ -169,7 +164,7 @@ try {
169
164
  if (staleFlashTasks.length > 0) {
170
165
  process.stdout.write(`${JSON.stringify({
171
166
  decision: 'block',
172
- reason: `Completion gate: ${staleFlashTasks.length} task(s) marked done with FLASH_UNVERIFIED (${staleFlashTasks.join(', ')}). Run /hapo:test, then use explicit sync-finalize.`
167
+ reason: `Completion gate: ${staleFlashTasks.length} task(s) marked done with FLASH_UNVERIFIED (${staleFlashTasks.join(', ')}). Run /cf:test, then use explicit sync-finalize.`
173
168
  })}\n`);
174
169
  process.exit(0);
175
170
  }
@@ -7,6 +7,7 @@ const path = require('path');
7
7
  const {
8
8
  atomicWrite,
9
9
  getHookContext,
10
+ hookStateDir,
10
11
  logCrash,
11
12
  readPayload
12
13
  } = require('./lib/hook-context.cjs');
@@ -21,7 +22,7 @@ function cacheFile(projectRoot, sessionId) {
21
22
  .update(String(sessionId || 'unknown'))
22
23
  .digest('hex')
23
24
  .slice(0, 16);
24
- return path.join(projectRoot, '.codex', 'hooks', '.logs', `tollgate-${key}.txt`);
25
+ return path.join(hookStateDir(projectRoot), `tollgate-${key}.txt`);
25
26
  }
26
27
 
27
28
  try {
@@ -155,7 +156,7 @@ try {
155
156
  }
156
157
  lines.push(
157
158
  '- Sync each flat task `Status:` only after verified work; task proof belongs in that task\'s inline `## Receipt`.',
158
- `- Workflow source: \`specs/${active.featureName}/plan.md\`; Stop revalidates every task currently marked done.`,
159
+ `- Workflow source: \`specs/${active.featureName}/plan.md\`; Stop re-checks every done receipt, binding Base/Head to the runtime until the task file is committed, unchanged, and the tree outside the specs root is clean.`,
159
160
  '- Hooks revalidate receipt bytes but never grant approval.'
160
161
  );
161
162
  } else {
@@ -38,6 +38,11 @@
38
38
  "UserPromptSubmit": [
39
39
  {
40
40
  "hooks": [
41
+ {
42
+ "type": "command",
43
+ "command": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/secret-output-guardrail.cjs\"",
44
+ "commandWindows": "node \".codex/hooks/secret-output-guardrail.cjs\""
45
+ },
41
46
  {
42
47
  "type": "command",
43
48
  "command": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/privacy-approval.cjs\"",
@@ -30,7 +30,13 @@ state transition.
30
30
  Set `Status: done` only after the inline Receipt passes the
31
31
  `validateCanonicalReceipt` contract: exact command, `Exit: 0`,
32
32
  `Verification: PASS`, runtime-derived Base and Head values, and a non-empty
33
- fenced block of current command output. Missing, stale, contradictory,
33
+ fenced block of current command output.
34
+
35
+ A done task keeps that binding while its file differs from its committed bytes,
36
+ and is validated on structure alone once the file is committed, unchanged, and the
37
+ tree outside the specs root is clean. The gate detects drift, not invention: a valid
38
+ Base and Head pair costs one command and no verification run, so neither mode proves
39
+ that the command was executed. Missing, stale, contradictory,
34
40
  placeholder, copied, failure, or zero-test evidence leaves the task unfinished.
35
41
 
36
42
  Synchronization never fabricates commands, output, timestamps, completed
@@ -1,4 +1,5 @@
1
1
  {
2
+ "$schema": "./runtime.schema.json",
2
3
  "privacyBlock": true,
3
4
  "inspect": {
4
5
  "enabled": true
@@ -23,6 +24,7 @@
23
24
  "thinkingLanguage": null,
24
25
  "responseLanguage": null
25
26
  },
27
+ "codingLevel": 1,
26
28
  "hooks": {},
27
29
  "assertions": []
28
30
  }
@@ -8,7 +8,7 @@
8
8
  # ============================================
9
9
 
10
10
  # Google Gemini / AI Studio API key
11
- # Required for hapo:test Multimodal Visual checks
11
+ # Required for cf:test Multimodal Visual checks
12
12
  # Get from: https://aistudio.google.com/apikey
13
13
  GEMINI_API_KEY=
14
14
 
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Copyright (c) 2026 Haposoft. MIT License.
3
+ *
4
+ * CafeKit bridge for Oh My Pi.
5
+ *
6
+ * omp has no hooks.json. It loads extensions from `.omp/extensions/` and emits lifecycle
7
+ * events to them. This extension is the only thing that stands between those events and
8
+ * CafeKit's gate scripts in `.omp/hooks/`: it shapes each omp event into the Claude-shaped
9
+ * payload the scripts read, runs the scripts as child processes, and translates their
10
+ * verdicts back into omp's contract.
11
+ *
12
+ * Three facts read from the installed omp binary drive the design:
13
+ * - omp fails CLOSED at `tool_call`: a handler that throws or exceeds
14
+ * `extensionHandlers.toolCallTimeoutMs` (default 30000) is replaced by omp's own
15
+ * `{ block: true, reason: "Extension … timed out" }`. The bridge therefore keeps a
16
+ * shorter budget so a slow gate blocks with its own name instead of a generic reason.
17
+ * - omp's `input` payload is `{ type, text, images, source }` with no session id or cwd.
18
+ * Session-scoped hooks (`rules.cjs`) exit when the id is missing, so the bridge mints one
19
+ * per load and reuses it; omp loads an extension once per session.
20
+ * - `session_stop` carries `stop_hook_active`, the same field `spec-gate.cjs` uses to avoid
21
+ * re-blocking its own continuation. The bridge honours it before dispatching anything.
22
+ *
23
+ * Not mapped, because omp has no such events: SubagentStart (`agent.cjs`) and SubagentStop
24
+ * (`semantic-review-authority.cjs`). Task 04 documents this gap for operators.
25
+ */
26
+
27
+ import { spawn } from 'node:child_process';
28
+ import { randomUUID } from 'node:crypto';
29
+ import { existsSync } from 'node:fs';
30
+ import path from 'node:path';
31
+ import { fileURLToPath } from 'node:url';
32
+
33
+ /** Stay under omp's 30000 ms cut-off so our reason wins over its substitute. */
34
+ export const BRIDGE_TIMEOUT_MS = 8000;
35
+
36
+ /**
37
+ * omp event → the CafeKit hooks registered for the equivalent Claude event.
38
+ * Mirrors src/claude/settings/settings.json; `omp-bridge.test.js` fails if the two drift.
39
+ */
40
+ export const DISPATCH = {
41
+ session_start: { event: 'SessionStart', hooks: ['session.cjs', 'docs-sync.cjs', 'state.cjs'] },
42
+ session_compact: { event: 'SessionStart', hooks: ['session.cjs'], source: 'compact' },
43
+ session_before_compact: { event: 'PreCompact', hooks: ['precompact.cjs'] },
44
+ input: { event: 'UserPromptSubmit', hooks: ['secret-output-guardrail.cjs', 'rules.cjs', 'completion-authority.cjs', 'spec-state.cjs', 'usage.cjs'] },
45
+ tool_call: { event: 'PreToolUse', hooks: ['privacy-block.cjs', 'inspect-block.cjs', 'task-scaffold-guard.cjs'] },
46
+ tool_result: { event: 'PostToolUse', hooks: ['state.cjs', 'usage.cjs'] },
47
+ session_stop: { event: 'Stop', hooks: ['spec-gate.cjs', 'completion-authority.cjs', 'state.cjs'] },
48
+ };
49
+
50
+ /** One id per extension load. omp loads an extension once per session, so this is per-session. */
51
+ const SESSION_ID = `omp-${randomUUID()}`;
52
+
53
+ export function hooksDir() {
54
+ if (process.env.CAFEKIT_OMP_HOOKS_DIR) return process.env.CAFEKIT_OMP_HOOKS_DIR;
55
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'hooks');
56
+ }
57
+
58
+ /** Build the Claude-shaped payload a CafeKit hook reads on stdin. */
59
+ export function shapePayload(kind, event, ctx) {
60
+ const entry = DISPATCH[kind];
61
+ const cwd = ctx?.cwd || process.cwd();
62
+ const payload = {
63
+ hook_event_name: entry.event,
64
+ session_id: event?.session_id || SESSION_ID,
65
+ cwd,
66
+ };
67
+ switch (kind) {
68
+ case 'session_start':
69
+ payload.source = event?.reason === 'resume' ? 'resume' : 'startup';
70
+ break;
71
+ case 'session_compact':
72
+ payload.source = 'compact';
73
+ break;
74
+ case 'session_before_compact':
75
+ payload.trigger = event?.reason === 'manual' ? 'manual' : 'auto';
76
+ break;
77
+ case 'input':
78
+ payload.prompt = typeof event?.text === 'string' ? event.text : '';
79
+ break;
80
+ case 'tool_call':
81
+ payload.tool_name = event?.toolName ?? event?.tool_name ?? '';
82
+ payload.tool_input = event?.input ?? {};
83
+ payload.tool_use_id = event?.toolCallId ?? null;
84
+ break;
85
+ case 'tool_result':
86
+ payload.tool_name = event?.toolName ?? event?.tool_name ?? '';
87
+ payload.tool_input = event?.input ?? {};
88
+ payload.tool_response = { content: event?.content ?? [], isError: event?.isError === true };
89
+ payload.tool_use_id = event?.toolCallId ?? null;
90
+ break;
91
+ case 'session_stop':
92
+ payload.stop_hook_active = event?.stop_hook_active === true;
93
+ payload.last_assistant_message = event?.last_assistant_message ?? null;
94
+ break;
95
+ default:
96
+ break;
97
+ }
98
+ return payload;
99
+ }
100
+
101
+ /**
102
+ * Read a hook's verdict. No CafeKit hook writes a block reason to stderr; the three
103
+ * mechanisms in use are stdout JSON `decision:"block"`, stdout JSON
104
+ * `hookSpecificOutput.permissionDecision` of `deny` (or `ask`, which omp cannot express),
105
+ * and exit 2 with the reason on stdout. Anything else is an allow, and any stdout text
106
+ * that is not a verdict is context to inject.
107
+ */
108
+ export function interpretVerdict({ code, stdout = '', stderr = '' }, hookName) {
109
+ const out = String(stdout).trim();
110
+ const lines = out ? out.split('\n') : [];
111
+ const inject = [];
112
+ let block = null;
113
+
114
+ for (const line of lines) {
115
+ let parsed = null;
116
+ try { parsed = JSON.parse(line); } catch { parsed = null; }
117
+ if (!parsed || typeof parsed !== 'object') { inject.push(line); continue; }
118
+
119
+ if (parsed.decision === 'block') {
120
+ block = block || { reason: parsed.reason || `Blocked by ${hookName}` };
121
+ continue;
122
+ }
123
+ const decision = parsed.hookSpecificOutput?.permissionDecision;
124
+ if (decision === 'deny' || decision === 'ask') {
125
+ block = block || { reason: parsed.hookSpecificOutput.permissionDecisionReason || `Blocked by ${hookName}` };
126
+ continue;
127
+ }
128
+ const extra = parsed.hookSpecificOutput?.additionalContext;
129
+ if (typeof extra === 'string' && extra) inject.push(extra);
130
+ }
131
+
132
+ if (!block && code === 2) {
133
+ block = { reason: out || String(stderr).trim() || `Blocked by ${hookName}` };
134
+ }
135
+ return { block: Boolean(block), reason: block?.reason, inject: inject.join('\n').trim() };
136
+ }
137
+
138
+ /** Run one hook as a child process with a hard budget. Never throws. */
139
+ export function dispatchHook(hookPath, payload, { cwd, timeoutMs = BRIDGE_TIMEOUT_MS } = {}) {
140
+ return new Promise((resolve) => {
141
+ let child;
142
+ try {
143
+ child = spawn(process.execPath, [hookPath], {
144
+ cwd,
145
+ env: { ...process.env, PROJECT_ROOT: cwd },
146
+ stdio: ['pipe', 'pipe', 'pipe'],
147
+ });
148
+ } catch (error) {
149
+ resolve({ code: null, stdout: '', stderr: String(error?.message || error), timedOut: false, failed: true });
150
+ return;
151
+ }
152
+ const out = []; const err = [];
153
+ let settled = false;
154
+ const finish = (result) => { if (!settled) { settled = true; resolve(result); } };
155
+ const timer = setTimeout(() => {
156
+ try { child.kill('SIGKILL'); } catch { /* already gone */ }
157
+ finish({ code: null, stdout: Buffer.concat(out).toString('utf8'), stderr: Buffer.concat(err).toString('utf8'), timedOut: true, failed: true });
158
+ }, timeoutMs);
159
+ child.stdout.on('data', (c) => out.push(c));
160
+ child.stderr.on('data', (c) => err.push(c));
161
+ child.on('error', (error) => { clearTimeout(timer); finish({ code: null, stdout: '', stderr: String(error?.message || error), timedOut: false, failed: true }); });
162
+ child.on('close', (code) => { clearTimeout(timer); finish({ code, stdout: Buffer.concat(out).toString('utf8'), stderr: Buffer.concat(err).toString('utf8'), timedOut: false, failed: false }); });
163
+ try { child.stdin.write(JSON.stringify(payload)); child.stdin.end(); } catch { /* close handler still runs */ }
164
+ });
165
+ }
166
+
167
+ /**
168
+ * Dispatch every hook for an omp event and fold their verdicts. The first block wins and
169
+ * short-circuits; injected context from allowing hooks is concatenated. A hook that fails
170
+ * or times out on a gating event counts as a block naming that hook, matching omp's own
171
+ * fail-closed posture rather than fighting it.
172
+ */
173
+ export async function runEvent(kind, event, ctx, { timeoutMs = BRIDGE_TIMEOUT_MS, dir = hooksDir() } = {}) {
174
+ const entry = DISPATCH[kind];
175
+ if (!entry) return { block: false, inject: '' };
176
+
177
+ // A Stop that omp re-fires after our own block must not block again (infinite loop).
178
+ if (kind === 'session_stop' && event?.stop_hook_active === true) return { block: false, inject: '', skipped: 'stop_hook_active' };
179
+
180
+ const gating = kind === 'tool_call' || kind === 'session_stop';
181
+ const payload = shapePayload(kind, event, ctx);
182
+ const injections = [];
183
+
184
+ for (const hookName of entry.hooks) {
185
+ const hookPath = path.join(dir, hookName);
186
+ if (!existsSync(hookPath)) {
187
+ // A missing gate must be loud on gating events; silently allowing is the worst outcome.
188
+ if (gating) return { block: true, reason: `CafeKit gate ${hookName} is not installed under ${dir}`, hook: hookName };
189
+ continue;
190
+ }
191
+ const result = await dispatchHook(hookPath, payload, { cwd: payload.cwd, timeoutMs });
192
+ if (result.failed) {
193
+ if (gating) {
194
+ const why = result.timedOut ? `exceeded ${timeoutMs} ms` : (result.stderr.trim() || 'failed to run');
195
+ return { block: true, reason: `CafeKit gate ${hookName} ${why}`, hook: hookName };
196
+ }
197
+ continue;
198
+ }
199
+ const verdict = interpretVerdict(result, hookName);
200
+ if (verdict.block) return { block: true, reason: verdict.reason, hook: hookName };
201
+ if (verdict.inject) injections.push(verdict.inject);
202
+ }
203
+ return { block: false, inject: injections.join('\n\n') };
204
+ }
205
+
206
+ /** Extension entry point: omp calls this once with its ExtensionAPI. */
207
+ export default function cafekitBridge(pi) {
208
+ const opts = {};
209
+
210
+ pi.on('session_start', async (event, ctx) => { await runEvent('session_start', event, ctx, opts); });
211
+ pi.on('session_compact', async (event, ctx) => { await runEvent('session_compact', event, ctx, opts); });
212
+ pi.on('session_before_compact', async (event, ctx) => { await runEvent('session_before_compact', event, ctx, opts); });
213
+ pi.on('tool_result', async (event, ctx) => { await runEvent('tool_result', event, ctx, opts); });
214
+
215
+ pi.on('tool_call', async (event, ctx) => {
216
+ const r = await runEvent('tool_call', event, ctx, opts);
217
+ return r.block ? { block: true, reason: r.reason } : undefined;
218
+ });
219
+
220
+ // omp cannot block a prompt, so a hook's block reason on input is surfaced as context.
221
+ pi.on('input', async (event, ctx) => {
222
+ const r = await runEvent('input', event, ctx, opts);
223
+ const extra = [r.block ? r.reason : '', r.inject].filter(Boolean).join('\n\n');
224
+ if (!extra) return { action: 'continue' };
225
+ return { action: 'transform', text: `${event.text}\n\n${extra}`, images: event.images };
226
+ });
227
+
228
+ pi.on('session_stop', async (event, ctx) => {
229
+ const r = await runEvent('session_stop', event, ctx, opts);
230
+ if (r.block) return { decision: 'block', reason: r.reason };
231
+ return r.inject ? { continue: true, additionalContext: r.inject } : undefined;
232
+ });
233
+ }
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Copyright (c) 2026 Haposoft. MIT License.
4
+ *
5
+ * PreToolUse Hook — privacy-block.cjs
6
+ *
7
+ * Claude Code CLI privacy gate for sensitive files.
8
+ *
9
+ * Runtime contract:
10
+ * - Sensitive access returns Claude Code's native PreToolUse "ask" decision
11
+ * - The same decision applies to direct tools and Bash commands
12
+ * - Symlinks are classified by their resolved target before their alias
13
+ *
14
+ * Exit: 0 (the JSON hook output carries the permission decision)
15
+ */
16
+
17
+ try {
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { commandAccess } = require('./lib/privacy-command-analysis.cjs');
21
+
22
+ const RESTRICTED_PATTERNS = [
23
+ /^\.env(?:[.\[*?{]|$)/i,
24
+ /^credentials/i,
25
+ /secrets?\.(ya?ml|json)$/i,
26
+ /\.pem$/i,
27
+ /\.key$/i,
28
+ /\.p12$/i,
29
+ /\.pfx$/i,
30
+ /^id_(rsa|ed25519|ecdsa|dsa)$/i,
31
+ /\.netrc$/i,
32
+ /\.pgpass$/i,
33
+ /kubeconfig/i,
34
+ /\.keystore$/i,
35
+ /\.jks$/i,
36
+ /auth\.json$/i,
37
+ /token(s)?\.json$/i
38
+ ];
39
+
40
+ const ALLOWED_EXEMPTIONS = [
41
+ /\.env\.(example|sample|template|test)$/i
42
+ ];
43
+
44
+ const { runtimeDirName, runtimeDir, runtimePath } = require('./lib/runtime-dir.cjs');
45
+ function readRuntime(cwd) {
46
+ try {
47
+ const file = runtimePath(cwd, 'runtime.json');
48
+ return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : {};
49
+ } catch {
50
+ return {};
51
+ }
52
+ }
53
+
54
+ function isSafe(filePath) {
55
+ const base = path.basename(filePath);
56
+ return ALLOWED_EXEMPTIONS.some((rule) => rule.test(base) || rule.test(filePath));
57
+ }
58
+
59
+ function isSensitive(filePath) {
60
+ const base = path.basename(filePath);
61
+ return RESTRICTED_PATTERNS.some((rule) => rule.test(base) || rule.test(filePath));
62
+ }
63
+
64
+ /**
65
+ * Resolve a symlink to its real target so a harmless-looking name that points
66
+ * at a sensitive file cannot slip through the basename check. Returns null
67
+ * when the path cannot be resolved (missing file, broken link) — fail-open to
68
+ * the original-path check in that case. See isSymlinkToSensitive for dangling handling.
69
+ */
70
+ function resolveTarget(filePath, cwd) {
71
+ try {
72
+ const requested = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
73
+ return fs.realpathSync(requested);
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * When realpath fails, check whether the requested path is a symlink (or chain)
81
+ * whose lexical target is sensitive. Dangling link to protected target -> true (fail-closed ask);
82
+ * dangling benign link or link to allowed example/template -> false (allow).
83
+ * Limit hops to avoid loops; malformed/looping -> fail closed.
84
+ * TOCTOU note: check and use are not atomic; target could be swapped between this check and the actual read.
85
+ * This is a best-effort guardrail, not an arbitrary-shell security boundary. For strong isolation use OS sandbox.
86
+ */
87
+ function isSymlinkToSensitive(filePath, cwd) {
88
+ try {
89
+ const requested = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
90
+ let current = requested;
91
+ let hops = 0;
92
+ const MAX_HOPS = 8;
93
+ while (hops < MAX_HOPS) {
94
+ let stat;
95
+ try {
96
+ stat = fs.lstatSync(current);
97
+ } catch {
98
+ return false;
99
+ }
100
+ if (!stat.isSymbolicLink()) return false;
101
+ let linkTarget;
102
+ try {
103
+ linkTarget = fs.readlinkSync(current);
104
+ } catch {
105
+ return true;
106
+ }
107
+ if (isSensitive(linkTarget) && !isSafe(linkTarget)) return true;
108
+ if (isSafe(linkTarget)) return false;
109
+ const next = path.isAbsolute(linkTarget) ? linkTarget : path.resolve(path.dirname(current), linkTarget);
110
+ if (next === current) return true;
111
+ try {
112
+ const real = fs.realpathSync(next);
113
+ if (isSensitive(real) && !isSafe(real)) return true;
114
+ if (isSafe(real)) return false;
115
+ return false;
116
+ } catch {
117
+ current = next;
118
+ hops += 1;
119
+ continue;
120
+ }
121
+ }
122
+ return true;
123
+ } catch {
124
+ return true;
125
+ }
126
+ }
127
+
128
+ function extractBashPaths(command) {
129
+ // The analyzer parses the command and returns the operands of every reading
130
+ // command it recognises, nested shells and `find -exec` included. Treating
131
+ // every unresolved expansion as sensitive, as this did before, asked on
132
+ // `cat $FILE`, `wc -l *.cjs` and `cat ~/.zshrc` alike. Noise is what trains
133
+ // people to click through the prompt that matters.
134
+ // A heredoc whose delimiter is quoted (<<'BODY') is inert: the shell
135
+ // expands nothing inside it, so no word in the body is ever read. The
136
+ // analyzer has no notion of heredocs and parses the body as shell, so prose
137
+ // that quotes a command -- a PR body carrying `cat .env` in backticks --
138
+ // came back as a real command substitution. Blank those bodies out first.
139
+ // An unquoted delimiter (<<BODY) does expand, so it is left to the
140
+ // analyzer.
141
+ const scrubbed = String(command).replace(
142
+ /(<<-?[ \t]*)(['"])([A-Za-z_]\w*)\2[^\n]*\n[\s\S]*?\n[ \t]*\3\b/g,
143
+ '$1$2$3$2'
144
+ );
145
+
146
+ const paths = new Set(commandAccess(scrubbed).paths);
147
+
148
+ // Two shapes the analyzer cannot return. It drops any word carrying an
149
+ // expansion, so `~/.ssh/id_rsa` and `certs/*.pem` still have to be matched
150
+ // by name. And it only walks commands it recognises, so an unknown tool
151
+ // handed a secret through a file-shaped option, such as
152
+ // `docker compose --env-file .env`, is invisible to it.
153
+ const FILE_OPTION = /^--?[\w-]*(?:file|config|key|cert|identity)$/i;
154
+ const tokens = scrubbed.split(/[\s|;&<>"'()`]+/);
155
+ for (let index = 0; index < tokens.length; index += 1) {
156
+ const token = tokens[index];
157
+ const shaped = /^~|[*?[{]/.test(token);
158
+ const optionFed = index > 0 && FILE_OPTION.test(tokens[index - 1]);
159
+ if (!shaped && !optionFed) continue;
160
+ const value = token.replace(/^~/, '').replace(/^["'()`]+|["'(),]+$/g, '');
161
+ if (value && isSensitive(value) && !isSafe(value)) paths.add(value);
162
+ }
163
+
164
+ return [...paths];
165
+ }
166
+
167
+ function extractPaths(toolName, input) {
168
+ const paths = [];
169
+ if (!input) return paths;
170
+
171
+ for (const key of ['file_path', 'path']) {
172
+ if (typeof input[key] === 'string' && input[key].trim()) {
173
+ paths.push(input[key].trim());
174
+ }
175
+ }
176
+
177
+ for (const key of ['paths', 'search_paths']) {
178
+ if (Array.isArray(input[key])) {
179
+ paths.push(...input[key].filter(Boolean));
180
+ }
181
+ }
182
+
183
+ if (toolName === 'Bash' && typeof input.command === 'string') {
184
+ paths.push(...extractBashPaths(input.command));
185
+ }
186
+
187
+ return paths.filter(Boolean);
188
+ }
189
+
190
+ function askForPermission(filePath) {
191
+ const basename = path.basename(filePath);
192
+ process.stdout.write(JSON.stringify({
193
+ hookSpecificOutput: {
194
+ hookEventName: 'PreToolUse',
195
+ // omp has no ask state on tool_call, so an ask under Claude becomes a denial here.
196
+ permissionDecision: 'deny',
197
+ permissionDecisionReason: `Sensitive file access requires approval: ${basename}`
198
+ }
199
+ }) + '\n');
200
+ }
201
+
202
+ const stdin = fs.readFileSync(0, 'utf8').trim();
203
+ if (!stdin) process.exit(0);
204
+
205
+ const { normalizeHookPayload } = require('./lib/hook-payload.cjs');
206
+ const data = normalizeHookPayload(JSON.parse(stdin));
207
+ const toolName = data.tool_name || '';
208
+ const toolInput = data.tool_input || {};
209
+ const cwd = typeof data.cwd === 'string' && data.cwd.trim() ? data.cwd : process.cwd();
210
+ const runtime = readRuntime(cwd);
211
+
212
+ if (runtime.privacyBlock === false) process.exit(0);
213
+
214
+ const paths = extractPaths(toolName, toolInput);
215
+ if (!paths.length) process.exit(0);
216
+
217
+ for (const filePath of paths) {
218
+ const target = resolveTarget(filePath, cwd);
219
+
220
+ // The real target is authoritative: an exempt-looking alias must not hide
221
+ // a sensitive target, while a real .env.example target remains safe.
222
+ if (target) {
223
+ if (isSensitive(target) && !isSafe(target)) {
224
+ askForPermission(filePath);
225
+ process.exit(0);
226
+ }
227
+ if (isSafe(target)) continue;
228
+ } else {
229
+ if (isSymlinkToSensitive(filePath, cwd)) {
230
+ askForPermission(filePath);
231
+ process.exit(0);
232
+ }
233
+ }
234
+
235
+ if (isSafe(filePath) || !isSensitive(filePath)) continue;
236
+ askForPermission(filePath);
237
+ process.exit(0);
238
+ }
239
+
240
+ process.exit(0);
241
+ } catch (_) {
242
+ process.stdout.write(JSON.stringify({
243
+ hookSpecificOutput: {
244
+ hookEventName: 'PreToolUse',
245
+ permissionDecision: 'deny',
246
+ permissionDecisionReason: 'Sensitive access could not be evaluated safely.'
247
+ }
248
+ }) + '\n');
249
+ process.exit(0);
250
+ }