@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.
- package/README.md +117 -28
- package/bin/__tests__/codex-hooks-ownership.test.js +239 -0
- package/bin/__tests__/codex-hooks.test.js +175 -0
- package/bin/__tests__/codex-native.test.js +1882 -70
- package/bin/__tests__/develop-contract.test.js +442 -23
- package/bin/__tests__/grok-envelope.test.js +271 -0
- package/bin/__tests__/grok-platform.test.js +133 -0
- package/bin/__tests__/hook-payload.test.js +194 -0
- package/bin/__tests__/installer-safety.test.js +3 -3
- package/bin/__tests__/omp-bridge.test.js +168 -0
- package/bin/__tests__/omp-hooks.test.js +178 -0
- package/bin/__tests__/omp-runtime.test.js +150 -0
- package/bin/__tests__/optional-skill-inventory.test.js +249 -0
- package/bin/__tests__/output-styles.test.js +214 -0
- package/bin/__tests__/package-inventory.test.js +1735 -98
- package/bin/__tests__/runtime-dir.test.js +158 -0
- package/bin/__tests__/runtime-schema.test.js +113 -0
- package/bin/__tests__/secret-output-guardrail.test.js +122 -0
- package/bin/__tests__/skill-routing-source.test.js +172 -0
- package/bin/__tests__/specs-v2-execution-closeout.test.js +28 -1
- package/bin/install.js +15 -5
- package/bin/lib/codex-install.js +170 -5
- package/bin/lib/context.js +64 -3
- package/bin/lib/i18n.js +15 -36
- package/bin/lib/skill-deps.js +5 -5
- package/bin/phases/claude-runtime.js +23 -0
- package/bin/phases/claude-settings.js +8 -0
- package/bin/phases/codex-hooks.js +156 -0
- package/bin/phases/codex-runtime.js +29 -38
- package/bin/phases/copy-payload.js +15 -2
- package/bin/phases/omp-runtime.js +140 -0
- package/bin/phases/root-config.js +3 -0
- package/bin/phases/select-platform.js +22 -1
- package/bin/phases/select-skill-bundles.js +96 -0
- package/bin/phases/skill-inventory.js +71 -0
- package/bin/phases/skills-setup.js +9 -2
- package/bin/phases/summary.js +0 -4
- package/bin/phases/write-metadata.js +8 -5
- package/package.json +8 -2
- package/src/claude/CLAUDE.md +3 -3
- package/src/claude/agents/brainstormer.md +52 -69
- package/src/claude/agents/code-auditor.md +32 -14
- package/src/claude/agents/debugger.md +35 -20
- package/src/claude/agents/deployer.md +12 -5
- package/src/claude/agents/docs-keeper.md +11 -1
- package/src/claude/agents/implementer.md +25 -13
- package/src/claude/agents/inspector.md +6 -3
- package/src/claude/agents/project-manager.md +1 -1
- package/src/claude/agents/researcher.md +26 -55
- package/src/claude/agents/spec-maker.md +12 -7
- package/src/claude/agents/strategist.md +52 -0
- package/src/claude/agents/test-runner.md +117 -132
- package/src/claude/agents/ui-ux-designer.md +1 -1
- package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -0
- package/src/claude/hooks/__tests__/precompact.test.js +158 -0
- package/src/claude/hooks/__tests__/semantic-review-authority.test.js +2 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +348 -3
- package/src/claude/hooks/__tests__/state.test.js +7 -0
- package/src/claude/hooks/__tests__/statusline.test.js +196 -0
- package/src/claude/hooks/agent.cjs +13 -8
- package/src/claude/hooks/completion-authority.cjs +10 -4
- package/src/claude/hooks/docs-sync.cjs +14 -11
- package/src/claude/hooks/inspect-block.cjs +28 -8
- package/src/claude/hooks/lib/config.cjs +8 -2
- package/src/claude/hooks/lib/counter.cjs +7 -5
- package/src/claude/hooks/lib/hook-payload.cjs +160 -0
- package/src/claude/hooks/lib/hook-state-dir.cjs +39 -0
- package/src/claude/hooks/lib/runtime-dir.cjs +49 -0
- package/src/claude/hooks/lib/secret-keywords.cjs +42 -0
- package/src/claude/hooks/precompact.cjs +106 -0
- package/src/claude/hooks/privacy-block.cjs +4 -2
- package/src/claude/hooks/rules.cjs +5 -3
- package/src/claude/hooks/secret-output-guardrail.cjs +44 -0
- package/src/claude/hooks/semantic-review-authority.cjs +6 -4
- package/src/claude/hooks/session.cjs +37 -9
- package/src/claude/hooks/spec-gate.cjs +21 -11
- package/src/claude/hooks/spec-state.cjs +11 -7
- package/src/claude/hooks/state.cjs +7 -5
- package/src/claude/hooks/task-scaffold-guard.cjs +30 -9
- package/src/claude/hooks/usage.cjs +6 -5
- package/src/claude/migration-manifest.json +31 -15
- package/src/claude/output-styles/coding-level-0-eli5.md +103 -0
- package/src/claude/output-styles/coding-level-1-junior.md +124 -0
- package/src/claude/output-styles/coding-level-2-mid.md +146 -0
- package/src/claude/output-styles/coding-level-3-senior.md +148 -0
- package/src/claude/output-styles/coding-level-4-lead.md +159 -0
- package/src/claude/output-styles/coding-level-5-god.md +91 -0
- package/src/claude/references/debugger/core-philosophy.md +5 -3
- package/src/claude/references/debugger/frontend-verification.md +2 -2
- package/src/claude/references/debugger/log-ci-analysis.md +13 -1
- package/src/claude/references/debugger/parallel-agent-hydration.md +19 -34
- package/src/claude/references/debugger/performance-diagnostics.md +2 -2
- package/src/claude/references/debugger/root-cause-tracing.md +37 -6
- package/src/claude/references/debugger/side-effect-gate.md +3 -1
- package/src/claude/references/debugger/verification-protocol.md +13 -12
- package/src/claude/rules/orchestrator.md +7 -1
- package/src/claude/rules/process-management.md +39 -0
- package/src/claude/rules/review-audit-self-decision.md +48 -0
- package/src/claude/rules/skill-domain-routing.md +26 -24
- package/src/claude/rules/skill-workflow-routing.md +22 -14
- package/src/claude/rules/state-sync.md +7 -1
- package/src/claude/rules/workflow.md +5 -3
- package/src/claude/runtime.json +1 -0
- package/src/claude/runtime.schema.json +205 -0
- package/src/claude/scripts/generate-skill-catalog.cjs +156 -122
- package/src/claude/scripts/provenance.cjs +29 -9
- package/src/claude/scripts/spec-receipt.cjs +65 -4
- package/src/claude/scripts/workflow-policy.cjs +1 -1
- package/src/claude/settings/settings.json +16 -0
- package/src/claude/skills/agent-browser/SKILL.md +1 -1
- package/src/claude/skills/ai-multimodal/SKILL.md +1 -1
- package/src/claude/skills/brainstorm/SKILL.md +193 -172
- package/src/claude/skills/brainstorm/references/question-framework.md +105 -167
- package/src/claude/skills/chrome-devtools/SKILL.md +1 -1
- package/src/claude/skills/code-review/SKILL.md +43 -25
- package/src/claude/skills/code-review/references/spec-compliance-review.md +3 -4
- package/src/claude/skills/code-review/references/verification-gate.md +1 -1
- package/src/claude/skills/debug/SKILL.md +48 -19
- package/src/claude/skills/delegate/SKILL.md +1 -1
- package/src/claude/skills/develop/SKILL.md +92 -65
- package/src/claude/skills/develop/references/parallel-waves.md +36 -63
- package/src/claude/skills/develop/references/quality-gate.md +31 -4
- package/src/claude/skills/develop/references/subagent-patterns.md +21 -7
- package/src/claude/skills/docs/SKILL.md +42 -23
- package/src/claude/skills/docs/references/init-workflow.md +4 -4
- package/src/claude/skills/docs/references/reconstruct-workflow.md +6 -6
- package/src/claude/skills/docs/references/standard-docs-workflow.md +6 -2
- package/src/claude/skills/docs/references/summarize-workflow.md +2 -2
- package/src/claude/skills/docs/references/update-workflow.md +9 -7
- package/src/claude/skills/docs/templates/requirements-as-is.md +1 -1
- package/src/claude/skills/docx/SKILL.md +1 -1
- package/src/claude/skills/git/SKILL.md +1 -1
- package/src/claude/skills/git/references/commit-protocols.md +1 -1
- package/src/claude/skills/git/references/finish-branch.md +1 -1
- package/src/claude/skills/hotfix/SKILL.md +159 -170
- package/src/claude/skills/hotfix/references/diagnosis-protocol.md +14 -9
- package/src/claude/skills/hotfix/references/parallel-patterns.md +27 -13
- package/src/claude/skills/hotfix/references/prevention-gate.md +5 -0
- package/src/claude/skills/hotfix/references/review-cycle.md +21 -15
- package/src/claude/skills/hotfix/references/workflow-specialized.md +53 -21
- package/src/claude/skills/inspect/SKILL.md +55 -46
- package/src/claude/skills/inspect/references/internal-inspection.md +21 -5
- package/src/claude/skills/loop/SKILL.md +114 -0
- package/src/claude/skills/loop/references/bounded-loop-protocol.md +104 -0
- package/src/claude/skills/loop/references/metric-and-guard-contract.md +76 -0
- package/src/claude/skills/pdf/SKILL.md +1 -1
- package/src/claude/skills/pptx/SKILL.md +1 -1
- package/src/claude/skills/question/SKILL.md +16 -13
- package/src/claude/skills/research/SKILL.md +70 -43
- package/src/claude/skills/route/SKILL.md +94 -0
- package/src/claude/skills/route/references/agent-timing.md +46 -0
- package/src/claude/skills/route/references/chaining-patterns.md +53 -0
- package/src/claude/skills/route/references/task-taxonomy.md +48 -0
- package/src/claude/skills/specs/SKILL.md +51 -71
- package/src/claude/skills/specs/references/review.md +11 -8
- package/src/claude/skills/specs/references/templates.md +34 -18
- package/src/claude/skills/specs/templates/requirements-init.md +1 -1
- package/src/claude/skills/specs/templates/research.md +2 -2
- package/src/claude/skills/sync/SKILL.md +6 -6
- package/src/claude/skills/sync/references/sync-protocols.md +1 -1
- package/src/claude/skills/test/SKILL.md +120 -120
- package/src/claude/skills/test/references/execution-strategy.md +208 -361
- package/src/claude/skills/test/references/failure-triage.md +55 -121
- package/src/claude/skills/test/references/test-memory.md +20 -34
- package/src/claude/skills/ui-ux-pro-max/SKILL.md +1 -1
- package/src/claude/skills/web-testing/SKILL.md +1 -1
- package/src/claude/skills/xlsx/SKILL.md +1 -1
- package/src/claude/status.cjs +135 -33
- package/src/codex/AGENTS.md +14 -4
- package/src/codex/hooks/docs-sync.cjs +8 -3
- package/src/codex/hooks/lib/hook-context.cjs +18 -4
- package/src/codex/hooks/rules.cjs +29 -1
- package/src/codex/hooks/secret-output-guardrail.cjs +30 -0
- package/src/codex/hooks/spec-gate.cjs +3 -8
- package/src/codex/hooks/spec-state.cjs +3 -2
- package/src/codex/hooks.json +5 -0
- package/src/codex/rules/state-sync.md +7 -1
- package/src/codex/runtime.json +2 -0
- package/src/common/.env.example +1 -1
- package/src/omp/extensions/cafekit-bridge.mjs +233 -0
- package/src/omp/hooks/privacy-block.cjs +250 -0
- package/src/omp/runtime.json +33 -0
- package/bin/lib/rtk-run.js +0 -28
- package/bin/phases/setup-rtk.js +0 -159
- package/src/claude/skills/backend-development/SKILL.md +0 -101
- package/src/claude/skills/backend-development/references/backend-api-design.md +0 -495
- package/src/claude/skills/backend-development/references/backend-architecture.md +0 -454
- package/src/claude/skills/backend-development/references/backend-authentication.md +0 -338
- package/src/claude/skills/backend-development/references/backend-code-quality.md +0 -659
- package/src/claude/skills/backend-development/references/backend-debugging.md +0 -904
- package/src/claude/skills/backend-development/references/backend-devops.md +0 -491
- package/src/claude/skills/backend-development/references/backend-mindset.md +0 -387
- package/src/claude/skills/backend-development/references/backend-performance.md +0 -397
- package/src/claude/skills/backend-development/references/backend-security.md +0 -342
- package/src/claude/skills/backend-development/references/backend-technologies.md +0 -256
- package/src/claude/skills/backend-development/references/backend-testing.md +0 -429
- package/src/claude/skills/devops/.env.example +0 -76
- package/src/claude/skills/devops/SKILL.md +0 -102
- package/src/claude/skills/devops/references/browser-rendering.md +0 -305
- package/src/claude/skills/devops/references/cloudflare-d1-kv.md +0 -123
- package/src/claude/skills/devops/references/cloudflare-platform.md +0 -271
- package/src/claude/skills/devops/references/cloudflare-r2-storage.md +0 -280
- package/src/claude/skills/devops/references/cloudflare-workers-advanced.md +0 -312
- package/src/claude/skills/devops/references/cloudflare-workers-apis.md +0 -309
- package/src/claude/skills/devops/references/cloudflare-workers-basics.md +0 -418
- package/src/claude/skills/devops/references/docker-basics.md +0 -297
- package/src/claude/skills/devops/references/docker-compose.md +0 -292
- package/src/claude/skills/devops/references/gcloud-platform.md +0 -297
- package/src/claude/skills/devops/references/gcloud-services.md +0 -304
- package/src/claude/skills/devops/references/kubernetes-basics.md +0 -95
- package/src/claude/skills/devops/references/kubernetes-helm-advanced.md +0 -70
- package/src/claude/skills/devops/references/kubernetes-helm.md +0 -81
- package/src/claude/skills/devops/references/kubernetes-kubectl.md +0 -74
- package/src/claude/skills/devops/references/kubernetes-security-advanced.md +0 -94
- package/src/claude/skills/devops/references/kubernetes-security.md +0 -89
- package/src/claude/skills/devops/references/kubernetes-troubleshooting-advanced.md +0 -74
- package/src/claude/skills/devops/references/kubernetes-troubleshooting.md +0 -49
- package/src/claude/skills/devops/references/kubernetes-workflows-advanced.md +0 -75
- package/src/claude/skills/devops/references/kubernetes-workflows.md +0 -76
- package/src/claude/skills/devops/scripts/cloudflare_deploy.py +0 -269
- package/src/claude/skills/devops/scripts/docker_optimize.py +0 -332
- package/src/claude/skills/devops/scripts/requirements.txt +0 -20
- package/src/claude/skills/devops/scripts/tests/requirements.txt +0 -3
- package/src/claude/skills/devops/scripts/tests/test_cloudflare_deploy.py +0 -285
- package/src/claude/skills/devops/scripts/tests/test_docker_optimize.py +0 -436
- package/src/claude/skills/frontend-design/SKILL.md +0 -135
- package/src/claude/skills/frontend-design/references/ai-multimodal-overview.md +0 -165
- package/src/claude/skills/frontend-design/references/analysis-best-practices.md +0 -80
- package/src/claude/skills/frontend-design/references/analysis-prompts.md +0 -141
- package/src/claude/skills/frontend-design/references/analysis-techniques.md +0 -118
- package/src/claude/skills/frontend-design/references/animejs.md +0 -396
- package/src/claude/skills/frontend-design/references/anti-slop-rules.md +0 -103
- package/src/claude/skills/frontend-design/references/asset-generation.md +0 -337
- package/src/claude/skills/frontend-design/references/bento-motion-engine.md +0 -142
- package/src/claude/skills/frontend-design/references/design-extraction-overview.md +0 -71
- package/src/claude/skills/frontend-design/references/extraction-best-practices.md +0 -141
- package/src/claude/skills/frontend-design/references/extraction-output-templates.md +0 -162
- package/src/claude/skills/frontend-design/references/extraction-prompts.md +0 -127
- package/src/claude/skills/frontend-design/references/magicui-components.md +0 -129
- package/src/claude/skills/frontend-design/references/performance-guardrails.md +0 -169
- package/src/claude/skills/frontend-design/references/premium-design-patterns.md +0 -93
- package/src/claude/skills/frontend-design/references/redesign-audit-checklist.md +0 -114
- package/src/claude/skills/frontend-design/references/technical-accessibility.md +0 -119
- package/src/claude/skills/frontend-design/references/technical-best-practices.md +0 -97
- package/src/claude/skills/frontend-design/references/technical-optimization.md +0 -44
- package/src/claude/skills/frontend-design/references/technical-overview.md +0 -90
- package/src/claude/skills/frontend-design/references/technical-workflows.md +0 -150
- package/src/claude/skills/frontend-design/references/visual-analysis-overview.md +0 -95
- package/src/claude/skills/frontend-design/references/workflow-3d.md +0 -102
- package/src/claude/skills/frontend-design/references/workflow-describe.md +0 -87
- package/src/claude/skills/frontend-design/references/workflow-immersive.md +0 -87
- package/src/claude/skills/frontend-design/references/workflow-quick.md +0 -57
- package/src/claude/skills/frontend-design/references/workflow-screenshot.md +0 -63
- package/src/claude/skills/frontend-design/references/workflow-video.md +0 -74
- package/src/claude/skills/frontend-development/SKILL.md +0 -406
- package/src/claude/skills/frontend-development/resources/common-patterns.md +0 -331
- package/src/claude/skills/frontend-development/resources/complete-examples.md +0 -872
- package/src/claude/skills/frontend-development/resources/component-patterns.md +0 -502
- package/src/claude/skills/frontend-development/resources/data-fetching.md +0 -767
- package/src/claude/skills/frontend-development/resources/file-organization.md +0 -502
- package/src/claude/skills/frontend-development/resources/loading-and-error-states.md +0 -501
- package/src/claude/skills/frontend-development/resources/performance.md +0 -406
- package/src/claude/skills/frontend-development/resources/routing-guide.md +0 -364
- package/src/claude/skills/frontend-development/resources/styling-guide.md +0 -428
- package/src/claude/skills/frontend-development/resources/typescript-standards.md +0 -418
- package/src/claude/skills/mobile-development/SKILL.md +0 -218
- package/src/claude/skills/mobile-development/references/mobile-android.md +0 -604
- package/src/claude/skills/mobile-development/references/mobile-best-practices.md +0 -544
- package/src/claude/skills/mobile-development/references/mobile-debugging.md +0 -1089
- package/src/claude/skills/mobile-development/references/mobile-frameworks.md +0 -465
- package/src/claude/skills/mobile-development/references/mobile-ios.md +0 -496
- package/src/claude/skills/mobile-development/references/mobile-mindset.md +0 -542
- package/src/claude/skills/react-best-practices/AGENTS.md +0 -2249
- package/src/claude/skills/react-best-practices/README.md +0 -123
- package/src/claude/skills/react-best-practices/SKILL.md +0 -128
- package/src/claude/skills/react-best-practices/metadata.json +0 -15
- package/src/claude/skills/react-best-practices/rules/_sections.md +0 -46
- package/src/claude/skills/react-best-practices/rules/_template.md +0 -28
- package/src/claude/skills/react-best-practices/rules/advanced-event-handler-refs.md +0 -55
- package/src/claude/skills/react-best-practices/rules/advanced-use-latest.md +0 -49
- package/src/claude/skills/react-best-practices/rules/async-api-routes.md +0 -38
- package/src/claude/skills/react-best-practices/rules/async-defer-await.md +0 -80
- package/src/claude/skills/react-best-practices/rules/async-dependencies.md +0 -36
- package/src/claude/skills/react-best-practices/rules/async-parallel.md +0 -28
- package/src/claude/skills/react-best-practices/rules/async-suspense-boundaries.md +0 -99
- package/src/claude/skills/react-best-practices/rules/bundle-barrel-imports.md +0 -59
- package/src/claude/skills/react-best-practices/rules/bundle-conditional.md +0 -31
- package/src/claude/skills/react-best-practices/rules/bundle-defer-third-party.md +0 -49
- package/src/claude/skills/react-best-practices/rules/bundle-dynamic-imports.md +0 -35
- package/src/claude/skills/react-best-practices/rules/bundle-preload.md +0 -50
- package/src/claude/skills/react-best-practices/rules/client-event-listeners.md +0 -74
- package/src/claude/skills/react-best-practices/rules/client-swr-dedup.md +0 -56
- package/src/claude/skills/react-best-practices/rules/js-batch-dom-css.md +0 -82
- package/src/claude/skills/react-best-practices/rules/js-cache-function-results.md +0 -80
- package/src/claude/skills/react-best-practices/rules/js-cache-property-access.md +0 -28
- package/src/claude/skills/react-best-practices/rules/js-cache-storage.md +0 -70
- package/src/claude/skills/react-best-practices/rules/js-combine-iterations.md +0 -32
- package/src/claude/skills/react-best-practices/rules/js-early-exit.md +0 -50
- package/src/claude/skills/react-best-practices/rules/js-hoist-regexp.md +0 -45
- package/src/claude/skills/react-best-practices/rules/js-index-maps.md +0 -37
- package/src/claude/skills/react-best-practices/rules/js-length-check-first.md +0 -49
- package/src/claude/skills/react-best-practices/rules/js-min-max-loop.md +0 -82
- package/src/claude/skills/react-best-practices/rules/js-set-map-lookups.md +0 -24
- package/src/claude/skills/react-best-practices/rules/js-tosorted-immutable.md +0 -57
- package/src/claude/skills/react-best-practices/rules/rendering-activity.md +0 -26
- package/src/claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
- package/src/claude/skills/react-best-practices/rules/rendering-conditional-render.md +0 -40
- package/src/claude/skills/react-best-practices/rules/rendering-content-visibility.md +0 -38
- package/src/claude/skills/react-best-practices/rules/rendering-hoist-jsx.md +0 -46
- package/src/claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
- package/src/claude/skills/react-best-practices/rules/rendering-svg-precision.md +0 -28
- package/src/claude/skills/react-best-practices/rules/rerender-defer-reads.md +0 -39
- package/src/claude/skills/react-best-practices/rules/rerender-dependencies.md +0 -45
- package/src/claude/skills/react-best-practices/rules/rerender-derived-state.md +0 -29
- package/src/claude/skills/react-best-practices/rules/rerender-functional-setstate.md +0 -74
- package/src/claude/skills/react-best-practices/rules/rerender-lazy-state-init.md +0 -58
- package/src/claude/skills/react-best-practices/rules/rerender-memo.md +0 -44
- package/src/claude/skills/react-best-practices/rules/rerender-transitions.md +0 -40
- package/src/claude/skills/react-best-practices/rules/server-after-nonblocking.md +0 -73
- package/src/claude/skills/react-best-practices/rules/server-cache-lru.md +0 -41
- package/src/claude/skills/react-best-practices/rules/server-cache-react.md +0 -26
- package/src/claude/skills/react-best-practices/rules/server-parallel-fetching.md +0 -79
- 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,
|
|
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 /
|
|
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,
|
|
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
|
|
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 {
|
package/src/codex/hooks.json
CHANGED
|
@@ -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.
|
|
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
|
package/src/codex/runtime.json
CHANGED
package/src/common/.env.example
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# ============================================
|
|
9
9
|
|
|
10
10
|
# Google Gemini / AI Studio API key
|
|
11
|
-
# Required for
|
|
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
|
+
}
|