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