cc-safe-setup 29.8.0 → 30.0.4
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-plugin/marketplace.json +72 -19
- package/.claude-plugin/plugin.json +1 -1
- package/CONTRIBUTING.md +7 -2
- package/COOKBOOK.md +50 -46
- package/LICENSE +21 -0
- package/MIGRATION.md +8 -8
- package/README.md +419 -512
- package/SAFETY_CHECKLIST.md +15 -7
- package/SERVICES.md +194 -0
- package/SETTINGS_REFERENCE.md +236 -17
- package/SKILL.md +5 -5
- package/TROUBLESHOOTING.md +35 -14
- package/action.yml +12 -4
- package/audit/README.md +84 -0
- package/audit/audit-checklist.md +152 -0
- package/audit/boundary-selftest.sh +85 -0
- package/audit/boundary.sh +123 -0
- package/audit/count-hooks.py +42 -0
- package/audit/find-dead-hooks.sh +82 -0
- package/audit/find-unfirable-rules.sh +142 -0
- package/audit/fire.sh +97 -0
- package/audit/selftest.sh +42 -0
- package/audit/unfirable-selftest.sh +81 -0
- package/audit-web/index.html +9 -9
- package/examples/README.md +54 -17
- package/examples/account-billing-log.sh +98 -0
- package/examples/account-routing-preflight.sh +123 -0
- package/examples/agent-sdk-credit-pool-monitor.sh +145 -0
- package/examples/agent-view-quota-decrement.sh +32 -0
- package/examples/agent-view-quota-warn.sh +94 -0
- package/examples/agents-md-edit-drift-warner.sh +223 -0
- package/examples/agents-md-loader.sh +160 -0
- package/examples/agents-md-sync-checker.sh +102 -0
- package/examples/ai-slop-punctuation-arrest.sh +210 -0
- package/examples/allowlist.sh +70 -10
- package/examples/always-allow-pattern-suggester.sh +125 -0
- package/examples/ansible-vault-guard.sh +2 -1
- package/examples/anthropic-base-url-guard.sh +76 -0
- package/examples/api-busyloop-guard.sh +123 -0
- package/examples/api-endpoint-guard.sh +12 -0
- package/examples/api-key-in-url-guard.sh +17 -1
- package/examples/api-rate-limit-guard.sh +36 -1
- package/examples/api-rate-limit-tracker.sh +12 -0
- package/examples/articulated-scope-capture.sh +101 -0
- package/examples/askuserquestion-autonomy-gate.sh +102 -0
- package/examples/attachment-traversal-guard.sh +118 -0
- package/examples/aup-block-pattern-logger.sh +161 -0
- package/examples/aup-false-positive-helper.sh +124 -0
- package/examples/aup-large-tool-output-warner.sh +282 -0
- package/examples/aup-retry-loop-guard.sh +229 -0
- package/examples/auth-expiry-reminder.sh +157 -0
- package/examples/auth-macos-sleep-detector.sh +185 -0
- package/examples/auth-path-detector.sh +171 -0
- package/examples/auth-status-checker.sh +157 -0
- package/examples/authorized-reconfirmation-detector.sh +195 -0
- package/examples/auto-approve-build.sh +39 -0
- package/examples/auto-approve-cargo.sh +31 -1
- package/examples/auto-approve-docker.sh +35 -0
- package/examples/auto-approve-git-read.sh +37 -14
- package/examples/auto-approve-go.sh +30 -1
- package/examples/auto-approve-gradle.sh +29 -1
- package/examples/auto-approve-make.sh +29 -1
- package/examples/auto-approve-maven.sh +29 -1
- package/examples/auto-approve-python.sh +41 -1
- package/examples/auto-approve-readonly-tools.sh +1 -0
- package/examples/auto-approve-readonly.sh +110 -33
- package/examples/auto-approve-ssh.sh +30 -1
- package/examples/auto-approve-test.sh +44 -0
- package/examples/auto-checkpoint.sh +1 -1
- package/examples/auto-compact-prep.sh +0 -0
- package/examples/auto-git-checkpoint.sh +0 -0
- package/examples/auto-mode-safe-commands.sh +53 -1
- package/examples/auto-mode-safety-enforcer.sh +151 -4
- package/examples/auto-push-session-branch.sh +22 -0
- package/examples/aws-production-guard.sh +13 -1
- package/examples/aws-region-guard.sh +1 -1
- package/examples/background-cost-launch-guard.sh +82 -0
- package/examples/background-task-guard.sh +12 -0
- package/examples/backup-before-refactor.sh +0 -0
- package/examples/banned-command-guard.sh +12 -0
- package/examples/bash-allowlist-secondary-check.sh +157 -0
- package/examples/bash-domain-allowlist.sh +12 -0
- package/examples/bash-fanout-bounded-rewriter.sh +191 -0
- package/examples/bash-heuristic-approver.sh +112 -10
- package/examples/bash-project-fence.sh +194 -0
- package/examples/bash-safety-auto-deny.sh +12 -0
- package/examples/bash-secret-output-detector.sh +44 -3
- package/examples/bash-trace-guard.sh +12 -0
- package/examples/bashrc-safety-check.sh +0 -0
- package/examples/bg-permission-prompt-warner.sh +182 -0
- package/examples/bg-task-cooldown-guard.sh +12 -0
- package/examples/binary-read-detector.sh +135 -0
- package/examples/binary-upload-guard.sh +12 -0
- package/examples/block-database-wipe.sh +35 -10
- package/examples/branch-name-check.sh +8 -3
- package/examples/branch-naming-convention.sh +0 -0
- package/examples/broad-find-guard.sh +12 -0
- package/examples/broad-prefix-session-trap-warner.sh +106 -0
- package/examples/bulk-file-delete-guard.sh +45 -0
- package/examples/bypass-mode-effective-verifier.sh +203 -0
- package/examples/cache-creation-drift-detector.sh +120 -0
- package/examples/cache-residue-detector.sh +195 -0
- package/examples/cache-tier-logger.sh +70 -0
- package/examples/cache-ttl-eviction-detector.sh +149 -0
- package/examples/cargo-publish-guard.sh +14 -1
- package/examples/case-insensitive-path-guard.sh +21 -2
- package/examples/case-sensitive-guard.sh +23 -2
- package/examples/cch-cache-guard.sh +0 -0
- package/examples/cch-sentinel-precommit-guard.sh +47 -0
- package/examples/check-abort-controller.sh +1 -1
- package/examples/check-accessibility.sh +0 -0
- package/examples/check-aria-labels.sh +0 -0
- package/examples/check-before-act-enforcer.sh +12 -0
- package/examples/check-charset-meta.sh +0 -0
- package/examples/check-cookie-flags.sh +0 -0
- package/examples/check-cors-config.sh +0 -0
- package/examples/check-csp-headers.sh +0 -0
- package/examples/check-csrf-protection.sh +0 -0
- package/examples/check-debounce.sh +1 -1
- package/examples/check-dependency-age.sh +0 -0
- package/examples/check-dependency-license.sh +0 -0
- package/examples/check-dockerfile-best-practice.sh +0 -0
- package/examples/check-error-boundaries.sh +0 -0
- package/examples/check-error-class.sh +1 -1
- package/examples/check-error-handling.sh +0 -0
- package/examples/check-error-logging.sh +1 -1
- package/examples/check-error-message.sh +0 -0
- package/examples/check-error-stack.sh +0 -0
- package/examples/check-favicon.sh +0 -0
- package/examples/check-git-hooks-compat.sh +0 -0
- package/examples/check-gitattributes.sh +0 -0
- package/examples/check-https-redirect.sh +0 -0
- package/examples/check-input-validation.sh +0 -0
- package/examples/check-lang-attribute.sh +0 -0
- package/examples/check-npm-scripts-exist.sh +0 -0
- package/examples/check-package-size.sh +0 -0
- package/examples/check-port-availability.sh +0 -0
- package/examples/check-promise-all.sh +1 -1
- package/examples/check-rate-limiting.sh +0 -0
- package/examples/check-return-types.sh +0 -0
- package/examples/check-semantic-versioning.sh +0 -0
- package/examples/check-suspense-fallback.sh +1 -1
- package/examples/check-test-exists.sh +0 -0
- package/examples/check-test-naming.sh +0 -0
- package/examples/check-timeout-cleanup.sh +1 -1
- package/examples/check-tls-version.sh +0 -0
- package/examples/check-unsubscribe.sh +1 -1
- package/examples/check-viewport-meta.sh +0 -0
- package/examples/check-worker-terminate.sh +1 -1
- package/examples/checkpoint-tamper-guard.sh +12 -0
- package/examples/chmod-guard.sh +12 -0
- package/examples/chown-guard.sh +24 -0
- package/examples/ci-workflow-guard.sh +6 -1
- package/examples/claim-verify-detector.sh +109 -0
- package/examples/claim-vs-caveat-checker.sh +77 -0
- package/examples/classifier-fallback-allow.sh +114 -40
- package/examples/claude-cache-gc.sh +1 -0
- package/examples/claude-code-version-pin-advisor.sh +183 -0
- package/examples/claude-md-reinjector.sh +129 -0
- package/examples/claude-md-size-monitor.sh +66 -0
- package/examples/claude-update-budget-guard.sh +123 -0
- package/examples/claude-update-smart.sh +113 -0
- package/examples/claudeignore-enforce-guard.sh +12 -0
- package/examples/claudemd-enforcer.sh +0 -0
- package/examples/claudemd-tool-prohibition.sh +128 -0
- package/examples/clear-command-confirm-guard.sh +32 -15
- package/examples/cli-config-pinning-detector.sh +162 -0
- package/examples/cliff-countdown-advisor.sh +96 -0
- package/examples/clipboard-secret-guard.sh +12 -0
- package/examples/closure-word-verify-gate.sh +150 -0
- package/examples/cloud-cli-guard.sh +39 -2
- package/examples/commit-all-scope-guard.sh +44 -0
- package/examples/commit-message-check.sh +1 -1
- package/examples/commit-message-quality.sh +6 -1
- package/examples/commitment-carry-forward-arrest.sh +361 -0
- package/examples/compact-alert-notification.sh +0 -0
- package/examples/compact-dispatch-watchdog.sh +149 -0
- package/examples/completion-claim-without-verification-detector.sh +207 -0
- package/examples/composer-guard.sh +12 -0
- package/examples/compound-bash-permission-resolver.sh +156 -0
- package/examples/compound-command-allow.sh +1 -1
- package/examples/compound-command-deny-enforcer.sh +183 -0
- package/examples/compound-inject-guard.sh +12 -0
- package/examples/concurrent-edit-lock.sh +12 -0
- package/examples/conflict-marker-guard.sh +12 -0
- package/examples/consecutive-error-breaker.sh +0 -0
- package/examples/console-log-count.sh +1 -0
- package/examples/context-compact-advisor.sh +29 -2
- package/examples/context-length-budget.sh +69 -0
- package/examples/context-threshold-alert.sh +12 -0
- package/examples/context-usage-drift-alert.sh +41 -13
- package/examples/conversation-history-guard.sh +12 -0
- package/examples/core-file-protect-guard.sh +12 -0
- package/examples/cors-star-warn.sh +0 -0
- package/examples/cost-incident-self-audit.sh +162 -0
- package/examples/cowork-claude-md-load-checker.sh +100 -0
- package/examples/cowork-fuse-staleness-watcher.sh +140 -0
- package/examples/cowork-hook-absence-warner.sh +167 -0
- package/examples/cowork-model-picker-advisor.sh +115 -0
- package/examples/credential-exfil-guard.sh +65 -4
- package/examples/credential-file-cat-guard.sh +17 -1
- package/examples/cron-create-receipt.sh +112 -0
- package/examples/cron-modification-guard.sh +12 -0
- package/examples/cross-product-mcp-spawn-detector.sh +178 -0
- package/examples/cwd-project-boundary-guard.sh +0 -0
- package/examples/daemon-zombie-worker-surfacer.sh +118 -0
- package/examples/daily-cost-guard.sh +164 -0
- package/examples/daily-usage-tracker.sh +0 -0
- package/examples/dangerous-pip-flag-guard.sh +12 -0
- package/examples/db-connect-guard.sh +13 -1
- package/examples/denied-action-retry-guard.sh +12 -0
- package/examples/deny-bypass-detector.sh +12 -0
- package/examples/deny-rule-integrity-verifier.sh +181 -0
- package/examples/dependency-install-guard.sh +12 -0
- package/examples/deploy-guard.sh +14 -2
- package/examples/deploy-path-verify-guard.sh +12 -0
- package/examples/deployment-readback-gate.sh +135 -0
- package/examples/deployment-readback-gh-adapter.sh +247 -0
- package/examples/deployment-verify-guard.sh +25 -2
- package/examples/destructive-db-script-write-guard.sh +166 -0
- package/examples/destructive-migration-write-guard.sh +163 -0
- package/examples/diff-size-guard.sh +12 -0
- package/examples/disabled-feature-toggle-advisor.sh +88 -0
- package/examples/disk-partition-guard.sh +12 -0
- package/examples/dispatch-allowlist-preflight.sh +301 -0
- package/examples/dispatch-liveness-watchdog.sh +216 -0
- package/examples/dispatch-receipt.sh +174 -0
- package/examples/django-migrate-guard.sh +12 -0
- package/examples/dns-config-guard.sh +12 -0
- package/examples/docker-dangerous-guard.sh +12 -0
- package/examples/docker-volume-guard.sh +0 -0
- package/examples/dockerfile-latest-guard.sh +1 -0
- package/examples/dotenv-anthropic-key-billing-guard.sh +99 -0
- package/examples/dotenv-commit-guard.sh +12 -0
- package/examples/dotenv-read-guard.sh +12 -0
- package/examples/dotfile-protection-guard.sh +12 -0
- package/examples/dotnet-build-on-edit.sh +1 -1
- package/examples/drizzle-migrate-guard.sh +27 -2
- package/examples/echo-probe-spam-detector.sh +176 -0
- package/examples/edit-counter-test-gate.sh +12 -0
- package/examples/edit-error-counter.sh +1 -1
- package/examples/edit-guard.sh +12 -0
- package/examples/edit-old-string-validator.sh +12 -0
- package/examples/edit-quote-ambiguity-guard.sh +97 -0
- package/examples/edit-retry-loop-guard.sh +0 -0
- package/examples/edit-verify.sh +0 -0
- package/examples/env-inline-secret-guard.sh +14 -2
- package/examples/env-naming-convention.sh +0 -0
- package/examples/env-prod-guard.sh +0 -0
- package/examples/env-required-check.sh +0 -0
- package/examples/env-source-guard.sh +12 -0
- package/examples/env-var-check.sh +24 -1
- package/examples/ephemeral-container-detector.sh +147 -0
- package/examples/error-memory-guard.sh +12 -0
- package/examples/evidence-claim-gate.sh +187 -0
- package/examples/exploration-budget-guard.sh +12 -0
- package/examples/expo-eject-guard.sh +13 -0
- package/examples/extended-thinking-loop-guard.sh +139 -0
- package/examples/extended-thinking-resume-warning.sh +119 -0
- package/examples/extended-thinking-tool-use-mismatch-detector.sh +212 -0
- package/examples/fabricated-command-detector.sh +145 -0
- package/examples/false-disk-full-guard.sh +63 -0
- package/examples/feature-deprecation-detector.sh +109 -0
- package/examples/file-change-tracker.sh +1 -1
- package/examples/file-change-undo-tracker.sh +0 -0
- package/examples/file-edit-backup.sh +63 -14
- package/examples/file-reference-check.sh +0 -0
- package/examples/file-size-limit.sh +12 -0
- package/examples/financial-operation-guard.sh +12 -0
- package/examples/firewall-guard.sh +12 -0
- package/examples/fish-shell-wrapper.sh +125 -7
- package/examples/five-hundred-milestone.sh +1 -0
- package/examples/flask-static-route-guard.sh +89 -0
- package/examples/forbidden-response-phrase-detector.sh +161 -0
- package/examples/gem-push-guard.sh +13 -0
- package/examples/gh-cli-destructive-guard.sh +13 -1
- package/examples/git-checkout-safety-guard.sh +12 -0
- package/examples/git-checkout-uncommitted-guard.sh +12 -0
- package/examples/git-config-guard.sh +12 -0
- package/examples/git-crypt-worktree-guard.sh +12 -0
- package/examples/git-filter-repo-guard.sh +56 -0
- package/examples/git-history-rewrite-guard.sh +12 -0
- package/examples/git-hook-bypass-guard.sh +0 -0
- package/examples/git-maintenance-guard.sh +83 -0
- package/examples/git-merge-conflict-prevent.sh +0 -0
- package/examples/git-message-length-check.sh +0 -0
- package/examples/git-message-length.sh +0 -0
- package/examples/git-operations-require-approval.sh +68 -14
- package/examples/git-pack-temp-cleanup-guard.sh +102 -0
- package/examples/git-push-blast-radius-guard.sh +218 -0
- package/examples/git-remote-guard.sh +24 -3
- package/examples/git-rm-orphan-wipe-guard.sh +85 -0
- package/examples/git-stash-before-checkout.sh +12 -0
- package/examples/git-stash-before-danger.sh +0 -0
- package/examples/git-submodule-guard.sh +69 -2
- package/examples/git-tag-guard.sh +12 -0
- package/examples/github-actions-secret-guard.sh +0 -0
- package/examples/gitignore-check.sh +0 -0
- package/examples/gitops-drift-guard.sh +12 -0
- package/examples/go-mod-tidy-warn.sh +1 -0
- package/examples/go-vet-after-edit.sh +12 -0
- package/examples/goal-iteration-limit-warner.sh +152 -0
- package/examples/grep-nul-false-negative-guard.sh +30 -0
- package/examples/growthbook-flag-monitor.sh +257 -0
- package/examples/hallucination-url-check.sh +1 -1
- package/examples/halt-signal-detector.sh +135 -0
- package/examples/handoff-on-clear.sh +65 -0
- package/examples/hardcoded-ip-guard.sh +1 -0
- package/examples/headless-empty-result-guard.sh +0 -0
- package/examples/headless-stop-guard.sh +0 -0
- package/examples/helm-install-guard.sh +2 -1
- package/examples/home-critical-bash-guard.sh +17 -1
- package/examples/hook-debug-wrapper.sh +6 -2
- package/examples/hook-permission-fixer.sh +0 -0
- package/examples/hook-self-disable-detector.sh +99 -0
- package/examples/hook-stdout-sanitizer.sh +6 -2
- package/examples/hook-tamper-guard.sh +12 -0
- package/examples/idle-session-cost-alert.sh +0 -0
- package/examples/image-dimension-guard.sh +109 -0
- package/examples/image-file-validator.sh +0 -0
- package/examples/iteration-quality-degradation-detector.sh +170 -0
- package/examples/java-compile-on-edit.sh +1 -1
- package/examples/json-syntax-check.sh +0 -0
- package/examples/k8s-production-guard.sh +13 -1
- package/examples/kill-process-guard.sh +12 -0
- package/examples/kubernetes-guard.sh +13 -1
- package/examples/laravel-artisan-guard.sh +13 -0
- package/examples/large-file-guard.sh +1 -1
- package/examples/log-level-guard.sh +0 -0
- package/examples/log-truncation-guard.sh +12 -0
- package/examples/long-session-malformed-tool-call-detector.sh +183 -0
- package/examples/loop-detector.sh +12 -0
- package/examples/magic-number-warn.sh +1 -0
- package/examples/main-branch-warn.sh +12 -0
- package/examples/markdown-link-check.sh +0 -0
- package/examples/max-concurrent-agents.sh +12 -0
- package/examples/max-edit-size-guard.sh +13 -0
- package/examples/max-file-count-guard.sh +0 -0
- package/examples/max-file-delete-count.sh +0 -0
- package/examples/max-function-length.sh +1 -0
- package/examples/max-import-count.sh +0 -0
- package/examples/max-subagent-count.sh +0 -0
- package/examples/mcp-config-freeze.sh +12 -0
- package/examples/mcp-config-poisoning-audit.sh +150 -0
- package/examples/mcp-data-boundary.sh +1 -1
- package/examples/mcp-misdiagnosis-arrest.sh +209 -0
- package/examples/mcp-orphan-leak-warner.sh +49 -0
- package/examples/mcp-permissions-ask-warner.sh +151 -0
- package/examples/mcp-routine-approval-detector.sh +154 -0
- package/examples/mcp-server-allowlist.sh +12 -0
- package/examples/mcp-server-guard.sh +12 -0
- package/examples/mcp-startup-bloat-detector.sh +87 -0
- package/examples/mcp-stdio-compatibility-test.sh +153 -0
- package/examples/mcp-tool-audit-log.sh +0 -0
- package/examples/mcp-tool-guard.sh +101 -1
- package/examples/mcp-warmup-wait.sh +0 -0
- package/examples/memory-chain-audit.sh +56 -0
- package/examples/memory-orphan-detector.sh +188 -0
- package/examples/memory-write-guard.sh +99 -17
- package/examples/migration-verify-guard.sh +12 -0
- package/examples/model-swap-suggester.sh +177 -0
- package/examples/model-version-alert.sh +5 -1
- package/examples/model-version-lock.sh +63 -0
- package/examples/move-delete-sequence-guard.sh +33 -2
- package/examples/multi-file-plan-routing-gate.sh +192 -0
- package/examples/multi-vendor-concurrent-warner.sh +108 -0
- package/examples/multi-window-auth-drift-detector.sh +183 -0
- package/examples/multiline-command-approver.sh +161 -23
- package/examples/nested-background-agent-guard.sh +109 -0
- package/examples/nested-spawn-inflight-guard.sh +178 -0
- package/examples/network-exfil-guard.sh +12 -0
- package/examples/network-guard.sh +12 -0
- package/examples/network-interface-guard.sh +12 -0
- package/examples/nextjs-env-guard.sh +1 -1
- package/examples/no-absolute-import.sh +0 -0
- package/examples/no-alert-confirm-prompt.sh +0 -0
- package/examples/no-anonymous-default-export.sh +0 -0
- package/examples/no-any-type.sh +0 -0
- package/examples/no-any-typescript.sh +1 -0
- package/examples/no-ask-human.sh +12 -0
- package/examples/no-assignment-in-condition.sh +0 -0
- package/examples/no-base64-exfil.sh +12 -0
- package/examples/no-callback-hell.sh +0 -0
- package/examples/no-circular-dependency.sh +0 -0
- package/examples/no-cleartext-storage.sh +0 -0
- package/examples/no-commented-code.sh +0 -0
- package/examples/no-commit-fixup.sh +0 -0
- package/examples/no-console-assert.sh +0 -0
- package/examples/no-console-error-swallow.sh +0 -0
- package/examples/no-console-in-prod.sh +0 -0
- package/examples/no-console-log-commit.sh +12 -0
- package/examples/no-console-log.sh +0 -0
- package/examples/no-console-time.sh +0 -0
- package/examples/no-cors-wildcard.sh +4 -1
- package/examples/no-curl-upload.sh +0 -0
- package/examples/no-dangerouslySetInnerHTML.sh +0 -0
- package/examples/no-dangling-await.sh +1 -0
- package/examples/no-debug-commit.sh +12 -0
- package/examples/no-debug-in-commit.sh +0 -0
- package/examples/no-deep-nesting.sh +0 -0
- package/examples/no-deep-relative-import.sh +1 -0
- package/examples/no-default-credentials.sh +0 -0
- package/examples/no-deploy-friday.sh +13 -1
- package/examples/no-disabled-test.sh +0 -0
- package/examples/no-document-write.sh +0 -0
- package/examples/no-empty-function.sh +0 -0
- package/examples/no-eval-in-template.sh +0 -0
- package/examples/no-eval-template.sh +3 -1
- package/examples/no-eval.sh +0 -0
- package/examples/no-exec-user-input.sh +0 -0
- package/examples/no-fixme-ship.sh +12 -0
- package/examples/no-floating-promises.sh +0 -0
- package/examples/no-force-flag.sh +22 -5
- package/examples/no-force-install.sh +0 -0
- package/examples/no-git-amend.sh +12 -0
- package/examples/no-git-rebase-public.sh +0 -0
- package/examples/no-global-install.sh +12 -0
- package/examples/no-global-state.sh +0 -0
- package/examples/no-hardcoded-port.sh +1 -0
- package/examples/no-hardcoded-url.sh +0 -0
- package/examples/no-helmet-missing.sh +0 -0
- package/examples/no-http-url.sh +1 -0
- package/examples/no-http-without-https.sh +0 -0
- package/examples/no-inline-style.sh +0 -0
- package/examples/no-inline-styles.sh +1 -0
- package/examples/no-innerhtml.sh +0 -0
- package/examples/no-install-global.sh +12 -0
- package/examples/no-jwt-in-url.sh +0 -0
- package/examples/no-large-commit.sh +0 -0
- package/examples/no-localhost-expose.sh +0 -0
- package/examples/no-magic-number.sh +0 -0
- package/examples/no-md5-sha1.sh +0 -0
- package/examples/no-mixed-line-endings.sh +8 -1
- package/examples/no-mutation-in-reducer.sh +0 -0
- package/examples/no-nested-ternary.sh +0 -0
- package/examples/no-network-exfil.sh +0 -0
- package/examples/no-open-redirect.sh +0 -0
- package/examples/no-output-truncation.sh +12 -0
- package/examples/no-package-downgrade.sh +0 -0
- package/examples/no-package-lock-edit.sh +12 -0
- package/examples/no-path-join-user-input.sh +0 -0
- package/examples/no-port-bind.sh +0 -0
- package/examples/no-process-exit.sh +0 -0
- package/examples/no-prototype-pollution.sh +0 -0
- package/examples/no-push-without-ci.sh +0 -0
- package/examples/no-push-without-tests.sh +12 -0
- package/examples/no-raw-password-in-url.sh +0 -0
- package/examples/no-root-user-docker.sh +1 -0
- package/examples/no-root-write.sh +12 -0
- package/examples/no-secrets-in-args.sh +4 -1
- package/examples/no-secrets-in-logs.sh +0 -0
- package/examples/no-sensitive-log.sh +0 -0
- package/examples/no-sleep-in-hooks.sh +0 -0
- package/examples/no-star-import-python.sh +1 -0
- package/examples/no-string-concat-sql.sh +0 -0
- package/examples/no-sudo-guard.sh +12 -0
- package/examples/no-sync-fs.sh +0 -0
- package/examples/no-todo-in-merge.sh +0 -0
- package/examples/no-todo-in-production.sh +1 -0
- package/examples/no-todo-without-issue.sh +0 -0
- package/examples/no-unused-import.sh +0 -0
- package/examples/no-var-keyword.sh +0 -0
- package/examples/no-verify-blocker.sh +12 -0
- package/examples/no-wget-piped-bash.sh +12 -0
- package/examples/no-wildcard-cors.sh +0 -0
- package/examples/no-wildcard-delete.sh +0 -0
- package/examples/no-wildcard-import.sh +0 -0
- package/examples/no-with-statement.sh +0 -0
- package/examples/no-write-outside-src.sh +0 -0
- package/examples/no-xml-external-entity.sh +0 -0
- package/examples/non-english-quality-warner.sh +156 -0
- package/examples/notify-waiting.sh +1 -1
- package/examples/npm-audit-warn.sh +0 -0
- package/examples/npm-global-install-guard.sh +12 -0
- package/examples/npm-publish-guard.sh +12 -0
- package/examples/npm-script-injection.sh +0 -0
- package/examples/npm-supply-chain-guard.sh +0 -0
- package/examples/nuxt-config-guard.sh +1 -0
- package/examples/oauth-refresh-monitor.sh +210 -0
- package/examples/opus-degradation-tracker.sh +187 -0
- package/examples/opus48-routine-task-warning.sh +196 -0
- package/examples/opus48-thinking-wedge-advisor.sh +143 -0
- package/examples/output-credential-scan.sh +1 -1
- package/examples/output-secret-mask.sh +31 -9
- package/examples/output-token-spike-detector.sh +145 -0
- package/examples/package-json-guard.sh +12 -0
- package/examples/package-lock-frozen.sh +12 -0
- package/examples/parallel-batch-size-limiter.sh +121 -0
- package/examples/parallel-cascade-detector.sh +116 -0
- package/examples/partial-view-claim-arrest.sh +174 -0
- package/examples/path-deny-bash-guard.sh +12 -0
- package/examples/path-traversal-guard.sh +12 -0
- package/examples/per-prompt-token-cap.sh +124 -0
- package/examples/permission-audit-log.sh +1 -1
- package/examples/permission-denial-enforcer.sh +12 -0
- package/examples/permission-entry-validator.sh +0 -0
- package/examples/persisted-1m-model-advisor.sh +88 -0
- package/examples/php-lint-on-edit.sh +1 -1
- package/examples/pii-upload-guard.sh +12 -0
- package/examples/pip-publish-guard.sh +13 -0
- package/examples/pip-requirements-guard.sh +12 -0
- package/examples/pip-venv-required.sh +12 -0
- package/examples/plan-mode-edit-guard.sh +12 -0
- package/examples/plan-mode-enforcer.sh +12 -0
- package/examples/plan-mode-strict-guard.sh +12 -0
- package/examples/plugin-hooks-json-bloat-detector.sh +169 -0
- package/examples/plugin-process-cleanup.sh +0 -0
- package/examples/polyglot-rm-guard.sh +12 -0
- package/examples/post-compact-safety.sh +12 -0
- package/examples/post-edit-disk-verify.sh +152 -0
- package/examples/powershell-remove-item-guard.sh +85 -0
- package/examples/pr-description-check.sh +0 -0
- package/examples/pr-duplicate-guard.sh +13 -0
- package/examples/pre-bash-sed-line-ending-windows.sh +87 -0
- package/examples/pre-compact-knowledge-save.sh +0 -0
- package/examples/pre-compact-transcript-backup.sh +0 -0
- package/examples/pre-execution-claim-detector.sh +179 -0
- package/examples/prefer-builtin-tools.sh +10 -1
- package/examples/prefer-const.sh +0 -0
- package/examples/prefer-dedicated-tools.sh +12 -0
- package/examples/prefer-optional-chaining.sh +0 -0
- package/examples/prisma-migrate-guard.sh +24 -7
- package/examples/production-port-kill-guard.sh +12 -0
- package/examples/prompt-injection-guard.sh +2 -1
- package/examples/prompt-usage-logger.sh +0 -0
- package/examples/protect-claudemd.sh +12 -0
- package/examples/protect-dotfiles.sh +12 -0
- package/examples/proxy-capture-suggester.sh +159 -0
- package/examples/public-artefact-socratic-narrowing.sh +232 -0
- package/examples/public-repo-push-guard.sh +12 -0
- package/examples/push-requires-test-pass-record.sh +0 -0
- package/examples/push-requires-test-pass.sh +12 -0
- package/examples/quota-anomaly-detector.sh +173 -0
- package/examples/quota-reset-audit.sh +58 -0
- package/examples/quoted-flag-approver.sh +99 -10
- package/examples/rails-migration-guard.sh +12 -0
- package/examples/rate-limit-guard.sh +0 -0
- package/examples/read-all-files-enforcer.sh +0 -0
- package/examples/read-budget-guard.sh +12 -0
- package/examples/read-loop-detector.sh +59 -0
- package/examples/read-once-guard.sh +96 -0
- package/examples/read-only-mode.sh +12 -0
- package/examples/readme-exists-check.sh +0 -0
- package/examples/record-read-coverage.sh +73 -0
- package/examples/redirect-fragment-warner.sh +149 -0
- package/examples/redis-flushall-guard.sh +13 -0
- package/examples/redundant-read-blocker.sh +132 -0
- package/examples/refusal-arrest-gate.sh +248 -0
- package/examples/registry-publish-guard.sh +13 -1
- package/examples/remote-control-billing-classifier.sh +135 -0
- package/examples/replace-all-guard.sh +12 -0
- package/examples/repo-visibility-guard.sh +12 -0
- package/examples/reroute-after-block-guard.sh +150 -0
- package/examples/response-budget-guard.sh +0 -0
- package/examples/resume-drift-watcher.sh +57 -0
- package/examples/rg-replace-flag-detector.sh +125 -0
- package/examples/rhetorical-verification-prompt-detector.sh +102 -0
- package/examples/ripgrep-permission-fix.sh +0 -0
- package/examples/rm-safety-net.sh +151 -8
- package/examples/role-tool-guard.sh +12 -0
- package/examples/route-handler-emptiness-gate.sh +324 -0
- package/examples/ruby-lint-on-edit.sh +1 -1
- package/examples/runtime-binary-change-detector.sh +102 -0
- package/examples/same-command-repeat-detector.sh +196 -0
- package/examples/same-correction-arrest.sh +169 -0
- package/examples/sandbox-denywrite-enforcer.sh +78 -0
- package/examples/sandbox-relative-path-audit.sh +83 -25
- package/examples/sandbox-write-verify.sh +12 -0
- package/examples/schema-migration-guard.sh +0 -0
- package/examples/scope-expansion-receipt.sh +178 -0
- package/examples/scope-guard.sh +88 -9
- package/examples/secret-file-read-guard.sh +12 -0
- package/examples/secret-file-write-guard.sh +150 -0
- package/examples/self-modify-bypass-guard.sh +0 -0
- package/examples/sensitive-file-read-guard.sh +23 -3
- package/examples/sensitive-log-guard.sh +1 -0
- package/examples/server-side-prompt-injection-detector.sh +69 -0
- package/examples/session-agent-cost-limiter.sh +12 -0
- package/examples/session-backup-on-start.sh +1 -1
- package/examples/session-cost-alert.sh +12 -0
- package/examples/session-drift-guard.sh +12 -0
- package/examples/session-duration-guard.sh +25 -2
- package/examples/session-end-logger.sh +0 -0
- package/examples/session-health-monitor.sh +0 -0
- package/examples/session-index-repair.sh +1 -1
- package/examples/session-memory-watchdog.sh +1 -0
- package/examples/session-permission-reset-guard.sh +12 -0
- package/examples/session-persistence-verifier.sh +93 -0
- package/examples/session-plugin-pin-watch.sh +68 -0
- package/examples/session-rate-monitor.sh +176 -0
- package/examples/session-resume-env-fix.sh +0 -0
- package/examples/session-start-quota-status.sh +193 -0
- package/examples/session-start-safety-check.sh +55 -3
- package/examples/session-state-saver.sh +0 -0
- package/examples/session-summary-stop.sh +1 -1
- package/examples/session-summary.sh +0 -0
- package/examples/session-token-counter.sh +1 -1
- package/examples/settings-hooks-key-restorer.sh +99 -0
- package/examples/settings-json-health-check.sh +57 -0
- package/examples/settings-json-model-guard.sh +0 -0
- package/examples/settings-regression-tester.sh +188 -0
- package/examples/shell-config-truncation-guard.sh +12 -0
- package/examples/shell-wrapper-guard.sh +12 -0
- package/examples/skill-cumulative-size-detector.sh +104 -0
- package/examples/skill-description-drop-detector.sh +148 -0
- package/examples/skill-gate.sh +0 -0
- package/examples/skill-truncation-detector.sh +154 -0
- package/examples/skills-context-recorder.sh +167 -0
- package/examples/skills-load-verifier.sh +96 -0
- package/examples/skills-settings-validator.sh +139 -0
- package/examples/sonnet-45-deprecation-helper.sh +85 -0
- package/examples/spec-file-scope-guard.sh +12 -0
- package/examples/spring-profile-guard.sh +1 -0
- package/examples/spurious-malformed-notice-detector.sh +247 -0
- package/examples/sql-bulk-delete-warn.sh +88 -0
- package/examples/sql-injection-detect.sh +0 -0
- package/examples/ssh-key-protect.sh +12 -0
- package/examples/staged-secret-scan.sh +22 -1
- package/examples/stale-temp-settings-detector.sh +86 -0
- package/examples/standing-rule-session-surfacer.sh +137 -0
- package/examples/stop-hook-sigterm-wrapper.sh +182 -0
- package/examples/stop-transcript-write-health.sh +64 -0
- package/examples/strict-allowlist.sh +57 -15
- package/examples/strip-coauthored-by.sh +50 -10
- package/examples/subagent-blast-radius-guard.sh +184 -0
- package/examples/subagent-boundary-precheck.sh +126 -0
- package/examples/subagent-budget-guard.sh +12 -0
- package/examples/subagent-claudemd-inject.sh +0 -0
- package/examples/subagent-closure-verify-gate.sh +164 -0
- package/examples/subagent-destructive-git-guard.sh +120 -0
- package/examples/subagent-error-detector.sh +0 -0
- package/examples/subagent-forged-system-reminder-guard.sh +156 -0
- package/examples/subagent-identity-leak-guard.sh +102 -0
- package/examples/subagent-identity-reassertion.sh +51 -0
- package/examples/subagent-inheritance-tester.sh +179 -0
- package/examples/subagent-permission-mode-guard.sh +110 -0
- package/examples/subagent-scope-guard.sh +12 -0
- package/examples/subagent-spawn-verification-enforcer.sh +102 -0
- package/examples/subagent-tool-allowlist-enforcer.sh +101 -0
- package/examples/subagent-tool-call-limiter.sh +87 -15
- package/examples/subcommand-chain-guard.sh +12 -0
- package/examples/subscription-api-billing-warner.sh +160 -0
- package/examples/subscription-bypass-detector.sh +152 -0
- package/examples/svelte-lint-on-edit.sh +1 -0
- package/examples/swift-build-on-edit.sh +1 -1
- package/examples/symlink-guard.sh +27 -2
- package/examples/symlink-protect.sh +1 -0
- package/examples/system-dir-protection-guard.sh +73 -8
- package/examples/system-message-workaround.sh +38 -13
- package/examples/system-package-guard.sh +12 -0
- package/examples/systemd-service-guard.sh +12 -0
- package/examples/task-integrity-guard.sh +12 -0
- package/examples/temp-file-cleanup.sh +1 -1
- package/examples/temporal-suggestion-detector.sh +135 -0
- package/examples/terraform-guard.sh +27 -6
- package/examples/test-after-edit.sh +1 -1
- package/examples/test-before-push.sh +12 -0
- package/examples/test-coverage-reminder.sh +24 -1
- package/examples/thinking-budget-effort-mismatch-detector.sh +149 -0
- package/examples/timezone-guard.sh +0 -0
- package/examples/tmp-output-size-guard.sh +0 -0
- package/examples/todo-check.sh +1 -1
- package/examples/token-budget-guard.sh +12 -0
- package/examples/token-budget-per-task.sh +12 -0
- package/examples/token-usage-tracker.sh +7 -1
- package/examples/tokenizer-ratio-alert-corpus.sh +90 -0
- package/examples/tokenizer-ratio-alert.sh +104 -0
- package/examples/tool-call-rate-limiter.sh +0 -0
- package/examples/tool-result-correlation-checker.sh +167 -0
- package/examples/tool-result-size-guard.sh +132 -0
- package/examples/tool-retry-budget-guard.sh +12 -0
- package/examples/transcript-contamination-detector.sh +138 -0
- package/examples/trustfall-mcp-injection-guard.sh +103 -0
- package/examples/ultrareview-large-diff-advisor.sh +184 -0
- package/examples/unbounded-output-guard.sh +94 -0
- package/examples/uncommitted-changes-stop.sh +1 -0
- package/examples/uncommitted-discard-guard.sh +23 -5
- package/examples/uncommitted-work-guard.sh +12 -0
- package/examples/usage-warn.sh +0 -0
- package/examples/user-account-guard.sh +12 -0
- package/examples/userprompt-submit-receipt.sh +123 -0
- package/examples/variable-expansion-guard.sh +12 -0
- package/examples/verify-after-publish-reminder.sh +93 -0
- package/examples/verify-before-commit.sh +12 -0
- package/examples/version-bump-detector.sh +156 -0
- package/examples/version-regression-warner.sh +144 -0
- package/examples/vue-lint-on-edit.sh +1 -0
- package/examples/warn-cron-cost-trap.sh +173 -0
- package/examples/webfetch-runaway-guard.sh +89 -0
- package/examples/wellness-break-reminder.sh +118 -0
- package/examples/windows-destructive-command-guard.sh +177 -0
- package/examples/windows-path-guard.sh +12 -0
- package/examples/windows-python-stub-detector.sh +63 -0
- package/examples/work-hours-guard.sh +12 -0
- package/examples/working-directory-fence.sh +12 -0
- package/examples/workspace-lease-guard.sh +205 -0
- package/examples/worktree-create-log.sh +1 -0
- package/examples/worktree-delete-guard.sh +12 -0
- package/examples/worktree-edit-boundary-guard.sh +127 -0
- package/examples/worktree-escape-write-guard.sh +114 -0
- package/examples/worktree-hook-linker.sh +0 -0
- package/examples/worktree-hooks-path-fix.sh +111 -0
- package/examples/worktree-parent-write-guard.sh +43 -0
- package/examples/worktree-project-unify.sh +2 -1
- package/examples/worktree-remove-uncommitted-guard.sh +13 -0
- package/examples/worktree-unmerged-guard.sh +7 -1
- package/examples/write-byte-integrity-verifier.sh +190 -0
- package/examples/write-empty-content-guard.sh +67 -0
- package/examples/write-nul-corruption-detector.sh +61 -0
- package/examples/write-overwrite-confirm.sh +160 -14
- package/examples/write-secret-guard.sh +27 -5
- package/examples/write-shrink-guard.sh +12 -0
- package/examples/write-test-ratio.sh +0 -0
- package/examples/wsl-host-disk-space-guard.sh +97 -0
- package/examples/xml-format-leak-detector.sh +266 -0
- package/examples/yaml-syntax-check.sh +12 -0
- package/hooks/hooks.json +6 -6
- package/index.mjs +1699 -183
- package/mission.md +2 -0
- package/package.json +4 -4
- package/plugins/credential-guard/.claude-plugin/plugin.json +9 -7
- package/plugins/git-protection/.claude-plugin/plugin.json +10 -8
- package/plugins/safety-essentials/.claude-plugin/plugin.json +9 -7
- package/plugins/token-guard/.claude-plugin/plugin.json +10 -8
- package/scripts/agents-md-sync-setup.sh +156 -0
- package/scripts/autonomy-blocked-summary.sh +148 -0
- package/scripts/build-search-index.py +43 -0
- package/scripts/build-sitemap.py +119 -0
- package/scripts/cc-doctor.sh +192 -0
- package/scripts/check-hook-event-names.py +127 -0
- package/scripts/check-misfire-instrument.sh +64 -0
- package/scripts/claim-verify-audit.sh +467 -0
- package/scripts/cowork-claudemd-helper.sh +136 -0
- package/scripts/june-15-deprecated-model-scan.sh +111 -0
- package/scripts/measure-heredoc-shapes.sh +67 -0
- package/scripts/measure-literal-match-misfire.sh +110 -0
- package/scripts/probe-scope-guard-branches.sh +48 -0
- package/scripts/probe-skip-line-both-ways.sh +65 -0
- package/scripts/receipts-aggregate.py +199 -0
- package/scripts/session-forensic-audit.py +360 -0
- package/scripts.json +9 -9
- package/scripts.json.bak-2026-07-27-find +13 -0
- package/skills/safety-setup/SKILL.md +5 -5
- package/tests/approve-side-remaining-two.test.sh +129 -0
- package/tests/approver-list-granularity.test.sh +145 -0
- package/tests/askuserquestion-autonomy-gate.test.sh +181 -0
- package/tests/audit-ci-gate.test.sh +87 -0
- package/tests/audit-cross-layer-claudemd.test.sh +170 -0
- package/tests/audit-fail-open-parse.test.sh +100 -0
- package/tests/auto-approve-compound-tail.test.sh +194 -0
- package/tests/auto-mode-path-invoked-commands.test.sh +89 -0
- package/tests/auto-mode-quoted-targets.test.sh +127 -0
- package/tests/auto-mode-unquoted-targets.test.sh +112 -0
- package/tests/bash-secret-output-detector-detects.test.sh +102 -0
- package/tests/blindspots-cross-layer.test.sh +177 -0
- package/tests/block-database-wipe.test.sh +64 -0
- package/tests/bundled-short-flags-bypass.test.sh +85 -0
- package/tests/cd-git-allow-compound-tail.test.sh +67 -0
- package/tests/chown-guard-filesystem-root.test.sh +64 -0
- package/tests/ci-workflow-guard-skip-rule.test.sh +115 -0
- package/tests/compound-deny-safe-rm-targets.test.sh +105 -0
- package/tests/core-scripts-parser-fallback.test.sh +140 -0
- package/tests/destructive-guard-bare-glob.test.sh +74 -0
- package/tests/destructive-guard-every-operand.test.sh +100 -0
- package/tests/destructive-guard-find-deletion.test.sh +114 -0
- package/tests/destructive-guard-flag-order.test.sh +106 -0
- package/tests/destructive-guard-mentions-vs-invocations.test.sh +107 -0
- package/tests/destructive-guard-quoted-target.test.sh +89 -0
- package/tests/destructive-guard-separators.test.sh +105 -0
- package/tests/destructive-guard-variable-target.test.sh +98 -0
- package/tests/destructive-guard-wrapped-command.test.sh +91 -0
- package/tests/doctor-hook-disabling-env.test.sh +92 -0
- package/tests/doctor-misspelled-settings-keys.test.sh +106 -0
- package/tests/example-guards-wrapped-command.test.sh +169 -0
- package/tests/file-edit-backup.test.sh +93 -0
- package/tests/git-filter-repo-guard.test.sh +60 -0
- package/tests/git-global-options-bypass.test.sh +141 -0
- package/tests/git-invocation-prefixes-all-guards.test.sh +114 -0
- package/tests/git-maintenance-guard.test.sh +83 -0
- package/tests/git-pack-temp-cleanup-guard.test.sh +133 -0
- package/tests/git-rm-orphan-wipe-guard.test.sh +66 -0
- package/tests/git-submodule-guard.test.sh +50 -0
- package/tests/grep-pcre-portability.test.sh +155 -0
- package/tests/hook-registration-matches-input.test.sh +73 -0
- package/tests/hook-trigger-matches-envelope.test.sh +178 -0
- package/tests/install-does-not-claim-protection.test.sh +110 -0
- package/tests/install-example-standalone-tools.test.sh +139 -0
- package/tests/line-continuation-bypass.test.sh +114 -0
- package/tests/mcp-config-poisoning-audit.test.sh +106 -0
- package/tests/nested-background-agent-guard.test.sh +58 -0
- package/tests/no-unqualified-safety-claims.test.sh +114 -0
- package/tests/option-eating-grep-patterns.test.sh +130 -0
- package/tests/outdated-core-guards.test.sh +134 -0
- package/tests/outdated-report.test.sh +155 -0
- package/tests/output-secret-mask-warns.test.sh +73 -0
- package/tests/plugin-hooks-fail-open-under-sh.test.sh +165 -0
- package/tests/plugins-word-order.test.sh +126 -0
- package/tests/powershell-remove-item-guard.test.sh +85 -0
- package/tests/project-dir-naming.test.sh +103 -0
- package/tests/quoted-safe-targets-and-mentions.test.sh +190 -0
- package/tests/reroute-after-block-guard.test.sh +104 -0
- package/tests/secret-guard-documented-promises.test.sh +97 -0
- package/tests/session-plugin-pin-watch.test.sh +45 -0
- package/tests/settings-unreadable-refuses-to-write.test.sh +95 -0
- package/tests/shield-no-silent-adoption.test.py +110 -0
- package/tests/shield-no-silent-adoption.test.sh +46 -0
- package/tests/stop-transcript-write-health.test.sh +46 -0
- package/tests/system-dir-guard-root-erased.test.sh +66 -0
- package/tests/test-account-billing-log.sh +100 -0
- package/tests/test-account-routing-preflight.sh +77 -0
- package/tests/test-agent-sdk-credit-pool-monitor.sh +157 -0
- package/tests/test-agent-view-quota-warn.sh +184 -0
- package/tests/test-agents-md-edit-drift-warner.sh +176 -0
- package/tests/test-agents-md-loader.sh +204 -0
- package/tests/test-agents-md-sync-checker.sh +125 -0
- package/tests/test-ai-slop-punctuation-arrest.sh +280 -0
- package/tests/test-allowlist.sh +60 -0
- package/tests/test-always-allow-pattern-suggester.sh +147 -0
- package/tests/test-anthropic-base-url-guard.sh +88 -0
- package/tests/test-articulated-scope-capture.sh +139 -0
- package/tests/test-attachment-traversal-guard.sh +168 -0
- package/tests/test-aup-block-pattern-logger.sh +315 -0
- package/tests/test-aup-false-positive-helper.sh +188 -0
- package/tests/test-aup-large-tool-output-warner.sh +370 -0
- package/tests/test-aup-retry-loop-guard.sh +341 -0
- package/tests/test-auth-expiry-reminder.sh +303 -0
- package/tests/test-auth-macos-sleep-detector.sh +254 -0
- package/tests/test-auth-path-detector.sh +232 -0
- package/tests/test-auth-status-checker.sh +233 -0
- package/tests/test-authorized-reconfirmation-detector.sh +280 -0
- package/tests/test-background-cost-launch-guard.sh +62 -0
- package/tests/test-bash-allowlist-secondary-check.sh +143 -0
- package/tests/test-bash-fanout-bounded-rewriter.sh +186 -0
- package/tests/test-bash-project-fence.sh +127 -0
- package/tests/test-bg-permission-prompt-warner.sh +221 -0
- package/tests/test-binary-read-detector.sh +173 -0
- package/tests/test-broad-prefix-session-trap-warner.sh +167 -0
- package/tests/test-bulk-delete-target-resolution.sh +49 -0
- package/tests/test-bypass-mode-effective-verifier.sh +179 -0
- package/tests/test-cache-creation-drift-detector.sh +244 -0
- package/tests/test-cache-residue-detector.sh +429 -0
- package/tests/test-cache-tier-logger.sh +59 -0
- package/tests/test-cache-ttl-eviction-detector.sh +282 -0
- package/tests/test-cch-sentinel-precommit-guard.sh +64 -0
- package/tests/test-claim-verify-detector.sh +222 -0
- package/tests/test-claim-vs-caveat-checker.sh +134 -0
- package/tests/test-claude-code-version-pin-advisor.sh +353 -0
- package/tests/test-claude-md-reinjector.sh +173 -0
- package/tests/test-claude-md-size-monitor.sh +107 -0
- package/tests/test-claude-update-budget-guard.sh +132 -0
- package/tests/test-claude-update-smart.sh +91 -0
- package/tests/test-claudemd-tool-prohibition.sh +239 -0
- package/tests/test-cli-config-pinning-detector.sh +233 -0
- package/tests/test-cliff-countdown-advisor.sh +138 -0
- package/tests/test-closure-word-verify-gate.sh +158 -0
- package/tests/test-commit-all-scope-guard.sh +73 -0
- package/tests/test-commitment-carry-forward-arrest.sh +250 -0
- package/tests/test-compact-dispatch-watchdog.sh +147 -0
- package/tests/test-completion-claim-without-verification-detector.sh +326 -0
- package/tests/test-compound-bash-permission-resolver.sh +264 -0
- package/tests/test-compound-command-deny-enforcer.sh +67 -0
- package/tests/test-context-length-budget.sh +68 -0
- package/tests/test-context-usage-drift-alert.sh +52 -22
- package/tests/test-cost-incident-self-audit.sh +94 -0
- package/tests/test-cowork-claude-md-load-checker.sh +257 -0
- package/tests/test-cowork-claudemd-helper.sh +141 -0
- package/tests/test-cowork-fuse-staleness-watcher.sh +287 -0
- package/tests/test-cowork-hook-absence-warner.sh +230 -0
- package/tests/test-cowork-model-picker-advisor.sh +317 -0
- package/tests/test-credential-exfil-guard.sh +62 -0
- package/tests/test-cron-create-receipt.sh +165 -0
- package/tests/test-cross-product-mcp-spawn-detector.sh +112 -0
- package/tests/test-daemon-zombie-worker-surfacer.sh +71 -0
- package/tests/test-daily-cost-guard.sh +131 -0
- package/tests/test-deny-rule-integrity-verifier.sh +288 -0
- package/tests/test-deployment-readback-gate.sh +92 -0
- package/tests/test-deployment-readback-gh-adapter.sh +163 -0
- package/tests/test-disabled-feature-toggle-advisor.sh +78 -0
- package/tests/test-dispatch-allowlist-preflight.sh +385 -0
- package/tests/test-dispatch-liveness-watchdog.sh +319 -0
- package/tests/test-dispatch-receipt.sh +314 -0
- package/tests/test-dotenv-anthropic-key-billing-guard.sh +160 -0
- package/tests/test-echo-probe-spam-detector.sh +143 -0
- package/tests/test-edit-quote-ambiguity-guard.sh +75 -0
- package/tests/test-ephemeral-container-detector.sh +251 -0
- package/tests/test-evidence-claim-gate.sh +291 -0
- package/tests/test-extended-thinking-loop-guard.sh +289 -0
- package/tests/test-extended-thinking-resume-warning.sh +240 -0
- package/tests/test-extended-thinking-tool-use-mismatch-detector.sh +236 -0
- package/tests/test-fabricated-command-detector.sh +296 -0
- package/tests/test-false-disk-full-guard.sh +36 -0
- package/tests/test-feature-deprecation-detector.sh +117 -0
- package/tests/test-forbidden-response-phrase-detector.sh +246 -0
- package/tests/test-git-operations-require-approval.sh +53 -0
- package/tests/test-git-push-blast-radius-guard.sh +115 -0
- package/tests/test-goal-iteration-limit-warner.sh +128 -0
- package/tests/test-growthbook-flag-monitor.sh +428 -0
- package/tests/test-halt-signal-detector.sh +120 -0
- package/tests/test-hook-self-disable-detector.sh +232 -0
- package/tests/test-iteration-quality-degradation-detector.sh +244 -0
- package/tests/test-long-session-malformed-tool-call-detector.sh +185 -0
- package/tests/test-marketplace-plugins.sh +193 -0
- package/tests/test-mcp-misdiagnosis-arrest.sh +235 -0
- package/tests/test-mcp-orphan-leak-warner.sh +71 -0
- package/tests/test-mcp-permissions-ask-warner.sh +207 -0
- package/tests/test-mcp-routine-approval-detector.sh +229 -0
- package/tests/test-mcp-startup-bloat-detector.sh +141 -0
- package/tests/test-mcp-stdio-compatibility-test.sh +112 -0
- package/tests/test-memory-chain-audit.sh +59 -0
- package/tests/test-memory-orphan-detector.sh +227 -0
- package/tests/test-model-swap-suggester.sh +368 -0
- package/tests/test-model-version-lock.sh +83 -0
- package/tests/test-multi-file-plan-routing-gate.sh +277 -0
- package/tests/test-multi-window-auth-drift-detector.sh +262 -0
- package/tests/test-nested-spawn-inflight-guard.sh +281 -0
- package/tests/test-no-force-flag.sh +49 -0
- package/tests/test-non-english-quality-warner.sh +229 -0
- package/tests/test-oauth-refresh-monitor.sh +311 -0
- package/tests/test-opus-degradation-tracker.sh +274 -0
- package/tests/test-opus48-routine-task-warning.sh +201 -0
- package/tests/test-opus48-thinking-wedge-advisor.sh +249 -0
- package/tests/test-output-token-spike-detector.sh +315 -0
- package/tests/test-parallel-batch-size-limiter.sh +228 -0
- package/tests/test-parallel-cascade-detector.sh +191 -0
- package/tests/test-partial-view-claim-arrest.sh +272 -0
- package/tests/test-per-prompt-token-cap.sh +136 -0
- package/tests/test-persisted-1m-model-advisor.sh +71 -0
- package/tests/test-plugin-hooks-json-bloat-detector.sh +267 -0
- package/tests/test-post-edit-disk-verify.sh +294 -0
- package/tests/test-pre-bash-sed-line-ending-windows.sh +108 -0
- package/tests/test-pre-execution-claim-detector.sh +366 -0
- package/tests/test-private-key-backup-and-git-credentials.sh +100 -0
- package/tests/test-proxy-capture-suggester.sh +214 -0
- package/tests/test-public-artefact-socratic-narrowing.sh +346 -0
- package/tests/test-quota-anomaly-detector.sh +321 -0
- package/tests/test-quota-reset-audit.sh +81 -0
- package/tests/test-read-loop-detector.sh +137 -0
- package/tests/test-read-once-guard.sh +85 -0
- package/tests/test-receipts-aggregate.sh +143 -0
- package/tests/test-redirect-fragment-warner.sh +130 -0
- package/tests/test-redundant-read-blocker.sh +155 -0
- package/tests/test-refusal-arrest-gate.sh +303 -0
- package/tests/test-remote-control-billing-classifier.sh +125 -0
- package/tests/test-resume-drift-watcher.sh +65 -0
- package/tests/test-rg-replace-flag-detector.sh +175 -0
- package/tests/test-rhetorical-verification-prompt-detector.sh +237 -0
- package/tests/test-rm-safety-net.sh +115 -0
- package/tests/test-route-handler-emptiness-gate.sh +300 -0
- package/tests/test-runtime-binary-change-detector.sh +196 -0
- package/tests/test-same-command-repeat-detector.sh +165 -0
- package/tests/test-same-correction-arrest.sh +165 -0
- package/tests/test-scope-expansion-receipt.sh +207 -0
- package/tests/test-server-side-prompt-injection-detector.sh +181 -0
- package/tests/test-session-persistence-verifier.sh +128 -0
- package/tests/test-session-rate-monitor.sh +284 -0
- package/tests/test-session-start-quota-status.sh +210 -0
- package/tests/test-session-start-safety-check.sh +138 -0
- package/tests/test-settings-hooks-key-restorer.sh +257 -0
- package/tests/test-settings-json-health-check.sh +70 -0
- package/tests/test-settings-regression-tester.sh +197 -0
- package/tests/test-skill-cumulative-size-detector.sh +198 -0
- package/tests/test-skill-description-drop-detector.sh +236 -0
- package/tests/test-skill-truncation-detector.sh +263 -0
- package/tests/test-skills-context-recorder.sh +329 -0
- package/tests/test-skills-load-verifier.sh +184 -0
- package/tests/test-skills-settings-validator.sh +242 -0
- package/tests/test-sonnet-45-deprecation-helper.sh +150 -0
- package/tests/test-spurious-malformed-notice-detector.sh +290 -0
- package/tests/test-sql-bulk-delete-warn.sh +123 -0
- package/tests/test-ssh-key-protect.sh +81 -0
- package/tests/test-stale-temp-settings-detector.sh +161 -0
- package/tests/test-standing-rule-session-surfacer.sh +215 -0
- package/tests/test-starter-hooks-manifest.sh +96 -0
- package/tests/test-stop-hook-sigterm-wrapper.sh +258 -0
- package/tests/test-strict-allowlist.sh +72 -0
- package/tests/test-subagent-boundary-precheck.sh +137 -0
- package/tests/test-subagent-closure-verify-gate.sh +193 -0
- package/tests/test-subagent-destructive-git-guard.sh +86 -0
- package/tests/test-subagent-identity-leak-guard.sh +62 -0
- package/tests/test-subagent-identity-reassertion.sh +61 -0
- package/tests/test-subagent-inheritance-tester.sh +319 -0
- package/tests/test-subagent-permission-mode-guard.sh +61 -0
- package/tests/test-subagent-spawn-verification-enforcer.sh +60 -0
- package/tests/test-subagent-tool-allowlist-enforcer.sh +62 -0
- package/tests/test-subagent-tool-call-limiter.sh +97 -0
- package/tests/test-subscription-api-billing-warner.sh +138 -0
- package/tests/test-subscription-bypass-detector.sh +255 -0
- package/tests/test-temporal-suggestion-detector.sh +183 -0
- package/tests/test-thinking-budget-effort-mismatch-detector.sh +327 -0
- package/tests/test-tokenizer-ratio-alert-corpus.sh +77 -0
- package/tests/test-tokenizer-ratio-alert.sh +93 -0
- package/tests/test-tool-result-correlation-checker.sh +264 -0
- package/tests/test-tool-result-size-guard.sh +121 -0
- package/tests/test-transcript-contamination-detector.sh +264 -0
- package/tests/test-trigger-header-parsing.sh +151 -0
- package/tests/test-ultrareview-large-diff-advisor.sh +192 -0
- package/tests/test-userprompt-submit-receipt.sh +181 -0
- package/tests/test-verify-after-publish-reminder.sh +184 -0
- package/tests/test-version-bump-detector.sh +212 -0
- package/tests/test-version-regression-warner.sh +165 -0
- package/tests/test-warn-cron-cost-trap.sh +82 -0
- package/tests/test-webfetch-runaway-guard.sh +77 -0
- package/tests/test-wellness-break-reminder.sh +138 -0
- package/tests/test-windows-destructive-command-guard.sh +169 -0
- package/tests/test-windows-python-stub-detector.sh +146 -0
- package/tests/test-worktree-edit-boundary-guard.sh +219 -0
- package/tests/test-write-byte-integrity-verifier.sh +192 -0
- package/tests/test-xml-format-leak-detector.sh +299 -0
- package/tests/trustfall-mcp-injection-guard.test.sh +183 -0
- package/tests/workspace-lease-guard.test.sh +144 -0
- package/tests/worktree-escape-write-guard.test.sh +68 -0
- package/tests/worktree-hooks-path-fix.test.sh +181 -0
- package/tests/worktree-parent-write-guard.test.sh +51 -0
- package/tests/write-empty-content-guard.test.sh +39 -0
- package/tests/write-nul-corruption-detector.test.sh +29 -0
- package/tests/write-overwrite-unread-content.test.sh +178 -0
- package/tests/wsl-host-disk-space-guard.test.sh +55 -0
package/index.mjs
CHANGED
|
@@ -1,25 +1,87 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, copyFileSync, unlinkSync } from 'fs';
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, copyFileSync, unlinkSync, readdirSync } from 'fs';
|
|
4
4
|
import { join, dirname } from 'path';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { createInterface } from 'readline';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
|
+
import { createHash } from 'crypto';
|
|
8
9
|
|
|
9
10
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
11
|
const HOME = homedir();
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
// Install target resolves to CLAUDE_PROJECT_DIR if set (per-profile / per-project
|
|
14
|
+
// installs where the env var points at the directory that hosts .claude/).
|
|
15
|
+
// Falls back to HOME for the historical default behavior so existing single-profile
|
|
16
|
+
// users see no change. Resolves issue #145.
|
|
17
|
+
// Logs and profiles intentionally remain HOME-based (see joins later in this file)
|
|
18
|
+
// because they aggregate state across projects; making them per-project would
|
|
19
|
+
// silently change semantics for anyone running multiple projects today.
|
|
20
|
+
const CLAUDE_BASE = process.env.CLAUDE_PROJECT_DIR || HOME;
|
|
21
|
+
const HOOKS_DIR = join(CLAUDE_BASE, '.claude', 'hooks');
|
|
22
|
+
const SETTINGS_PATH = join(CLAUDE_BASE, '.claude', 'settings.json');
|
|
13
23
|
|
|
14
24
|
// Convert Windows backslash paths to bash-compatible forward slashes
|
|
15
25
|
const toBashPath = (p) => p.replace(/\\/g, '/');
|
|
16
26
|
|
|
27
|
+
// One block record is "[timestamp] BLOCKED: reason | cmd: command".
|
|
28
|
+
// A blocked command may itself span several lines (heredocs, multi-line scripts),
|
|
29
|
+
// and those continuation lines land in the log without a timestamp. Counting raw
|
|
30
|
+
// non-empty lines therefore reports more blocks than actually happened — measured
|
|
31
|
+
// at +7,480 on 100,897 real records (+7.4%) in a 5-month log. Anchor on the record
|
|
32
|
+
// shape instead of the line count so the numbers we show users are the real ones.
|
|
33
|
+
const BLOCK_RECORD_RE = /^\[([^\]]+)\]\s*BLOCKED:\s*(.+?)\s*\|\s*cmd:\s*(.+)$/;
|
|
34
|
+
const readBlockRecords = (path) => {
|
|
35
|
+
if (!existsSync(path)) return [];
|
|
36
|
+
return readFileSync(path, 'utf-8').split('\n').filter(l => BLOCK_RECORD_RE.test(l));
|
|
37
|
+
};
|
|
38
|
+
|
|
17
39
|
const c = {
|
|
18
40
|
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
19
41
|
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
|
|
20
42
|
blue: '\x1b[36m',
|
|
21
43
|
};
|
|
22
44
|
|
|
45
|
+
// settings.json が存在するのに JSON として壊れている時、それを {} として扱ってはいけない。
|
|
46
|
+
// 呼び出し側は直後に settings を書き戻すので、利用者のフック・権限・環境変数が
|
|
47
|
+
// まるごと新しいオブジェクトで上書きされて消える。壊れているなら書かずに止める。
|
|
48
|
+
// いちばん多い引き金は、記事や README から写した設定例の先頭にある
|
|
49
|
+
// `// path/to/file` というコメント行で、これは JSON として不正になる。
|
|
50
|
+
function readSettingsForWrite() {
|
|
51
|
+
if (!existsSync(SETTINGS_PATH)) return {};
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.error('');
|
|
56
|
+
console.error(c.red + ' ✗' + c.reset + ' ' + SETTINGS_PATH + ' is not valid JSON (' + e.message + ')');
|
|
57
|
+
console.error(' Refusing to write: doing so would replace every hook, permission');
|
|
58
|
+
console.error(' and env var you already have with a fresh file.');
|
|
59
|
+
console.error('');
|
|
60
|
+
console.error(' Check it with: python3 -m json.tool ' + SETTINGS_PATH);
|
|
61
|
+
console.error(' A leading "// path/to/file" comment line is the usual cause.');
|
|
62
|
+
console.error('');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
// Days remaining until Anthropic's 2026-06-15 programmatic-billing split.
|
|
69
|
+
// Computed at run time so the CLI banner stays accurate without daily edits —
|
|
70
|
+
// hardcoded counts drifted (lines once read "23 days" and "20 days" on the same
|
|
71
|
+
// install) and that drift is the exact claim-vs-reality pattern CVH warns about.
|
|
72
|
+
// Compares calendar dates in the local timezone so the count matches what users
|
|
73
|
+
// would naturally count off a calendar (UTC-based math drifts by a day for JST).
|
|
74
|
+
function daysUntilJune15() {
|
|
75
|
+
const now = new Date();
|
|
76
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
77
|
+
const target = new Date(2026, 5, 15);
|
|
78
|
+
const diff = Math.round((target - today) / 86400000);
|
|
79
|
+
// Return the raw signed difference so callers can distinguish "still upcoming"
|
|
80
|
+
// (positive) from "already happened" (zero or negative). Clamping to 0 here
|
|
81
|
+
// made the post-install banner say "in 0 days" forever after June 15, 2026.
|
|
82
|
+
return diff;
|
|
83
|
+
}
|
|
84
|
+
|
|
23
85
|
const SCRIPTS = JSON.parse(readFileSync(join(__dirname, 'scripts.json'), 'utf-8'));
|
|
24
86
|
|
|
25
87
|
const HOOKS = {
|
|
@@ -76,6 +138,16 @@ const LEARN = process.argv.includes('--learn');
|
|
|
76
138
|
const SCAN = process.argv.includes('--scan');
|
|
77
139
|
const FULL = process.argv.includes('--full');
|
|
78
140
|
const DOCTOR = process.argv.includes('--doctor');
|
|
141
|
+
// Installing a hook copies the file. Nothing ever copies it back, so a hook
|
|
142
|
+
// installed in March keeps running its March logic forever — including bugs
|
|
143
|
+
// fixed here months ago. Nothing in the CLI could tell you that until now.
|
|
144
|
+
const OUTDATED = process.argv.includes('--outdated');
|
|
145
|
+
// --outdated can now name a stale core guard, but core guards are strings inside
|
|
146
|
+
// scripts.json rather than files under examples/, so --install-example cannot
|
|
147
|
+
// fetch one. Without a way to read the shipped body, the report would name a
|
|
148
|
+
// problem the reader has no way to act on.
|
|
149
|
+
const SHOW_CORE_IDX = process.argv.findIndex(a => a === '--show-core');
|
|
150
|
+
const SHOW_CORE = SHOW_CORE_IDX !== -1 ? process.argv[SHOW_CORE_IDX + 1] : null;
|
|
79
151
|
const WATCH = process.argv.includes('--watch');
|
|
80
152
|
const EXPORT = process.argv.includes('--export');
|
|
81
153
|
const IMPORT_IDX = process.argv.findIndex(a => a === '--import');
|
|
@@ -86,6 +158,7 @@ const LINT = process.argv.includes('--lint');
|
|
|
86
158
|
const DIFF_IDX = process.argv.findIndex(a => a === '--diff');
|
|
87
159
|
const DIFF_FILE = DIFF_IDX !== -1 ? process.argv[DIFF_IDX + 1] : null;
|
|
88
160
|
const SHARE = process.argv.includes('--share');
|
|
161
|
+
const SCORECARD = process.argv.includes('--scorecard') || process.argv.includes('--card');
|
|
89
162
|
const BENCHMARK = process.argv.includes('--benchmark');
|
|
90
163
|
const DASHBOARD = process.argv.includes('--dashboard');
|
|
91
164
|
const ISSUES = process.argv.includes('--issues');
|
|
@@ -94,6 +167,11 @@ const GENERATE_CI = process.argv.includes('--generate-ci');
|
|
|
94
167
|
const REPORT = process.argv.includes('--report');
|
|
95
168
|
const QUICKFIX = process.argv.includes('--quickfix');
|
|
96
169
|
const SHIELD = process.argv.includes('--shield');
|
|
170
|
+
// --shield は以前、~/.claude/hooks/ にある *すべて* の .sh を settings.json へ登録していた。
|
|
171
|
+
// 出所を問わないので、他のツールが置いたフックも、利用者が意図して登録を外したフックも、
|
|
172
|
+
// 走らせるたびに黙って復活した。実害3回(2026-06-10 / 2026-07-27 に2回)。
|
|
173
|
+
// 既定を「今回このコマンドが設置したものだけ」に変え、以前の挙動はこの旗で選ぶ。
|
|
174
|
+
const ADOPT_EXISTING = process.argv.includes('--adopt-existing');
|
|
97
175
|
const OPUS47 = process.argv.includes('--opus47');
|
|
98
176
|
const ANALYZE = process.argv.includes('--analyze');
|
|
99
177
|
const TEAM = process.argv.includes('--team');
|
|
@@ -110,6 +188,7 @@ const PROFILE = PROFILE_IDX !== -1 ? process.argv[PROFILE_IDX + 1] : null;
|
|
|
110
188
|
const COMPARE_IDX = process.argv.findIndex(a => a === '--compare');
|
|
111
189
|
const COMPARE = COMPARE_IDX !== -1 ? { a: process.argv[COMPARE_IDX + 1], b: process.argv[COMPARE_IDX + 2] } : null;
|
|
112
190
|
const REPLAY = process.argv.includes('--replay');
|
|
191
|
+
const BLINDSPOTS = process.argv.includes('--blindspots');
|
|
113
192
|
const SAVE_PROFILE_IDX = process.argv.findIndex(a => a === '--save-profile');
|
|
114
193
|
const SAVE_PROFILE = SAVE_PROFILE_IDX !== -1 ? process.argv[SAVE_PROFILE_IDX + 1] : null;
|
|
115
194
|
const CREATE_IDX = process.argv.findIndex(a => a === '--create');
|
|
@@ -137,9 +216,11 @@ if (HELP) {
|
|
|
137
216
|
cc-safe-setup — Make Claude Code safe for autonomous operation
|
|
138
217
|
|
|
139
218
|
Quick Start:
|
|
140
|
-
npx cc-safe-setup
|
|
141
|
-
npx cc-safe-setup --shield
|
|
142
|
-
npx cc-safe-setup --doctor
|
|
219
|
+
npx github:yurukusa/cc-safe-setup Install 8 safety hooks (30 sec)
|
|
220
|
+
npx github:yurukusa/cc-safe-setup --shield Maximum safety — one command
|
|
221
|
+
npx github:yurukusa/cc-safe-setup --doctor Diagnose hook problems
|
|
222
|
+
(the npm entry is frozen at 29.8.0 / 2026-04-20 and lets twenty-five
|
|
223
|
+
command shapes through that the current code blocks — install from GitHub)
|
|
143
224
|
|
|
144
225
|
Protect:
|
|
145
226
|
--protect .env Block edits to a specific file
|
|
@@ -149,7 +230,7 @@ if (HELP) {
|
|
|
149
230
|
--safe-mode Emergency: disable all hooks
|
|
150
231
|
|
|
151
232
|
Install & Configure:
|
|
152
|
-
--install-example <name> Install from
|
|
233
|
+
--install-example <name> Install from 916 example hooks
|
|
153
234
|
--examples Browse examples by category
|
|
154
235
|
--from-claudemd Convert CLAUDE.md rules into hooks
|
|
155
236
|
--rules [file] Compile YAML rules into hooks
|
|
@@ -159,11 +240,14 @@ if (HELP) {
|
|
|
159
240
|
|
|
160
241
|
Diagnose & Monitor:
|
|
161
242
|
--status / --verify Check installed hooks / test them
|
|
243
|
+
--outdated Which installed hooks differ from current ones
|
|
162
244
|
--doctor 13-point diagnostic
|
|
163
245
|
--audit [--fix] [--json] Safety score 0-100
|
|
246
|
+
--scorecard [--json] Shareable safety scorecard (screenshot it)
|
|
164
247
|
--watch Live blocked command feed
|
|
165
248
|
--health Hook health dashboard
|
|
166
249
|
--suggest Predict risks from project analysis
|
|
250
|
+
--blindspots What your guards never see, vs. your own sessions
|
|
167
251
|
|
|
168
252
|
Manage:
|
|
169
253
|
--dry-run / --uninstall Preview / remove hooks
|
|
@@ -173,11 +257,11 @@ if (HELP) {
|
|
|
173
257
|
|
|
174
258
|
More: --create, --why, --replay, --score, --changelog, --analyze,
|
|
175
259
|
--benchmark, --dashboard, --migrate-from, --init-project
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
260
|
+
--quickfix Auto-detect and fix common Claude Code problems
|
|
261
|
+
--stats Block statistics and patterns report
|
|
262
|
+
--export Export hooks config for team sharing
|
|
263
|
+
--import <file> Import hooks from exported config
|
|
264
|
+
--help Show this help
|
|
181
265
|
|
|
182
266
|
Hooks installed:
|
|
183
267
|
destructive-guard Blocks rm -rf, git reset --hard, NFS mount detection
|
|
@@ -196,6 +280,9 @@ if (HELP) {
|
|
|
196
280
|
Token Checkup: https://yurukusa.github.io/cc-safe-setup/token-checkup.html
|
|
197
281
|
Token Book: https://yurukusa.github.io/cc-safe-setup/token-book.html
|
|
198
282
|
Safety Guide: https://zenn.dev/yurukusa/books/6076c23b1cb18b
|
|
283
|
+
|
|
284
|
+
Hitting nested sub-agent dispatch (Task tool absent from sub-agent contexts)?
|
|
285
|
+
See: https://gist.github.com/yurukusa/cf477f03f03d9f93c184f1fb7d894f96
|
|
199
286
|
`);
|
|
200
287
|
process.exit(0);
|
|
201
288
|
}
|
|
@@ -304,15 +391,25 @@ function status() {
|
|
|
304
391
|
console.log(c.bold + ' All ' + installed + ' hooks installed.' + c.reset);
|
|
305
392
|
} else {
|
|
306
393
|
console.log(c.bold + ' ' + installed + '/' + Object.keys(HOOKS).length + ' hooks installed.' + c.reset);
|
|
307
|
-
console.log(' ' + c.dim + 'Run: npx cc-safe-setup' + c.reset);
|
|
394
|
+
console.log(' ' + c.dim + 'Run: npx github:yurukusa/cc-safe-setup' + c.reset);
|
|
308
395
|
}
|
|
309
396
|
if (installedExamples.length > 0) {
|
|
310
397
|
console.log(' ' + c.dim + '+ ' + installedExamples.length + ' example hooks' + c.reset);
|
|
311
398
|
}
|
|
312
399
|
console.log();
|
|
313
|
-
console.log(c.dim + ' Tip: --validate to check health · --
|
|
400
|
+
console.log(c.dim + ' Tip: --validate to check health · --scorecard for a shareable safety card · --shield for max safety' + c.reset);
|
|
401
|
+
console.log();
|
|
402
|
+
console.log(' ' + c.dim + 'On a team? Roll one shared safety baseline out across every repo (free playbook):' + c.reset);
|
|
403
|
+
console.log(' ' + c.dim + ' https://yurukusa.github.io/cc-safe-setup/team-rollout-guide.html' + c.reset);
|
|
314
404
|
console.log();
|
|
315
405
|
|
|
406
|
+
const daysLeft1 = daysUntilJune15();
|
|
407
|
+
const june15msg1 = daysLeft1 > 0
|
|
408
|
+
? 'Anthropic splits programmatic billing in ' + daysLeft1 + (daysLeft1 === 1 ? ' day' : ' days') + '.'
|
|
409
|
+
: 'Anthropic\'s programmatic billing split is in effect (since June 15, 2026).';
|
|
410
|
+
console.log(' ' + c.yellow + '⚠ June 15:' + c.reset + c.dim + ' ' + june15msg1 + c.reset);
|
|
411
|
+
console.log(' ' + c.dim + ' Diagnose any unexpected charge: https://htmlpreview.github.io/?https://gist.githubusercontent.com/yurukusa/d3a0e2403cc4078aa0183400c137d824/raw/wrong-charge-diagnostic.html' + c.reset);
|
|
412
|
+
console.log();
|
|
316
413
|
// Exit code for CI: 0 = all installed, 1 = missing hooks
|
|
317
414
|
if (missing > 0) process.exit(1);
|
|
318
415
|
}
|
|
@@ -337,8 +434,14 @@ async function verify() {
|
|
|
337
434
|
{ hook: 'branch-guard', input: '{"tool_input":{"command":"git push --force origin feature"}}', expect: 2, desc: 'blocks force-push' },
|
|
338
435
|
{ hook: 'destructive-guard', input: '{"tool_input":{"command":"git reset --hard HEAD~5"}}', expect: 2, desc: 'blocks git reset --hard' },
|
|
339
436
|
{ hook: 'destructive-guard', input: '{"tool_input":{"command":"sudo rm -rf /var"}}', expect: 2, desc: 'blocks sudo + destructive' },
|
|
437
|
+
{ hook: 'destructive-guard', input: '{"tool_input":{"command":"cd /tmp && sudo rm -rf myproject"}}', expect: 2, desc: 'blocks sudo after a separator' },
|
|
340
438
|
{ hook: 'destructive-guard', input: '{"tool_input":{"command":"Remove-Item -Recurse -Force *"}}', expect: 2, desc: 'blocks PowerShell Remove-Item' },
|
|
341
439
|
{ hook: 'destructive-guard', input: '{"tool_input":{"command":"Remove-Item ./file.txt"}}', expect: 0, desc: 'allows single file Remove-Item' },
|
|
440
|
+
{ hook: 'branch-guard', input: '{"tool_input":{"command":"npm test && git push --force origin main"}}', expect: 2, desc: 'blocks chained force-push (compound)' },
|
|
441
|
+
{ hook: 'secret-guard', input: '{"tool_input":{"command":"cd app && git add .env"}}', expect: 2, desc: 'blocks chained git add .env (compound)' },
|
|
442
|
+
{ hook: 'destructive-guard', input: '{"tool_input":{"command":"rm --recursive --force /"}}', expect: 2, desc: 'blocks rm long-form flags' },
|
|
443
|
+
{ hook: 'destructive-guard', input: '{"tool_input":{"command":"find / -print0 | xargs -0 rm -rf"}}', expect: 2, desc: 'blocks xargs rm -rf' },
|
|
444
|
+
{ hook: 'destructive-guard', input: '{"tool_input":{"command":"echo Y20gLXJmIC8= | base64 -d | bash"}}', expect: 2, desc: 'blocks base64-to-shell obfuscation' },
|
|
342
445
|
];
|
|
343
446
|
|
|
344
447
|
let pass = 0, fail = 0;
|
|
@@ -372,7 +475,7 @@ async function verify() {
|
|
|
372
475
|
console.log();
|
|
373
476
|
console.log(c.bold + ' ' + pass + '/' + (pass + fail) + ' tests passed.' + c.reset);
|
|
374
477
|
if (fail > 0) {
|
|
375
|
-
console.log(' ' + c.red + fail + ' failures.' + c.reset + ' Run ' + c.blue + 'npx cc-safe-setup' + c.reset + ' to reinstall.');
|
|
478
|
+
console.log(' ' + c.red + fail + ' failures.' + c.reset + ' Run ' + c.blue + 'npx github:yurukusa/cc-safe-setup' + c.reset + ' to reinstall.');
|
|
376
479
|
process.exit(1);
|
|
377
480
|
}
|
|
378
481
|
console.log();
|
|
@@ -651,6 +754,7 @@ function examples() {
|
|
|
651
754
|
'schema-migration-guard.sh': 'Warn on database schema migrations without backup',
|
|
652
755
|
'scope-guard.sh': 'Block file operations outside the project directory',
|
|
653
756
|
'secret-file-read-guard.sh': 'Block Read/Grep of files containing secrets',
|
|
757
|
+
'secret-file-write-guard.sh': 'Block Bash writes into .env, keys and credential files (the bundled Write|Edit guard cannot see them)',
|
|
654
758
|
'self-modify-bypass-guard.sh': 'Auto-allow .claude/ writes in bypass mode',
|
|
655
759
|
'sensitive-file-read-guard.sh': 'Block reading sensitive system/user files',
|
|
656
760
|
'sensitive-log-guard.sh': 'Sensitive Log Guard',
|
|
@@ -663,7 +767,7 @@ function examples() {
|
|
|
663
767
|
'ssh-key-protect.sh': 'Block reading/copying SSH private keys',
|
|
664
768
|
'staged-secret-scan.sh': 'Block git commit if staged diff contains secrets',
|
|
665
769
|
'strict-allowlist.sh': 'Only allow explicitly permitted commands',
|
|
666
|
-
'strip-coauthored-by.sh': '
|
|
770
|
+
'strip-coauthored-by.sh': 'Warn on Co-Authored-By trailers; block commits leaking the private Claude-Session URL (#69669)',
|
|
667
771
|
'symlink-guard.sh': 'Detect symlink/junction traversal before rm',
|
|
668
772
|
'symlink-protect.sh': 'Symlink Protect',
|
|
669
773
|
'system-package-guard.sh': 'Block system-level package installations',
|
|
@@ -739,6 +843,7 @@ function examples() {
|
|
|
739
843
|
'webfetch-domain-allow.sh': 'Auto-approve WebFetch for allowed domains',
|
|
740
844
|
},
|
|
741
845
|
'Quality': {
|
|
846
|
+
'grep-nul-false-negative-guard.sh': 'Warn when Grep reports "no matches" but files under the searched path contain NUL bytes, which Grep skips silently',
|
|
742
847
|
'bash-secret-output-detector.sh': 'Warn when Bash output contains secrets',
|
|
743
848
|
'bashrc-safety-check.sh': 'Warn about .bashrc lines that hang in non-interactive shells',
|
|
744
849
|
'branch-name-check.sh': 'Warn when creating branches with non-standard names',
|
|
@@ -914,6 +1019,8 @@ function examples() {
|
|
|
914
1019
|
'yaml-syntax-check.sh': 'Validate YAML after editing',
|
|
915
1020
|
},
|
|
916
1021
|
'Monitoring': {
|
|
1022
|
+
'model-version-alert.sh': 'Every 50th hook run, read the newest session log and alert when the active model is an older Opus 4.7 build',
|
|
1023
|
+
'worktree-create-log.sh': 'Append branch and path to ~/.claude/worktree-audit.log whenever a worktree is created',
|
|
917
1024
|
'api-rate-limit-tracker.sh': 'Track API call frequency and warn on burst',
|
|
918
1025
|
'cost-tracker.sh': 'Estimate session token cost',
|
|
919
1026
|
'cross-session-error-log.sh': 'Persist error patterns across sessions',
|
|
@@ -942,6 +1049,7 @@ function examples() {
|
|
|
942
1049
|
'subagent-error-detector.sh': 'Detect failed subagent results',
|
|
943
1050
|
},
|
|
944
1051
|
'Recovery': {
|
|
1052
|
+
'auto-push-session-branch.sh': 'Push the current branch to its upstream after each turn, skipping main/master/develop/release and detached HEAD; never fails the turn if the push is rejected',
|
|
945
1053
|
'auto-checkpoint.sh': 'Auto-commit after every edit for rollback protection',
|
|
946
1054
|
'auto-git-checkpoint.sh': 'Auto Git Checkpoint',
|
|
947
1055
|
'auto-snapshot.sh': 'Automatic file snapshots before every edit',
|
|
@@ -1019,7 +1127,7 @@ function examples() {
|
|
|
1019
1127
|
'skill-gate.sh': 'Skill Gate',
|
|
1020
1128
|
'stale-branch-guard.sh': 'Warn when working on a stale branch',
|
|
1021
1129
|
'stale-env-guard.sh': 'Warn when .env files are very old',
|
|
1022
|
-
'system-message-workaround.sh': '
|
|
1130
|
+
'system-message-workaround.sh': 'Put a non-blocking hook warning in front of the user (stderr)',
|
|
1023
1131
|
'temp-file-cleanup-stop.sh': 'Clean up tmpclaude-* files on session end',
|
|
1024
1132
|
'temp-file-cleanup.sh': 'Stop hook',
|
|
1025
1133
|
'tmp-cleanup.sh': 'Clean up /tmp/claude-*-cwd temp files',
|
|
@@ -1039,6 +1147,7 @@ function examples() {
|
|
|
1039
1147
|
'subagent-budget-guard.sh': 'Subagent Budget Guard',
|
|
1040
1148
|
'subagent-claudemd-inject.sh': 'Inject CLAUDE.md rules into subagent prompts',
|
|
1041
1149
|
'subagent-context-size-guard.sh': 'Warn on thin subagent prompts',
|
|
1150
|
+
'subagent-forged-system-reminder-guard.sh': 'Quarantine forged <system-reminder> markup in a subagent result',
|
|
1042
1151
|
'subagent-scope-guard.sh': 'Limit subagent file access scope',
|
|
1043
1152
|
'subagent-scope-validator.sh': 'Validate subagent task scope before launch',
|
|
1044
1153
|
'subagent-tool-call-limiter.sh': 'Limit total tool calls per session',
|
|
@@ -1056,7 +1165,14 @@ function examples() {
|
|
|
1056
1165
|
|
|
1057
1166
|
console.log();
|
|
1058
1167
|
console.log(c.bold + ' cc-safe-setup --examples' + c.reset + (filter ? ' ' + filter : ''));
|
|
1059
|
-
|
|
1168
|
+
// The catalogue below is hand-maintained; examples/ is the source of truth.
|
|
1169
|
+
// Count the directory so the header never understates what ships.
|
|
1170
|
+
const catalogued = new Set(Object.values(CATEGORIES).flatMap((cat) => Object.keys(cat)));
|
|
1171
|
+
let onDisk = [];
|
|
1172
|
+
try {
|
|
1173
|
+
onDisk = readdirSync(join(__dirname, 'examples')).filter((f) => f.endsWith('.sh'));
|
|
1174
|
+
} catch { /* running from a package without examples/: fall back to the catalogue */ }
|
|
1175
|
+
const totalExamples = onDisk.length || catalogued.size;
|
|
1060
1176
|
console.log(c.dim + ` ${totalExamples} hooks beyond the 8 built-in ones` + c.reset);
|
|
1061
1177
|
if (filter) console.log(c.dim + ' Filter: ' + filter + c.reset);
|
|
1062
1178
|
console.log();
|
|
@@ -1080,6 +1196,27 @@ function examples() {
|
|
|
1080
1196
|
console.log();
|
|
1081
1197
|
}
|
|
1082
1198
|
|
|
1199
|
+
// examples/ に在るのに目録へ登録されていないものを、実体から拾って並べる。
|
|
1200
|
+
// 目録は手で維持しているので放っておくと置き去りが増える(2026-08-02の実測で258本が
|
|
1201
|
+
// 「--install-example では入るのに一覧に出ない」状態だった)。実体を正として漏れを拾う。
|
|
1202
|
+
const uncatalogued = onDisk.filter((f) => !catalogued.has(f)).sort();
|
|
1203
|
+
if (uncatalogued.length > 0) {
|
|
1204
|
+
const described = uncatalogued.map((f) => [f, describeExample(f)]);
|
|
1205
|
+
const shown = filter
|
|
1206
|
+
? described.filter(([f, d]) =>
|
|
1207
|
+
f.toLowerCase().includes(filter) || d.toLowerCase().includes(filter))
|
|
1208
|
+
: described;
|
|
1209
|
+
if (shown.length > 0) {
|
|
1210
|
+
console.log(' ' + c.bold + c.blue + 'Not yet categorized' + c.reset +
|
|
1211
|
+
c.dim + ` (${uncatalogued.length}, listed straight from examples/)` + c.reset);
|
|
1212
|
+
for (const [file, desc] of shown) {
|
|
1213
|
+
console.log(' ' + c.green + '*' + c.reset + ' ' + c.bold + file + c.reset);
|
|
1214
|
+
console.log(' ' + c.dim + desc + c.reset);
|
|
1215
|
+
}
|
|
1216
|
+
console.log();
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1083
1220
|
// Show batch install command if filtered
|
|
1084
1221
|
const allFiltered = [];
|
|
1085
1222
|
for (const [cat, hooks] of Object.entries(CATEGORIES)) {
|
|
@@ -1095,17 +1232,49 @@ function examples() {
|
|
|
1095
1232
|
if (filter && allFiltered.length > 0 && allFiltered.length <= 20) {
|
|
1096
1233
|
console.log(c.dim + ' Install all filtered hooks:' + c.reset);
|
|
1097
1234
|
for (const h of allFiltered) {
|
|
1098
|
-
console.log(c.dim + ` npx cc-safe-setup --install-example ${h}` + c.reset);
|
|
1235
|
+
console.log(c.dim + ` npx github:yurukusa/cc-safe-setup --install-example ${h}` + c.reset);
|
|
1099
1236
|
}
|
|
1100
1237
|
console.log();
|
|
1101
1238
|
}
|
|
1102
1239
|
|
|
1103
1240
|
console.log(c.dim + ' Or install any single hook:' + c.reset);
|
|
1104
|
-
console.log(c.dim + ' npx cc-safe-setup --install-example <name>' + c.reset);
|
|
1241
|
+
console.log(c.dim + ' npx github:yurukusa/cc-safe-setup --install-example <name>' + c.reset);
|
|
1105
1242
|
console.log(c.dim + ' Source: ' + c.blue + 'https://github.com/yurukusa/cc-safe-setup/tree/main/examples' + c.reset);
|
|
1106
1243
|
console.log();
|
|
1107
1244
|
}
|
|
1108
1245
|
|
|
1246
|
+
// Pull a one-line description out of an example hook's header comment.
|
|
1247
|
+
// Skips the shebang, rule-off lines and machine headings (Trigger:, Matcher: ...),
|
|
1248
|
+
// and joins wrapped lines until the sentence ends. Used for hooks that are not in
|
|
1249
|
+
// the hand-maintained catalogue, so the listing can still say what they do.
|
|
1250
|
+
function describeExample(filename) {
|
|
1251
|
+
try {
|
|
1252
|
+
const lines = readFileSync(join(__dirname, 'examples', filename), 'utf8').split('\n').slice(1, 25);
|
|
1253
|
+
const isRule = (l) => /^[#\s]*[-=*_]{5,}\s*$/.test(l);
|
|
1254
|
+
const isHeading = (t) => /^(Trigger|Matcher|Usage|Exit|Event|Hook|Install|Requires|Note|See|Env|Author|License|Version|SPDX|PURPOSE)\b/i.test(t);
|
|
1255
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1256
|
+
const line = lines[i];
|
|
1257
|
+
if (!line.startsWith('#')) { if (line.trim() === '') continue; break; }
|
|
1258
|
+
let t = line.replace(/^#+/, '').trim();
|
|
1259
|
+
if (!t || isRule(line) || isHeading(t)) continue;
|
|
1260
|
+
const named = t.match(new RegExp('^' + filename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*[\u2014\u2013-]\\s*(.+)$'));
|
|
1261
|
+
if (named) t = named[1].trim();
|
|
1262
|
+
if (t.length < 10) continue;
|
|
1263
|
+
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
|
1264
|
+
if (/[.。]$/.test(t.trim())) break;
|
|
1265
|
+
const nxt = lines[j];
|
|
1266
|
+
if (!nxt.startsWith('#')) break;
|
|
1267
|
+
const cont = nxt.replace(/^#+/, '').trim();
|
|
1268
|
+
if (!cont || isRule(nxt) || isHeading(cont)) break;
|
|
1269
|
+
if (cont === cont.toUpperCase() && cont.length > 3) break;
|
|
1270
|
+
t += ' ' + cont;
|
|
1271
|
+
}
|
|
1272
|
+
return t.replace(/\s+/g, ' ').trim();
|
|
1273
|
+
}
|
|
1274
|
+
} catch { /* unreadable: fall through */ }
|
|
1275
|
+
return 'no description in the hook header';
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1109
1278
|
async function installExample(name) {
|
|
1110
1279
|
const examplesDir = join(__dirname, 'examples');
|
|
1111
1280
|
const filename = name.endsWith('.sh') ? name : name + '.sh';
|
|
@@ -1126,15 +1295,108 @@ async function installExample(name) {
|
|
|
1126
1295
|
|
|
1127
1296
|
// Parse hook header for matcher and trigger
|
|
1128
1297
|
const content = readFileSync(srcPath, 'utf8');
|
|
1298
|
+
|
|
1299
|
+
// Three examples are standalone tools that say so in their own header
|
|
1300
|
+
// ("Event: standalone CLI, not a Claude Code hook", "This is NOT a hook").
|
|
1301
|
+
// They carry no TRIGGER line, so the defaults below used to register them
|
|
1302
|
+
// on PreToolUse/Bash anyway. mcp-stdio-compatibility-test exits 2 when it
|
|
1303
|
+
// cannot read settings.json, and exit 2 on PreToolUse denies the call — so
|
|
1304
|
+
// installing that "test harness" turned every Bash call into a hard denial,
|
|
1305
|
+
// with nothing in the output explaining why. Copy the file (the user asked
|
|
1306
|
+
// for it) but do not wire it into settings.
|
|
1307
|
+
//
|
|
1308
|
+
// The check is anchored to comment lines in the first 25 lines so that prose
|
|
1309
|
+
// mentioning "the standalone Anthropic CLI" does not trip it: measured over
|
|
1310
|
+
// all 910 examples it matches exactly these 3, and none of the 716 files
|
|
1311
|
+
// that declare a TRIGGER.
|
|
1312
|
+
const header = content.split('\n').slice(0, 25).join('\n');
|
|
1313
|
+
if (/^#.*\bnot a (?:claude code )?hook\b/im.test(header)) {
|
|
1314
|
+
const usage = (header.match(/^#\s*Usage:\s*(.+)$/mi) || [])[1];
|
|
1315
|
+
console.log();
|
|
1316
|
+
console.log(c.yellow + ' Copied, but NOT registered as a hook.' + c.reset);
|
|
1317
|
+
console.log(c.dim + ' ' + filename + ' declares in its own header that it is not a' + c.reset);
|
|
1318
|
+
console.log(c.dim + ' Claude Code hook — it is a tool you run by hand. Registering it' + c.reset);
|
|
1319
|
+
console.log(c.dim + ' would run it before every tool call, and a non-zero exit would' + c.reset);
|
|
1320
|
+
console.log(c.dim + ' block that call.' + c.reset);
|
|
1321
|
+
console.log();
|
|
1322
|
+
console.log(' File: ' + destPath);
|
|
1323
|
+
console.log(' Run: ' + (usage ? usage.trim() : 'bash ' + destPath));
|
|
1324
|
+
console.log();
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1129
1328
|
let trigger = 'PreToolUse';
|
|
1130
1329
|
let matcher = 'Bash';
|
|
1131
1330
|
|
|
1132
1331
|
// Detect trigger from header comments (case-insensitive for "Trigger:" prefix)
|
|
1133
|
-
|
|
1332
|
+
//
|
|
1333
|
+
// The value is read as the whole rest of the line, not the first \S+ token.
|
|
1334
|
+
// Measured over all 915 examples on 2026-09-18: ten headers declare a trigger
|
|
1335
|
+
// the old `(\S+)` capture could not match, and every one of them fell through
|
|
1336
|
+
// to the default PreToolUse/Bash without a word:
|
|
1337
|
+
//
|
|
1338
|
+
// "Stop, UserPromptSubmit" -> captured "Stop," (trailing comma)
|
|
1339
|
+
// "SessionStart, and PreToolUse" -> captured "SessionStart,"
|
|
1340
|
+
// "PostToolUse+PreToolUse MATCHER:" -> captured the whole blob
|
|
1341
|
+
// "SubagentStop" -> a real event missing from the list
|
|
1342
|
+
// "none" -> a wrapper that must not be wired
|
|
1343
|
+
//
|
|
1344
|
+
// Two of those ten can exit 2 (commitment-carry-forward-arrest and
|
|
1345
|
+
// subagent-forged-system-reminder-guard). Registered on PreToolUse/Bash, an
|
|
1346
|
+
// exit 2 denies the Bash call — so a hook written to refuse a *Stop* was
|
|
1347
|
+
// refusing ordinary shell commands instead. That is the same failure mode the
|
|
1348
|
+
// standalone-tool guard above was written for.
|
|
1349
|
+
//
|
|
1350
|
+
// The event list was also two lists that had drifted apart: this one and
|
|
1351
|
+
// KNOWN_EVENTS in the settings auditor, which already knew SubagentStop,
|
|
1352
|
+
// SubagentStart, PostCompact and DirectoryAdded. Anything those two disagree
|
|
1353
|
+
// on silently became PreToolUse, so they are reconciled here.
|
|
1354
|
+
const REGISTRABLE_EVENTS = [
|
|
1355
|
+
'PreToolUse', 'PostToolUse', 'PermissionRequest', 'Notification',
|
|
1356
|
+
'Stop', 'SubagentStop', 'SubagentStart', 'UserPromptSubmit',
|
|
1357
|
+
'PreCompact', 'PostCompact', 'SessionStart', 'SessionEnd',
|
|
1358
|
+
'CwdChanged', 'FileChanged', 'DirectoryAdded',
|
|
1359
|
+
];
|
|
1360
|
+
const triggerMatch = content.match(/^#\s*[Tt][Rr][Ii][Gg][Gg][Ee][Rr]:\s*(.+)$/m);
|
|
1134
1361
|
if (triggerMatch) {
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1362
|
+
// Stop at MATCHER: so the inline "TRIGGER: X MATCHER: "Y"" form does not
|
|
1363
|
+
// leak the matcher's tool names into the event scan.
|
|
1364
|
+
// Drop parenthetical commentary before scanning for event names. Several
|
|
1365
|
+
// headers qualify their choice in parentheses — multi-vendor-concurrent-warner
|
|
1366
|
+
// says "SessionStart (also safe as PreToolUse)" — and reading the aside as a
|
|
1367
|
+
// declaration moves a correctly-registered hook off its event.
|
|
1368
|
+
const decl = triggerMatch[1]
|
|
1369
|
+
.split(/[Mm][Aa][Tt][Cc][Hh][Ee][Rr]\s*:/)[0]
|
|
1370
|
+
.replace(/\([^)]*\)/g, ' ');
|
|
1371
|
+
const names = (decl.match(/[A-Za-z]+/g) || []).filter((w) => REGISTRABLE_EVENTS.includes(w));
|
|
1372
|
+
if (/^\s*none\b/i.test(decl)) {
|
|
1373
|
+
// "TRIGGER: none" is a deliberate declaration by wrappers that are called
|
|
1374
|
+
// by another hook, never by Claude Code. Copy it, do not register it.
|
|
1375
|
+
console.log();
|
|
1376
|
+
console.log(c.yellow + ' Copied, but NOT registered as a hook.' + c.reset);
|
|
1377
|
+
console.log(c.dim + ' ' + filename + ' declares "TRIGGER: none" — it is a wrapper meant' + c.reset);
|
|
1378
|
+
console.log(c.dim + ' to be called by another hook, not by Claude Code directly.' + c.reset);
|
|
1379
|
+
console.log();
|
|
1380
|
+
console.log(' File: ' + destPath);
|
|
1381
|
+
console.log();
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
if (names.length > 0) {
|
|
1385
|
+
// Multi-event declarations get ONE registration, so the choice matters.
|
|
1386
|
+
// Prefer PreToolUse whenever it is declared: it is the only event where a
|
|
1387
|
+
// non-zero exit refuses the tool call, and the two-phase guards
|
|
1388
|
+
// (deny-bypass-detector, permission-denial-enforcer) declare
|
|
1389
|
+
// "PostToolUse+PreToolUse" while doing their blocking with exit 2. Taking
|
|
1390
|
+
// the literal first name would have moved them to PostToolUse, where the
|
|
1391
|
+
// tool has already run and exit 2 blocks nothing — disarming the guard
|
|
1392
|
+
// while the installer reported success.
|
|
1393
|
+
trigger = names.includes('PreToolUse') ? 'PreToolUse' : names[0];
|
|
1394
|
+
if (names.length > 1) {
|
|
1395
|
+
// Only one registration is written. Say so, rather than leaving the
|
|
1396
|
+
// operator believing all declared events are covered.
|
|
1397
|
+
console.log(c.yellow + ' ' + filename + ' declares ' + names.length + ' events (' + names.join(', ') + ').' + c.reset);
|
|
1398
|
+
console.log(c.dim + ' Registered on ' + names[0] + ' only — add the others by hand if you need them.' + c.reset);
|
|
1399
|
+
}
|
|
1138
1400
|
}
|
|
1139
1401
|
} else if (content.match(/^#.*PermissionRequest hook/m)) {
|
|
1140
1402
|
trigger = 'PermissionRequest';
|
|
@@ -1150,6 +1412,44 @@ async function installExample(name) {
|
|
|
1150
1412
|
// Extract quoted value if present (e.g., ".env" or ".env|.env.local"), otherwise use raw
|
|
1151
1413
|
const quoted = commentMatcher[1].match(/^"([^"]*)"/);
|
|
1152
1414
|
matcher = quoted ? quoted[1] : commentMatcher[1].trim();
|
|
1415
|
+
// Some headers answer "which matcher?" in prose instead of with a value:
|
|
1416
|
+
// "(none)", "(any — leave matcher empty)", "(empty — runs after every
|
|
1417
|
+
// tool use)", "No matcher support — fires on every compaction".
|
|
1418
|
+
// That prose was written into settings.json as the matcher itself. On an
|
|
1419
|
+
// event that actually uses matchers, "(any — leave matcher empty)" matches
|
|
1420
|
+
// no tool name, so the hook never fires. Measured over all 915 examples on
|
|
1421
|
+
// 2026-09-18: 29 headers are prose, and four of them sit on
|
|
1422
|
+
// PreToolUse/PostToolUse and were dead this way — context-size-alert,
|
|
1423
|
+
// daily-usage-tracker, settings-integrity-monitor, tool-call-rate-limiter.
|
|
1424
|
+
// Every one of the 29 means the same thing: no matcher, i.e. empty.
|
|
1425
|
+
if (!quoted && /^\(|—|no matcher support|leave matcher|runs (after|once)|fires on/i.test(matcher)) {
|
|
1426
|
+
matcher = '';
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// The header is written two ways across examples/: on its own comment line
|
|
1431
|
+
// (handled above) and appended to the TRIGGER line, as
|
|
1432
|
+
// "# TRIGGER: PostToolUse MATCHER: "Edit|Write"". Only the first form was
|
|
1433
|
+
// recognised, so 415 of 908 examples silently fell back to the default
|
|
1434
|
+
// matcher "Bash" — 221 of those declare another tool (Edit|Write and the
|
|
1435
|
+
// like) and therefore never fired at all, and 46 declare "" (every tool) and
|
|
1436
|
+
// were narrowed to Bash only. A guard that installs without error and then
|
|
1437
|
+
// never runs is the worst failure this project has.
|
|
1438
|
+
//
|
|
1439
|
+
// Deliberately additive: the inline form is consulted only when neither the
|
|
1440
|
+
// own-line comment nor an in-file "matcher" JSON resolved one. 31 examples
|
|
1441
|
+
// carry an inline header that disagrees with a JSON snippet in the same file
|
|
1442
|
+
// (e.g. auto-checkpoint declares Bash in the header and Edit|Write in the
|
|
1443
|
+
// JSON) and which one is right has to be settled per file. Preferring the
|
|
1444
|
+
// inline form here would flip those 31 silently, so their behaviour is left
|
|
1445
|
+
// exactly as it is today and the conflict is tracked separately.
|
|
1446
|
+
if (!commentMatcher && !matcherMatch) {
|
|
1447
|
+
const inlineMatcher = content.match(
|
|
1448
|
+
/^#\s*[Tt][Rr][Ii][Gg][Gg][Ee][Rr]:\s*\S+\s+[Mm][Aa][Tt][Cc][Hh][Ee][Rr]:\s*(.+)$/m);
|
|
1449
|
+
if (inlineMatcher) {
|
|
1450
|
+
const quoted = inlineMatcher[1].match(/^"([^"]*)"/);
|
|
1451
|
+
matcher = quoted ? quoted[1] : inlineMatcher[1].trim();
|
|
1452
|
+
}
|
|
1153
1453
|
}
|
|
1154
1454
|
|
|
1155
1455
|
// Update settings.json
|
|
@@ -1182,6 +1482,13 @@ async function installExample(name) {
|
|
|
1182
1482
|
}
|
|
1183
1483
|
|
|
1184
1484
|
async function audit() {
|
|
1485
|
+
// When --json is set, machine consumers (the GitHub Action, CI) parse stdout
|
|
1486
|
+
// as pure JSON. Route the human-readable report to stderr so stdout carries
|
|
1487
|
+
// only the JSON object emitted below. Without this, the colored report
|
|
1488
|
+
// printed ahead of the JSON made stdout unparseable, the Action scored it 0,
|
|
1489
|
+
// and every CI run failed (default threshold 70).
|
|
1490
|
+
const _stdoutLog = console.log;
|
|
1491
|
+
if (JSON_OUTPUT) console.log = (...a) => console.error(...a);
|
|
1185
1492
|
console.log();
|
|
1186
1493
|
console.log(c.bold + ' cc-safe-setup --audit' + c.reset);
|
|
1187
1494
|
console.log(c.dim + ' Analyzing your Claude Code safety setup...' + c.reset);
|
|
@@ -1192,8 +1499,22 @@ async function audit() {
|
|
|
1192
1499
|
|
|
1193
1500
|
// 1. Check if any PreToolUse hooks exist
|
|
1194
1501
|
let settings = {};
|
|
1502
|
+
// A settings.json that exists but does not parse is the worst case, not a neutral
|
|
1503
|
+
// one: Claude Code ignores the whole file, so every hook declared in it is inert.
|
|
1504
|
+
// Swallowing the parse error here made the checks below report "no PreToolUse
|
|
1505
|
+
// hooks" and send the user to reinstall — a true symptom with the cause hidden.
|
|
1506
|
+
// The usual trigger is a copied snippet that starts with a `// path/to/file`
|
|
1507
|
+
// comment line, which is not legal JSON.
|
|
1195
1508
|
if (existsSync(SETTINGS_PATH)) {
|
|
1196
|
-
try {
|
|
1509
|
+
try {
|
|
1510
|
+
settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
1511
|
+
} catch (e) {
|
|
1512
|
+
risks.push({
|
|
1513
|
+
severity: 'CRITICAL',
|
|
1514
|
+
issue: 'settings.json is not valid JSON (' + e.message + ') — Claude Code ignores the entire file, so every hook declared in it is silently inert',
|
|
1515
|
+
fix: 'python3 -m json.tool ' + SETTINGS_PATH + ' # then delete the offending line; a leading "// path" comment is the usual cause'
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1197
1518
|
}
|
|
1198
1519
|
const preHooks = (settings.hooks?.PreToolUse || []);
|
|
1199
1520
|
const postHooks = (settings.hooks?.PostToolUse || []);
|
|
@@ -1203,7 +1524,7 @@ async function audit() {
|
|
|
1203
1524
|
risks.push({
|
|
1204
1525
|
severity: 'CRITICAL',
|
|
1205
1526
|
issue: 'No PreToolUse hooks — destructive commands (rm -rf, git reset --hard) can run unchecked',
|
|
1206
|
-
fix: 'npx cc-safe-setup'
|
|
1527
|
+
fix: 'npx github:yurukusa/cc-safe-setup'
|
|
1207
1528
|
});
|
|
1208
1529
|
} else {
|
|
1209
1530
|
good.push('PreToolUse hooks installed (' + preHooks.length + ')');
|
|
@@ -1215,7 +1536,7 @@ async function audit() {
|
|
|
1215
1536
|
risks.push({
|
|
1216
1537
|
severity: 'HIGH',
|
|
1217
1538
|
issue: 'No destructive command blocker detected — rm -rf /, git reset --hard could execute',
|
|
1218
|
-
fix: 'npx cc-safe-setup (installs destructive-guard)'
|
|
1539
|
+
fix: 'npx github:yurukusa/cc-safe-setup (installs destructive-guard)'
|
|
1219
1540
|
});
|
|
1220
1541
|
} else {
|
|
1221
1542
|
good.push('Destructive command protection detected');
|
|
@@ -1226,7 +1547,7 @@ async function audit() {
|
|
|
1226
1547
|
risks.push({
|
|
1227
1548
|
severity: 'HIGH',
|
|
1228
1549
|
issue: 'No branch push protection — code could be pushed directly to main/master',
|
|
1229
|
-
fix: 'npx cc-safe-setup (installs branch-guard)'
|
|
1550
|
+
fix: 'npx github:yurukusa/cc-safe-setup (installs branch-guard)'
|
|
1230
1551
|
});
|
|
1231
1552
|
} else {
|
|
1232
1553
|
good.push('Branch push protection detected');
|
|
@@ -1237,7 +1558,7 @@ async function audit() {
|
|
|
1237
1558
|
risks.push({
|
|
1238
1559
|
severity: 'HIGH',
|
|
1239
1560
|
issue: 'No secret leak protection — .env files could be committed via git add .',
|
|
1240
|
-
fix: 'npx cc-safe-setup (installs secret-guard)'
|
|
1561
|
+
fix: 'npx github:yurukusa/cc-safe-setup (installs secret-guard)'
|
|
1241
1562
|
});
|
|
1242
1563
|
} else {
|
|
1243
1564
|
good.push('Secret leak protection detected');
|
|
@@ -1248,7 +1569,7 @@ async function audit() {
|
|
|
1248
1569
|
risks.push({
|
|
1249
1570
|
severity: 'MEDIUM',
|
|
1250
1571
|
issue: 'No database wipe protection — migrate:fresh, prisma migrate reset could wipe data',
|
|
1251
|
-
fix: 'npx cc-safe-setup --install-example block-database-wipe'
|
|
1572
|
+
fix: 'npx github:yurukusa/cc-safe-setup --install-example block-database-wipe'
|
|
1252
1573
|
});
|
|
1253
1574
|
} else {
|
|
1254
1575
|
good.push('Database wipe protection detected');
|
|
@@ -1259,16 +1580,40 @@ async function audit() {
|
|
|
1259
1580
|
risks.push({
|
|
1260
1581
|
severity: 'MEDIUM',
|
|
1261
1582
|
issue: 'No PostToolUse hooks — no automatic syntax checking after edits',
|
|
1262
|
-
fix: 'npx cc-safe-setup (installs syntax-check)'
|
|
1583
|
+
fix: 'npx github:yurukusa/cc-safe-setup (installs syntax-check)'
|
|
1263
1584
|
});
|
|
1264
1585
|
} else {
|
|
1265
1586
|
good.push('PostToolUse hooks installed (' + postHooks.length + ')');
|
|
1266
1587
|
}
|
|
1267
1588
|
|
|
1268
1589
|
// 7. Check for CLAUDE.md
|
|
1590
|
+
//
|
|
1591
|
+
// Claude Code walks up the directory tree to find CLAUDE.md, so the audit has
|
|
1592
|
+
// to walk up too. Looking only at the working directory told anyone who ran
|
|
1593
|
+
// this from a subdirectory of their own project "No CLAUDE.md found" while
|
|
1594
|
+
// Claude Code was loading one the whole time. The walk stops at the first
|
|
1595
|
+
// directory holding .git (that is the project root), at HOME, or at the
|
|
1596
|
+
// filesystem root, whichever comes first.
|
|
1269
1597
|
const CC_DIR = join(HOME, '.claude');
|
|
1270
|
-
const
|
|
1271
|
-
|
|
1598
|
+
const ancestorDirs = [];
|
|
1599
|
+
{
|
|
1600
|
+
let d = process.cwd();
|
|
1601
|
+
for (let i = 0; i < 24; i++) {
|
|
1602
|
+
ancestorDirs.push(d);
|
|
1603
|
+
let atRoot = false;
|
|
1604
|
+
try { atRoot = existsSync(join(d, '.git')); } catch {}
|
|
1605
|
+
if (atRoot || d === HOME) break;
|
|
1606
|
+
const parent = dirname(d);
|
|
1607
|
+
if (parent === d) break;
|
|
1608
|
+
d = parent;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
const claudeMdPaths = [...new Set([
|
|
1612
|
+
...ancestorDirs.flatMap(d => [join(d, 'CLAUDE.md'), join(d, '.claude', 'CLAUDE.md')]),
|
|
1613
|
+
join(CC_DIR, 'CLAUDE.md'),
|
|
1614
|
+
join(CLAUDE_BASE, '.claude', 'CLAUDE.md')
|
|
1615
|
+
])];
|
|
1616
|
+
const hasClaudeMd = claudeMdPaths.some(p => { try { return existsSync(p); } catch { return false; } });
|
|
1272
1617
|
if (!hasClaudeMd) {
|
|
1273
1618
|
risks.push({
|
|
1274
1619
|
severity: 'MEDIUM',
|
|
@@ -1284,7 +1629,7 @@ async function audit() {
|
|
|
1284
1629
|
risks.push({
|
|
1285
1630
|
severity: 'LOW',
|
|
1286
1631
|
issue: 'No dotfile protection — ~/.bashrc, ~/.aws/ could be modified',
|
|
1287
|
-
fix: 'npx cc-safe-setup --install-example protect-dotfiles'
|
|
1632
|
+
fix: 'npx github:yurukusa/cc-safe-setup --install-example protect-dotfiles'
|
|
1288
1633
|
});
|
|
1289
1634
|
}
|
|
1290
1635
|
|
|
@@ -1293,7 +1638,7 @@ async function audit() {
|
|
|
1293
1638
|
risks.push({
|
|
1294
1639
|
severity: 'LOW',
|
|
1295
1640
|
issue: 'No scope guard — files outside project directory could be modified',
|
|
1296
|
-
fix: 'npx cc-safe-setup --install-example scope-guard'
|
|
1641
|
+
fix: 'npx github:yurukusa/cc-safe-setup --install-example scope-guard'
|
|
1297
1642
|
});
|
|
1298
1643
|
}
|
|
1299
1644
|
|
|
@@ -1305,21 +1650,186 @@ async function audit() {
|
|
|
1305
1650
|
risks.push({
|
|
1306
1651
|
severity: 'CRITICAL',
|
|
1307
1652
|
issue: `${backslashCmds.length} hook command(s) have Windows backslash paths — hooks will fail with "No such file"`,
|
|
1308
|
-
fix: 'npx cc-safe-setup --uninstall && npx cc-safe-setup
|
|
1653
|
+
fix: 'npx github:yurukusa/cc-safe-setup --uninstall && npx github:yurukusa/cc-safe-setup'
|
|
1309
1654
|
});
|
|
1310
1655
|
}
|
|
1311
1656
|
|
|
1312
|
-
// 11. Check for hook permission fixer (SessionStart)
|
|
1657
|
+
// 11. Check for hook permission fixer (SessionStart).
|
|
1658
|
+
// Execute-bit loss is cross-platform, not Windows-only: plugin installs and
|
|
1659
|
+
// archive extraction strip +x from .sh hooks on Unix too, and Claude Code then
|
|
1660
|
+
// fails every affected hook with "Permission denied".
|
|
1661
|
+
// See anthropics/claude-code #39378 / #38901 / #39777 / #39798.
|
|
1313
1662
|
const sessionHooks = settings.hooks?.SessionStart || [];
|
|
1314
1663
|
const hasPermFixer = sessionHooks.some(e =>
|
|
1315
1664
|
(e.hooks || []).some(h => (h.command || '').includes('permission'))
|
|
1316
1665
|
);
|
|
1317
|
-
if (!hasPermFixer
|
|
1666
|
+
if (!hasPermFixer) {
|
|
1667
|
+
// Only flag when there is real evidence it is needed, to avoid noise:
|
|
1668
|
+
// a hook script that actually lacks the execute bit, or a plugins dir that
|
|
1669
|
+
// can introduce non-executable hooks on the next update.
|
|
1670
|
+
let nonExecCount = 0;
|
|
1671
|
+
let pluginsPresent = false;
|
|
1672
|
+
try {
|
|
1673
|
+
const { readdirSync, statSync } = await import('fs');
|
|
1674
|
+
if (existsSync(HOOKS_DIR)) {
|
|
1675
|
+
for (const f of readdirSync(HOOKS_DIR)) {
|
|
1676
|
+
if (!f.endsWith('.sh')) continue;
|
|
1677
|
+
try { if (!(statSync(join(HOOKS_DIR, f)).mode & 0o111)) nonExecCount++; } catch {}
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
const pluginsDir = join(CLAUDE_BASE, '.claude', 'plugins');
|
|
1681
|
+
pluginsPresent = existsSync(pluginsDir);
|
|
1682
|
+
} catch {}
|
|
1683
|
+
if (nonExecCount > 0) {
|
|
1684
|
+
risks.push({
|
|
1685
|
+
severity: 'HIGH',
|
|
1686
|
+
issue: `${nonExecCount} hook script(s) in ~/.claude/hooks lack the execute bit — Claude Code will fail them with "Permission denied" (anthropics/claude-code #39378)`,
|
|
1687
|
+
fix: 'npx github:yurukusa/cc-safe-setup --validate (one-shot fix) or --install-example hook-permission-fixer (persistent)'
|
|
1688
|
+
});
|
|
1689
|
+
} else if (pluginsPresent || process.platform === 'win32') {
|
|
1690
|
+
risks.push({
|
|
1691
|
+
severity: 'LOW',
|
|
1692
|
+
issue: 'No SessionStart permission fixer — plugin installs/updates can strip hook execute bits, causing "Permission denied"',
|
|
1693
|
+
fix: 'npx github:yurukusa/cc-safe-setup --install-example hook-permission-fixer'
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// 12. Cross-layer check: a rule that names a safety net which is not there.
|
|
1699
|
+
//
|
|
1700
|
+
// Every check above reads one layer. This one reads two against each other:
|
|
1701
|
+
// the instructions in CLAUDE.md, and the hooks actually registered in the
|
|
1702
|
+
// settings files. A rule that names a guard by filename passes every check
|
|
1703
|
+
// you can run on the rule (the sentence is fine) and every check you can run
|
|
1704
|
+
// on the config (the hooks that *are* registered are valid) — the
|
|
1705
|
+
// contradiction lives between them, so nothing that reads one file sees it.
|
|
1706
|
+
//
|
|
1707
|
+
// Precision is worth more than recall here: telling someone a working guard
|
|
1708
|
+
// is inert would make them distrust the whole report. So only backticked,
|
|
1709
|
+
// space-free, script-shaped tokens count; resolution is tried against every
|
|
1710
|
+
// plausible root; and "never registered" is decided against the raw text of
|
|
1711
|
+
// every settings file that could register it, not just the user one.
|
|
1712
|
+
// CLAUDE_BASE and HOME are the same for a plain install and different for an
|
|
1713
|
+
// isolated one, so both have to be read. Reading only CLAUDE_BASE drops the
|
|
1714
|
+
// user's global layer the moment CLAUDE_PROJECT_DIR is set, and the check
|
|
1715
|
+
// then goes silent on a real finding -- the same shape as the mis-report
|
|
1716
|
+
// fixed in #1046, where the diagnosis did not follow the installer.
|
|
1717
|
+
const settingsTexts = [];
|
|
1718
|
+
const seenPaths = new Set();
|
|
1719
|
+
const readOnce = (p) => {
|
|
1720
|
+
if (seenPaths.has(p)) return;
|
|
1721
|
+
seenPaths.add(p);
|
|
1722
|
+
try { if (existsSync(p)) settingsTexts.push(readFileSync(p, 'utf-8')); } catch {}
|
|
1723
|
+
};
|
|
1724
|
+
for (const base of [CLAUDE_BASE, HOME, process.cwd()]) {
|
|
1725
|
+
readOnce(join(base, '.claude', 'settings.json'));
|
|
1726
|
+
readOnce(join(base, '.claude', 'settings.local.json'));
|
|
1727
|
+
}
|
|
1728
|
+
const registrationText = settingsTexts.join('\n');
|
|
1729
|
+
|
|
1730
|
+
// Same set the check above resolves: every ancestor up to the project root,
|
|
1731
|
+
// plus the user layer. A rule two directories up is still a rule that binds.
|
|
1732
|
+
const layerFiles = claudeMdPaths
|
|
1733
|
+
.filter(p => { try { return existsSync(p); } catch { return false; } });
|
|
1734
|
+
|
|
1735
|
+
const hookDirs = [...new Set([HOOKS_DIR, join(HOME, '.claude', 'hooks')])];
|
|
1736
|
+
const namedScripts = new Map();
|
|
1737
|
+
for (const layer of layerFiles) {
|
|
1738
|
+
let text = '';
|
|
1739
|
+
try { text = readFileSync(layer, 'utf-8'); } catch { continue; }
|
|
1740
|
+
for (const m of text.matchAll(/`([^`\n]+)`/g)) {
|
|
1741
|
+
const tok = m[1].trim();
|
|
1742
|
+
// A script this check can reason about: one token, no glob, no URL, and
|
|
1743
|
+
// an extension Claude Code can actually run as a hook.
|
|
1744
|
+
if (!/^[\w./~@+-]+\.(sh|mjs|py|js)$/.test(tok)) continue;
|
|
1745
|
+
if (tok.includes('*') || tok.startsWith('http')) continue;
|
|
1746
|
+
// Build output is absent on a fresh checkout and present after `npm run
|
|
1747
|
+
// build`, so its absence says nothing about the rule. Measured against 40
|
|
1748
|
+
// public CLAUDE.md files on 2026-08-13: 13 named a script, and one of the
|
|
1749
|
+
// two that named something missing was `dist/extension.js` — a compiled
|
|
1750
|
+
// artifact, not a broken reference.
|
|
1751
|
+
if (/(^|\/)(dist|build|out|target|coverage|node_modules|\.next|\.nuxt|__pycache__)\//.test(tok)) continue;
|
|
1752
|
+
if (namedScripts.has(tok)) continue;
|
|
1753
|
+
const base = tok.split('/').pop();
|
|
1754
|
+
const roots = [];
|
|
1755
|
+
if (tok.startsWith('~/')) roots.push(join(HOME, tok.slice(2)));
|
|
1756
|
+
else if (tok.startsWith('/')) roots.push(tok);
|
|
1757
|
+
else {
|
|
1758
|
+
roots.push(join(dirname(layer), tok));
|
|
1759
|
+
roots.push(join(process.cwd(), tok));
|
|
1760
|
+
roots.push(join(CLAUDE_BASE, '.claude', tok));
|
|
1761
|
+
roots.push(join(HOME, '.claude', tok));
|
|
1762
|
+
}
|
|
1763
|
+
for (const hd of hookDirs) roots.push(join(hd, base));
|
|
1764
|
+
const resolved = roots.find(p => { try { return existsSync(p); } catch { return false; } }) || null;
|
|
1765
|
+
namedScripts.set(tok, { base, resolved });
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
if (namedScripts.size > 0) {
|
|
1770
|
+
const missing = [];
|
|
1771
|
+
const unregistered = [];
|
|
1772
|
+
for (const [tok, info] of namedScripts) {
|
|
1773
|
+
if (!info.resolved) { missing.push(tok); continue; }
|
|
1774
|
+
// Only a file sitting in the hooks directory can be "registered". A helper
|
|
1775
|
+
// script your rules tell *you* to run has no business in settings.json,
|
|
1776
|
+
// and flagging it would be the false positive that kills the report.
|
|
1777
|
+
let hooksHome = null;
|
|
1778
|
+
for (const hd of hookDirs) {
|
|
1779
|
+
try { if (existsSync(join(hd, info.base))) { hooksHome = hd; break; } } catch {}
|
|
1780
|
+
}
|
|
1781
|
+
if (!hooksHome) continue;
|
|
1782
|
+
info.hooksHome = hooksHome;
|
|
1783
|
+
if (!registrationText.includes(info.base)) unregistered.push(tok);
|
|
1784
|
+
}
|
|
1785
|
+
const sample = (arr) => arr.slice(0, 4).join(', ') + (arr.length > 4 ? ', …' : '');
|
|
1786
|
+
|
|
1787
|
+
if (missing.length > 0) {
|
|
1788
|
+
risks.push({
|
|
1789
|
+
severity: 'HIGH',
|
|
1790
|
+
issue: `${missing.length} rule(s) in CLAUDE.md name a script that exists nowhere I looked (${sample(missing)}) — the rule reads fine and the config is valid, so no single-file check can surface this`,
|
|
1791
|
+
fix: 'Open each rule and either create the file or delete the rule. A rule naming a file Claude cannot find is not a weak rule, it is an unfollowable one.'
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
if (unregistered.length > 0) {
|
|
1795
|
+
risks.push({
|
|
1796
|
+
severity: 'HIGH',
|
|
1797
|
+
issue: `${unregistered.length} hook script(s) named by CLAUDE.md sit in ${namedScripts.get(unregistered[0]).hooksHome} but appear in no settings file (${sample(unregistered)}) — they exist, so every existence check passes, and they never run`,
|
|
1798
|
+
fix: 'npx github:yurukusa/cc-safe-setup --doctor # then register the ones you meant to keep, or delete them so the rule stops promising a guard you do not have'
|
|
1799
|
+
});
|
|
1800
|
+
}
|
|
1801
|
+
if (missing.length === 0 && unregistered.length === 0) {
|
|
1802
|
+
good.push('Every script named in CLAUDE.md resolves, and the hook-directory ones are registered (' + namedScripts.size + ' checked)');
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// 13. A blocking hook that parses its input with `echo "$VAR" | jq` fails
|
|
1807
|
+
// open on Debian and Ubuntu. Claude Code runs an inline hook command through
|
|
1808
|
+
// /bin/sh, which is dash there, and dash's echo interprets backslash escapes.
|
|
1809
|
+
// The payload is JSON, so a newline inside a command or a file's content
|
|
1810
|
+
// arrives as the two characters \n; dash turns those into a real newline
|
|
1811
|
+
// inside a JSON string, the parser rejects the document, the variable comes
|
|
1812
|
+
// back empty, and the line that normally follows -- [ -z "$CMD" ] && exit 0
|
|
1813
|
+
// -- is an affirmative approval.
|
|
1814
|
+
//
|
|
1815
|
+
// Measured 2026-09-03 against this project's own plugin hooks: all four
|
|
1816
|
+
// guards approved everything under /bin/sh, and every local test passed
|
|
1817
|
+
// because the tests ran the same commands under bash. Nothing in the log
|
|
1818
|
+
// says anything is wrong; the hook is reported as "success".
|
|
1819
|
+
//
|
|
1820
|
+
// Only hooks that can block are reported. A logger that loses a field is a
|
|
1821
|
+
// cosmetic bug; a guard that loses it stops being a guard.
|
|
1822
|
+
const failOpenCmds = allCmds.filter(cmd =>
|
|
1823
|
+
/echo\s+"\$[A-Za-z_][A-Za-z0-9_]*"\s*\|\s*(jq|python3?|node)\b/.test(cmd) &&
|
|
1824
|
+
/exit\s+2\b/.test(cmd));
|
|
1825
|
+
if (failOpenCmds.length > 0) {
|
|
1318
1826
|
risks.push({
|
|
1319
|
-
severity: '
|
|
1320
|
-
issue:
|
|
1321
|
-
fix: '
|
|
1827
|
+
severity: 'HIGH',
|
|
1828
|
+
issue: `${failOpenCmds.length} blocking hook(s) parse input with \`echo "$VAR" | parser\` — under /bin/sh (dash) a payload containing an escape breaks the parse, the variable empties, and the guard exits 0 (allow)`,
|
|
1829
|
+
fix: 'Replace `echo "$INPUT" |` with `printf %s "$INPUT" |` in those hook commands'
|
|
1322
1830
|
});
|
|
1831
|
+
} else if (allCmds.some(cmd => /\|\s*(jq|python3?|node)\b/.test(cmd))) {
|
|
1832
|
+
good.push('Hook input parsing survives /bin/sh (no `echo "$VAR" | parser` in a blocking hook)');
|
|
1323
1833
|
}
|
|
1324
1834
|
|
|
1325
1835
|
// Display results
|
|
@@ -1332,7 +1842,17 @@ async function audit() {
|
|
|
1332
1842
|
}
|
|
1333
1843
|
|
|
1334
1844
|
if (risks.length === 0) {
|
|
1335
|
-
|
|
1845
|
+
// 「危険は見つからなかった」と「危険は無い」は別のことなのに、
|
|
1846
|
+
// 前の文面(Your setup looks solid)は後者に読めた。安全を売る道具で
|
|
1847
|
+
// 誤って安心させる側へ倒れるのは、見落とす側より高くつく。
|
|
1848
|
+
// 何を見て何を見ていないかを、その場で並べる。
|
|
1849
|
+
console.log(c.green + c.bold + ' No risks found in the checks this command runs.' + c.reset);
|
|
1850
|
+
console.log();
|
|
1851
|
+
console.log(c.dim + ' Checked: settings.json (permissions, hooks), and the scripts your' + c.reset);
|
|
1852
|
+
console.log(c.dim + ' CLAUDE.md names against what your settings register.' + c.reset);
|
|
1853
|
+
console.log(c.dim + ' Not checked: whether a registered hook actually refuses anything' + c.reset);
|
|
1854
|
+
console.log(c.dim + ' (audit/selftest.sh), whether your hooks match the shape of' + c.reset);
|
|
1855
|
+
console.log(c.dim + ' the commands you really run (--blindspots), and your CI.' + c.reset);
|
|
1336
1856
|
} else {
|
|
1337
1857
|
console.log(c.bold + ' ⚠ Risks found (' + risks.length + '):' + c.reset);
|
|
1338
1858
|
console.log();
|
|
@@ -1358,9 +1878,9 @@ async function audit() {
|
|
|
1358
1878
|
console.log(c.bold + ' Applying fixes...' + c.reset);
|
|
1359
1879
|
const { execSync } = await import('child_process');
|
|
1360
1880
|
for (const r of risks) {
|
|
1361
|
-
if (r.fix.startsWith('npx cc-safe-setup')) {
|
|
1881
|
+
if (r.fix.startsWith('npx github:yurukusa/cc-safe-setup')) {
|
|
1362
1882
|
try {
|
|
1363
|
-
const cmd = r.fix.replace('npx cc-safe-setup', 'node ' + process.argv[1]);
|
|
1883
|
+
const cmd = r.fix.replace('npx github:yurukusa/cc-safe-setup', 'node ' + process.argv[1]);
|
|
1364
1884
|
console.log(' ' + c.dim + '→ ' + r.fix + c.reset);
|
|
1365
1885
|
execSync(cmd, { stdio: 'inherit' });
|
|
1366
1886
|
} catch(e) {
|
|
@@ -1372,7 +1892,7 @@ async function audit() {
|
|
|
1372
1892
|
console.log(c.green + ' Re-run --audit to verify fixes.' + c.reset);
|
|
1373
1893
|
} else if (risks.length > 0) {
|
|
1374
1894
|
console.log();
|
|
1375
|
-
console.log(c.dim + ' Run with --fix to auto-apply: npx cc-safe-setup --audit --fix' + c.reset);
|
|
1895
|
+
console.log(c.dim + ' Run with --fix to auto-apply: npx github:yurukusa/cc-safe-setup --audit --fix' + c.reset);
|
|
1376
1896
|
}
|
|
1377
1897
|
|
|
1378
1898
|
// Badge output
|
|
@@ -1395,11 +1915,44 @@ async function audit() {
|
|
|
1395
1915
|
passing: good,
|
|
1396
1916
|
timestamp: new Date().toISOString(),
|
|
1397
1917
|
};
|
|
1918
|
+
console.log = _stdoutLog; // restore: the JSON object must reach stdout
|
|
1398
1919
|
console.log(JSON.stringify(output, null, 2));
|
|
1399
1920
|
}
|
|
1400
1921
|
|
|
1401
|
-
|
|
1402
|
-
|
|
1922
|
+
// Highest-intent moment: the user just saw their score. Point to genuinely
|
|
1923
|
+
// useful free next steps (skip when --json so machine consumers stay clean).
|
|
1924
|
+
// A low score → the incident-prevention guide fits; everyone gets the cost guide.
|
|
1925
|
+
if (!JSON_OUTPUT) {
|
|
1926
|
+
console.log();
|
|
1927
|
+
console.log(c.bold + ' Go deeper (all free to start):' + c.reset);
|
|
1928
|
+
if (score < 80) {
|
|
1929
|
+
// Lead the low-score (peak-concern) path with the free incident catalog,
|
|
1930
|
+
// not a book link: the 100 real incidents validate the abstract risks just
|
|
1931
|
+
// shown and cost nothing, so the deeper path stays free-education-first.
|
|
1932
|
+
console.log(c.dim + ' • 100 real incidents these guards prevent: https://yurukusa.github.io/cc-safe-setup/incidents.html' + c.reset);
|
|
1933
|
+
console.log(c.dim + ' • Incident-prevention guide (Ch.3 free): https://zenn.dev/yurukusa/books/6076c23b1cb18b' + c.reset);
|
|
1934
|
+
}
|
|
1935
|
+
console.log(c.dim + ' • Cut token cost (Ch.1 free): https://yurukusa.github.io/cc-safe-setup/token-book.html' + c.reset);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
console.log();
|
|
1939
|
+
// README has told people to put `--audit --ci` in a workflow since this section
|
|
1940
|
+
// was written, and until 2026-09-03 nothing here read `--ci`. The default
|
|
1941
|
+
// threshold is 0 and the score cannot go below 0, so `score < 0` was never true:
|
|
1942
|
+
// the step passed no matter what the audit found. A CI gate that cannot fail is
|
|
1943
|
+
// worse than no gate, because the reader stops watching. `--ci` now fails on any
|
|
1944
|
+
// risk; CC_AUDIT_THRESHOLD keeps working for score-based gating either way.
|
|
1945
|
+
// 門の線は CRITICAL/HIGH。any-risk で落とすと、この道具が薦める構成のままでも
|
|
1946
|
+
// MEDIUM 1件で赤くなり、利用者は真っ先にこの step を外す——落ちない門と同じ結末になる。
|
|
1947
|
+
const ciMode = process.argv.includes('--ci');
|
|
1948
|
+
const threshold = parseInt(process.env.CC_AUDIT_THRESHOLD) || 0;
|
|
1949
|
+
const blocking = risks.filter(r => r.severity === 'CRITICAL' || r.severity === 'HIGH');
|
|
1950
|
+
if (ciMode && !JSON_OUTPUT) {
|
|
1951
|
+
console.log(c.dim + ` --ci: fails on CRITICAL or HIGH only (${blocking.length} of ${risks.length} risk${risks.length === 1 ? '' : 's'} here).` + c.reset);
|
|
1952
|
+
console.log(c.dim + ' Set CC_AUDIT_THRESHOLD to also fail below a score.' + c.reset);
|
|
1953
|
+
console.log();
|
|
1954
|
+
}
|
|
1955
|
+
process.exit((score < threshold || (ciMode && blocking.length > 0)) ? 1 : 0);
|
|
1403
1956
|
}
|
|
1404
1957
|
|
|
1405
1958
|
function learn() {
|
|
@@ -1417,8 +1970,7 @@ function learn() {
|
|
|
1417
1970
|
return;
|
|
1418
1971
|
}
|
|
1419
1972
|
|
|
1420
|
-
const
|
|
1421
|
-
const lines = log.split('\n').filter(l => l.trim());
|
|
1973
|
+
const lines = readBlockRecords(logPath);
|
|
1422
1974
|
|
|
1423
1975
|
if (lines.length === 0) {
|
|
1424
1976
|
console.log(c.green + ' No blocked commands in history. Your setup is catching nothing (or everything is safe).' + c.reset);
|
|
@@ -1505,7 +2057,7 @@ exit 0`;
|
|
|
1505
2057
|
|
|
1506
2058
|
if (!process.argv.includes('--apply')) {
|
|
1507
2059
|
console.log();
|
|
1508
|
-
console.log(c.dim + ' Run with --apply to auto-create hooks: npx cc-safe-setup --learn --apply' + c.reset);
|
|
2060
|
+
console.log(c.dim + ' Run with --apply to auto-create hooks: npx github:yurukusa/cc-safe-setup --learn --apply' + c.reset);
|
|
1509
2061
|
}
|
|
1510
2062
|
}
|
|
1511
2063
|
|
|
@@ -1560,7 +2112,7 @@ async function testHook(hookName) {
|
|
|
1560
2112
|
console.log(c.bold + ' cc-safe-setup --test-hook <name>' + c.reset);
|
|
1561
2113
|
console.log(c.dim + ' Test any hook with sample inputs.' + c.reset);
|
|
1562
2114
|
console.log();
|
|
1563
|
-
console.log(' Example: npx cc-safe-setup --test-hook destructive-guard');
|
|
2115
|
+
console.log(' Example: npx github:yurukusa/cc-safe-setup --test-hook destructive-guard');
|
|
1564
2116
|
return;
|
|
1565
2117
|
}
|
|
1566
2118
|
|
|
@@ -1640,13 +2192,13 @@ async function saveProfile(name) {
|
|
|
1640
2192
|
const files = existsSync(profilesDir) ? readdirSync(profilesDir).filter(f => f.endsWith('.json')) : [];
|
|
1641
2193
|
if (files.length === 0) {
|
|
1642
2194
|
console.log(c.dim + ' No saved profiles yet.' + c.reset);
|
|
1643
|
-
console.log(c.dim + ' Save: npx cc-safe-setup --save-profile my-setup' + c.reset);
|
|
2195
|
+
console.log(c.dim + ' Save: npx github:yurukusa/cc-safe-setup --save-profile my-setup' + c.reset);
|
|
1644
2196
|
} else {
|
|
1645
2197
|
for (const f of files) {
|
|
1646
2198
|
const pName = f.replace('.json', '');
|
|
1647
2199
|
const data = JSON.parse(readFileSync(join(profilesDir, f), 'utf-8'));
|
|
1648
2200
|
console.log(` ${c.bold}${pName}${c.reset} (${data.hooks?.length || 0} hooks, saved ${data.savedAt?.split('T')[0] || '?'})`);
|
|
1649
|
-
console.log(c.dim + ` Load: npx cc-safe-setup --profile ${pName}` + c.reset);
|
|
2201
|
+
console.log(c.dim + ` Load: npx github:yurukusa/cc-safe-setup --profile ${pName}` + c.reset);
|
|
1650
2202
|
}
|
|
1651
2203
|
}
|
|
1652
2204
|
console.log();
|
|
@@ -1654,7 +2206,7 @@ async function saveProfile(name) {
|
|
|
1654
2206
|
}
|
|
1655
2207
|
|
|
1656
2208
|
// Save current hook state
|
|
1657
|
-
const hookDir =
|
|
2209
|
+
const hookDir = HOOKS_DIR;
|
|
1658
2210
|
const hooks = existsSync(hookDir) ? readdirSync(hookDir).filter(f => f.endsWith('.sh') || f.endsWith('.py')) : [];
|
|
1659
2211
|
|
|
1660
2212
|
const profile = {
|
|
@@ -1670,7 +2222,7 @@ async function saveProfile(name) {
|
|
|
1670
2222
|
console.log();
|
|
1671
2223
|
console.log(c.green + ` ✓ Profile "${name}" saved (${hooks.length} hooks)` + c.reset);
|
|
1672
2224
|
console.log(c.dim + ` File: ${profilePath}` + c.reset);
|
|
1673
|
-
console.log(c.dim + ` Load: npx cc-safe-setup --profile ${name}` + c.reset);
|
|
2225
|
+
console.log(c.dim + ` Load: npx github:yurukusa/cc-safe-setup --profile ${name}` + c.reset);
|
|
1674
2226
|
console.log();
|
|
1675
2227
|
}
|
|
1676
2228
|
|
|
@@ -1701,7 +2253,7 @@ async function scoreOnly() {
|
|
|
1701
2253
|
let score = 0;
|
|
1702
2254
|
|
|
1703
2255
|
// Hooks installed (max 50 points)
|
|
1704
|
-
const hookDir =
|
|
2256
|
+
const hookDir = HOOKS_DIR;
|
|
1705
2257
|
const hookCount = existsSync(hookDir) ? readdirSync(hookDir).filter(f => f.endsWith('.sh') || f.endsWith('.py')).length : 0;
|
|
1706
2258
|
score += Math.min(hookCount * 3, 50);
|
|
1707
2259
|
|
|
@@ -1897,7 +2449,10 @@ async function suggest() {
|
|
|
1897
2449
|
|
|
1898
2450
|
// Display results
|
|
1899
2451
|
if (risks.length === 0) {
|
|
1900
|
-
|
|
2452
|
+
// 「危険は無い」ではなく「この検査では見つからなかった」。
|
|
2453
|
+
// ここは設定と依存の走査だけで、実際に打たれた命令の形は読んでいない。
|
|
2454
|
+
console.log(c.green + ' No risks found in the checks this scan runs.' + c.reset);
|
|
2455
|
+
console.log(c.dim + ' It reads configuration, not what you actually run. --blindspots reads that.' + c.reset);
|
|
1901
2456
|
return;
|
|
1902
2457
|
}
|
|
1903
2458
|
|
|
@@ -1918,7 +2473,7 @@ async function suggest() {
|
|
|
1918
2473
|
if (isInstalled) {
|
|
1919
2474
|
console.log(c.green + ` ✓ ${r.hook} (installed)` + c.reset);
|
|
1920
2475
|
} else {
|
|
1921
|
-
console.log(c.yellow + ` → Install: npx cc-safe-setup --install-example ${r.hook}` + c.reset);
|
|
2476
|
+
console.log(c.yellow + ` → Install: npx github:yurukusa/cc-safe-setup --install-example ${r.hook}` + c.reset);
|
|
1922
2477
|
}
|
|
1923
2478
|
} else {
|
|
1924
2479
|
console.log(c.dim + ` → ${r.reason}` + c.reset);
|
|
@@ -1929,7 +2484,7 @@ async function suggest() {
|
|
|
1929
2484
|
const unprotected = risks.filter(r => r.hook && !installed.has(r.hook));
|
|
1930
2485
|
if (unprotected.length > 0) {
|
|
1931
2486
|
console.log(c.bold + ` ${unprotected.length} unprotected risk(s). Fix all:` + c.reset);
|
|
1932
|
-
console.log(c.yellow + ' npx cc-safe-setup --shield' + c.reset);
|
|
2487
|
+
console.log(c.yellow + ' npx github:yurukusa/cc-safe-setup --shield' + c.reset);
|
|
1933
2488
|
} else {
|
|
1934
2489
|
console.log(c.green + ' All detected risks are covered by installed hooks!' + c.reset);
|
|
1935
2490
|
}
|
|
@@ -1966,8 +2521,8 @@ async function why(hookName) {
|
|
|
1966
2521
|
console.log(c.dim + ' Show why a hook exists — the real incident that inspired it.' + c.reset);
|
|
1967
2522
|
console.log();
|
|
1968
2523
|
console.log(' Examples:');
|
|
1969
|
-
console.log(c.dim + ' npx cc-safe-setup --why destructive-guard' + c.reset);
|
|
1970
|
-
console.log(c.dim + ' npx cc-safe-setup --why token-budget-guard' + c.reset);
|
|
2524
|
+
console.log(c.dim + ' npx github:yurukusa/cc-safe-setup --why destructive-guard' + c.reset);
|
|
2525
|
+
console.log(c.dim + ' npx github:yurukusa/cc-safe-setup --why token-budget-guard' + c.reset);
|
|
1971
2526
|
console.log();
|
|
1972
2527
|
console.log(` ${Object.keys(WHY_DATA).length} hooks have documented incidents.`);
|
|
1973
2528
|
return;
|
|
@@ -1996,7 +2551,7 @@ async function why(hookName) {
|
|
|
1996
2551
|
console.log(c.dim + ` GitHub Issue: ${data.issue}` + c.reset);
|
|
1997
2552
|
}
|
|
1998
2553
|
console.log();
|
|
1999
|
-
console.log(c.dim + ' Install: npx cc-safe-setup --install-example ' + name + c.reset);
|
|
2554
|
+
console.log(c.dim + ' Install: npx github:yurukusa/cc-safe-setup --install-example ' + name + c.reset);
|
|
2000
2555
|
console.log();
|
|
2001
2556
|
}
|
|
2002
2557
|
|
|
@@ -2084,10 +2639,10 @@ async function guard(description) {
|
|
|
2084
2639
|
console.log(c.dim + ' Instantly enforce a rule — generates, installs, and activates a hook.' + c.reset);
|
|
2085
2640
|
console.log();
|
|
2086
2641
|
console.log(' Examples:');
|
|
2087
|
-
console.log(c.dim + ' npx cc-safe-setup --guard "never touch the database"' + c.reset);
|
|
2088
|
-
console.log(c.dim + ' npx cc-safe-setup --guard "block all sudo commands"' + c.reset);
|
|
2089
|
-
console.log(c.dim + ' npx cc-safe-setup --guard "no force push"' + c.reset);
|
|
2090
|
-
console.log(c.dim + ' npx cc-safe-setup --guard "warn before deleting files"' + c.reset);
|
|
2642
|
+
console.log(c.dim + ' npx github:yurukusa/cc-safe-setup --guard "never touch the database"' + c.reset);
|
|
2643
|
+
console.log(c.dim + ' npx github:yurukusa/cc-safe-setup --guard "block all sudo commands"' + c.reset);
|
|
2644
|
+
console.log(c.dim + ' npx github:yurukusa/cc-safe-setup --guard "no force push"' + c.reset);
|
|
2645
|
+
console.log(c.dim + ' npx github:yurukusa/cc-safe-setup --guard "warn before deleting files"' + c.reset);
|
|
2091
2646
|
console.log();
|
|
2092
2647
|
return;
|
|
2093
2648
|
}
|
|
@@ -2136,10 +2691,7 @@ async function guard(description) {
|
|
|
2136
2691
|
console.log(c.green + ' ✓' + c.reset + ` Hook created: ${hookPath}`);
|
|
2137
2692
|
|
|
2138
2693
|
// Register in settings.json
|
|
2139
|
-
let settings =
|
|
2140
|
-
if (existsSync(SETTINGS_PATH)) {
|
|
2141
|
-
try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
|
|
2142
|
-
}
|
|
2694
|
+
let settings = readSettingsForWrite();
|
|
2143
2695
|
if (!settings.hooks) settings.hooks = {};
|
|
2144
2696
|
if (!settings.hooks[trigger]) settings.hooks[trigger] = [];
|
|
2145
2697
|
|
|
@@ -2160,7 +2712,7 @@ async function guard(description) {
|
|
|
2160
2712
|
console.log();
|
|
2161
2713
|
console.log(c.dim + ` Rule: "${description}"` + c.reset);
|
|
2162
2714
|
console.log(c.dim + ` Hook: ${hookName}.sh` + c.reset);
|
|
2163
|
-
console.log(c.dim + ' Remove: npx cc-safe-setup --uninstall' + c.reset);
|
|
2715
|
+
console.log(c.dim + ' Remove: npx github:yurukusa/cc-safe-setup --uninstall' + c.reset);
|
|
2164
2716
|
console.log();
|
|
2165
2717
|
}
|
|
2166
2718
|
|
|
@@ -2230,7 +2782,7 @@ async function diffHooks(otherPath) {
|
|
|
2230
2782
|
}
|
|
2231
2783
|
} else {
|
|
2232
2784
|
console.log(c.dim + ' No project-level settings found.' + c.reset);
|
|
2233
|
-
console.log(c.dim + ' Create with: npx cc-safe-setup --team' + c.reset);
|
|
2785
|
+
console.log(c.dim + ' Create with: npx github:yurukusa/cc-safe-setup --team' + c.reset);
|
|
2234
2786
|
console.log();
|
|
2235
2787
|
console.log(c.bold + ' Global hooks (' + myHooks.size + '):' + c.reset);
|
|
2236
2788
|
[...myHooks].sort().forEach(h => console.log(` ${h}`));
|
|
@@ -2268,7 +2820,7 @@ async function fromClaudeMd() {
|
|
|
2268
2820
|
|
|
2269
2821
|
if (!claudeMdPath) {
|
|
2270
2822
|
console.log(c.yellow + ' No CLAUDE.md found in project.' + c.reset);
|
|
2271
|
-
console.log(c.dim + ' Run: npx cc-safe-setup --shield (creates one)' + c.reset);
|
|
2823
|
+
console.log(c.dim + ' Run: npx github:yurukusa/cc-safe-setup --shield (creates one)' + c.reset);
|
|
2272
2824
|
return;
|
|
2273
2825
|
}
|
|
2274
2826
|
|
|
@@ -2306,7 +2858,7 @@ async function fromClaudeMd() {
|
|
|
2306
2858
|
if (matched.length === 0) {
|
|
2307
2859
|
console.log(c.yellow + ' No enforceable rules detected in CLAUDE.md.' + c.reset);
|
|
2308
2860
|
console.log(c.dim + ' CLAUDE.md may contain guidelines that can\'t be converted to hooks.' + c.reset);
|
|
2309
|
-
console.log(c.dim + ' For custom hooks: npx cc-safe-setup --create "your rule"' + c.reset);
|
|
2861
|
+
console.log(c.dim + ' For custom hooks: npx github:yurukusa/cc-safe-setup --create "your rule"' + c.reset);
|
|
2310
2862
|
return;
|
|
2311
2863
|
}
|
|
2312
2864
|
|
|
@@ -2320,7 +2872,7 @@ async function fromClaudeMd() {
|
|
|
2320
2872
|
const status = installed ? c.dim + '(installed)' + c.reset : '';
|
|
2321
2873
|
console.log(` ${icon} ${m.desc} ${status}`);
|
|
2322
2874
|
if (!installed) {
|
|
2323
|
-
console.log(c.dim + ` npx cc-safe-setup --install-example ${m.hook}` + c.reset);
|
|
2875
|
+
console.log(c.dim + ` npx github:yurukusa/cc-safe-setup --install-example ${m.hook}` + c.reset);
|
|
2324
2876
|
}
|
|
2325
2877
|
}
|
|
2326
2878
|
|
|
@@ -2328,7 +2880,7 @@ async function fromClaudeMd() {
|
|
|
2328
2880
|
if (notInstalled.length > 0) {
|
|
2329
2881
|
console.log();
|
|
2330
2882
|
console.log(c.bold + ` Install all ${notInstalled.length} missing hooks:` + c.reset);
|
|
2331
|
-
console.log(c.dim + ' npx cc-safe-setup --shield' + c.reset);
|
|
2883
|
+
console.log(c.dim + ' npx github:yurukusa/cc-safe-setup --shield' + c.reset);
|
|
2332
2884
|
} else {
|
|
2333
2885
|
console.log();
|
|
2334
2886
|
console.log(c.green + ' All detected rules are already enforced by hooks!' + c.reset);
|
|
@@ -2344,7 +2896,7 @@ async function health() {
|
|
|
2344
2896
|
|
|
2345
2897
|
if (!existsSync(HOOKS_DIR)) {
|
|
2346
2898
|
console.log(c.red + ' No hooks directory found.' + c.reset);
|
|
2347
|
-
console.log(c.dim + ' Run: npx cc-safe-setup --shield' + c.reset);
|
|
2899
|
+
console.log(c.dim + ' Run: npx github:yurukusa/cc-safe-setup --shield' + c.reset);
|
|
2348
2900
|
return;
|
|
2349
2901
|
}
|
|
2350
2902
|
|
|
@@ -2396,7 +2948,7 @@ async function health() {
|
|
|
2396
2948
|
|
|
2397
2949
|
if (issues > 0) {
|
|
2398
2950
|
console.log();
|
|
2399
|
-
console.log(c.yellow + ' Fix issues: npx cc-safe-setup --quickfix' + c.reset);
|
|
2951
|
+
console.log(c.yellow + ' Fix issues: npx github:yurukusa/cc-safe-setup --quickfix' + c.reset);
|
|
2400
2952
|
}
|
|
2401
2953
|
|
|
2402
2954
|
// Settings.json hook count
|
|
@@ -2479,7 +3031,7 @@ async function migrateFrom(tool) {
|
|
|
2479
3031
|
const icon = detected ? c.green + '●' + c.reset : c.dim + '○' + c.reset;
|
|
2480
3032
|
console.log(` ${icon} ${c.bold}${id}${c.reset} — ${info.desc}`);
|
|
2481
3033
|
if (detected) console.log(` ${c.green}Detected in your settings${c.reset}`);
|
|
2482
|
-
console.log(` ${c.dim}npx cc-safe-setup --migrate-from ${id}${c.reset}`);
|
|
3034
|
+
console.log(` ${c.dim}npx github:yurukusa/cc-safe-setup --migrate-from ${id}${c.reset}`);
|
|
2483
3035
|
console.log();
|
|
2484
3036
|
}
|
|
2485
3037
|
return;
|
|
@@ -2551,7 +3103,7 @@ async function migrateFrom(tool) {
|
|
|
2551
3103
|
console.log(` ${c.green}${replacements.length}${c.reset} hooks can be replaced with cc-safe-setup equivalents`);
|
|
2552
3104
|
console.log(` ${c.dim}${existingHooks.length - replacements.length}${c.reset} hooks will be kept as-is`);
|
|
2553
3105
|
console.log();
|
|
2554
|
-
console.log(c.dim + ' To apply: npx cc-safe-setup --shield' + c.reset);
|
|
3106
|
+
console.log(c.dim + ' To apply: npx github:yurukusa/cc-safe-setup --shield' + c.reset);
|
|
2555
3107
|
console.log(c.dim + ' This installs cc-safe-setup hooks alongside existing ones.' + c.reset);
|
|
2556
3108
|
console.log(c.dim + ' Remove old hooks manually after verifying the new ones work.' + c.reset);
|
|
2557
3109
|
console.log();
|
|
@@ -2664,6 +3216,13 @@ async function team() {
|
|
|
2664
3216
|
console.log(c.dim + ` ${installed} hooks installed, ${allHooks.length} total configured.` + c.reset);
|
|
2665
3217
|
console.log(c.dim + ' Hooks use relative paths (.claude/hooks/) — portable across machines.' + c.reset);
|
|
2666
3218
|
console.log();
|
|
3219
|
+
// Highest-intent moment: the user just engaged. Offer genuinely useful FREE next
|
|
3220
|
+
// steps first (value-first, not a sales pitch). The deeper paid guides have free
|
|
3221
|
+
// chapters, so they read as "go deeper if this helped" rather than an upsell.
|
|
3222
|
+
console.log(c.bold + ' Go deeper (all free to start):' + c.reset);
|
|
3223
|
+
console.log(c.dim + ' • Cut token cost (Ch.1 free): https://yurukusa.github.io/cc-safe-setup/token-book.html' + c.reset);
|
|
3224
|
+
console.log(c.dim + ' • Incident-prevention guide (Ch.3 free): https://zenn.dev/yurukusa/books/6076c23b1cb18b' + c.reset);
|
|
3225
|
+
console.log();
|
|
2667
3226
|
}
|
|
2668
3227
|
|
|
2669
3228
|
async function profile(level) {
|
|
@@ -2715,7 +3274,7 @@ async function profile(level) {
|
|
|
2715
3274
|
for (const [name, prof] of Object.entries(PROFILES)) {
|
|
2716
3275
|
console.log(` ${c.bold}${name}${c.reset} (${prof.hooks.length} hooks)`);
|
|
2717
3276
|
console.log(` ${c.dim}${prof.desc}${c.reset}`);
|
|
2718
|
-
console.log(` ${c.dim}npx cc-safe-setup --profile ${name}${c.reset}`);
|
|
3277
|
+
console.log(` ${c.dim}npx github:yurukusa/cc-safe-setup --profile ${name}${c.reset}`);
|
|
2719
3278
|
console.log();
|
|
2720
3279
|
}
|
|
2721
3280
|
|
|
@@ -2731,7 +3290,7 @@ async function profile(level) {
|
|
|
2731
3290
|
const data = JSON.parse(readFileSync(join(profilesDir, f), 'utf-8'));
|
|
2732
3291
|
console.log(` ${c.bold}${sName}${c.reset} (${data.hooks?.length || 0} hooks)`);
|
|
2733
3292
|
console.log(` ${c.dim}Saved ${data.savedAt?.split('T')[0] || '?'}${c.reset}`);
|
|
2734
|
-
console.log(` ${c.dim}npx cc-safe-setup --profile ${sName}${c.reset}`);
|
|
3293
|
+
console.log(` ${c.dim}npx github:yurukusa/cc-safe-setup --profile ${sName}${c.reset}`);
|
|
2735
3294
|
console.log();
|
|
2736
3295
|
} catch {}
|
|
2737
3296
|
}
|
|
@@ -2778,10 +3337,7 @@ async function profile(level) {
|
|
|
2778
3337
|
}
|
|
2779
3338
|
|
|
2780
3339
|
// Update settings.json
|
|
2781
|
-
let settings =
|
|
2782
|
-
if (existsSync(SETTINGS_PATH)) {
|
|
2783
|
-
try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
|
|
2784
|
-
}
|
|
3340
|
+
let settings = readSettingsForWrite();
|
|
2785
3341
|
if (!settings.hooks) settings.hooks = {};
|
|
2786
3342
|
|
|
2787
3343
|
// Register all hooks in settings
|
|
@@ -2820,8 +3376,7 @@ async function analyze() {
|
|
|
2820
3376
|
const blockLog = join(HOME, '.claude', 'blocked-commands.log');
|
|
2821
3377
|
let blocks = [];
|
|
2822
3378
|
if (existsSync(blockLog)) {
|
|
2823
|
-
|
|
2824
|
-
blocks = content.split('\n').filter(l => l.trim());
|
|
3379
|
+
blocks = readBlockRecords(blockLog);
|
|
2825
3380
|
const recent = blocks.slice(-20);
|
|
2826
3381
|
console.log(c.bold + ' Blocked Commands' + c.reset + c.dim + ` (${blocks.length} total)` + c.reset);
|
|
2827
3382
|
if (recent.length > 0) {
|
|
@@ -2878,7 +3433,7 @@ async function analyze() {
|
|
|
2878
3433
|
|
|
2879
3434
|
// 4. Hook health
|
|
2880
3435
|
console.log(c.bold + ' Hook Health' + c.reset);
|
|
2881
|
-
const hookDir =
|
|
3436
|
+
const hookDir = HOOKS_DIR;
|
|
2882
3437
|
if (existsSync(hookDir)) {
|
|
2883
3438
|
const hooks = readdirSync(hookDir).filter(f => f.endsWith('.sh') || f.endsWith('.py'));
|
|
2884
3439
|
let execCount = 0, nonExec = 0;
|
|
@@ -2918,6 +3473,353 @@ async function analyze() {
|
|
|
2918
3473
|
console.log();
|
|
2919
3474
|
}
|
|
2920
3475
|
|
|
3476
|
+
// --blindspots (2026-09-03): 層をまたぐ診断。単層の点検では構造的に出ない欠陥を出す。
|
|
3477
|
+
//
|
|
3478
|
+
// このツールの他の命令は、どれも1つの層しか見ていない——ディスクの上のフック(--status)、
|
|
3479
|
+
// 設定ファイル(--lint)、阻止の記録(--stats)、配布版との差(--outdated)。
|
|
3480
|
+
// フックは在って、実行権があって、登録されていて、最新であっても、
|
|
3481
|
+
// この環境で実際に打たれるコマンドの形がその位置に届かなければ、一度も走らない。
|
|
3482
|
+
// その差は、セッションのログ(層4)をフックの正規表現(層1)の隣に置いて初めて見える。
|
|
3483
|
+
//
|
|
3484
|
+
// 読むのは全部ローカル。ネットワークへは何も送らない。
|
|
3485
|
+
async function blindspots() {
|
|
3486
|
+
const { createReadStream, statSync } = await import('fs');
|
|
3487
|
+
const { createInterface: makeLines } = await import('readline');
|
|
3488
|
+
const PROJECTS_DIR = join(HOME, '.claude', 'projects');
|
|
3489
|
+
|
|
3490
|
+
console.log();
|
|
3491
|
+
console.log(c.bold + ' cc-safe-setup --blindspots' + c.reset);
|
|
3492
|
+
console.log(c.dim + ' What your guards never see — measured against your own sessions' + c.reset);
|
|
3493
|
+
console.log();
|
|
3494
|
+
|
|
3495
|
+
if (!existsSync(PROJECTS_DIR)) {
|
|
3496
|
+
console.log(c.yellow + ' No session transcripts found at ' + PROJECTS_DIR + c.reset);
|
|
3497
|
+
console.log(c.dim + ' This check needs your own Claude Code history to say anything.' + c.reset);
|
|
3498
|
+
console.log();
|
|
3499
|
+
return;
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
// ---- 層4: 実際に打たれたコマンドを集める --------------------------------
|
|
3503
|
+
// 転写は2つの深さに置かれる。プロジェクト直下がメインのセッション、
|
|
3504
|
+
// その下の <session>/subagents/ がサブエージェントのもの。
|
|
3505
|
+
// 直下だけを読んでいた版は、この機械で 571 本中 271 本(47%)を落としていた。
|
|
3506
|
+
// サブエージェントの Bash にもフックは走るので、落とすと母集団が半分になる。
|
|
3507
|
+
const files = [];
|
|
3508
|
+
const collect = (dir, depth) => {
|
|
3509
|
+
// 上限は8。サブエージェントは入れ子になり、実測で project/session/subagents/…/subagents/…
|
|
3510
|
+
// まで潜っていた(上限4だと571本中418本しか読めなかった)。
|
|
3511
|
+
if (depth > 8) return;
|
|
3512
|
+
let entries;
|
|
3513
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
3514
|
+
for (const e of entries) {
|
|
3515
|
+
const p = join(dir, e.name);
|
|
3516
|
+
if (e.isDirectory()) { collect(p, depth + 1); continue; }
|
|
3517
|
+
if (!e.name.endsWith('.jsonl')) continue;
|
|
3518
|
+
try { files.push({ path: p, size: statSync(p).size }); } catch {}
|
|
3519
|
+
}
|
|
3520
|
+
};
|
|
3521
|
+
collect(PROJECTS_DIR, 0);
|
|
3522
|
+
const totalBytes = files.reduce((a, f) => a + f.size, 0);
|
|
3523
|
+
if (files.length === 0) {
|
|
3524
|
+
console.log(c.yellow + ' No .jsonl transcripts under ' + PROJECTS_DIR + c.reset);
|
|
3525
|
+
console.log();
|
|
3526
|
+
return;
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
const subCount = files.filter((f) => f.path.includes('/subagents/')).length;
|
|
3530
|
+
console.log(c.dim + ` reading ${files.length} transcripts (${(totalBytes / 1048576).toFixed(0)} MB, ${subCount} from subagents) — local only, nothing is sent` + c.reset);
|
|
3531
|
+
|
|
3532
|
+
const startedAt = Date.now();
|
|
3533
|
+
const commands = [];
|
|
3534
|
+
let firstTs = null, lastTs = null;
|
|
3535
|
+
for (const f of files) {
|
|
3536
|
+
await new Promise((resolve) => {
|
|
3537
|
+
const rl = makeLines({ input: createReadStream(f.path, { encoding: 'utf-8' }), crlfDelay: Infinity });
|
|
3538
|
+
rl.on('line', (line) => {
|
|
3539
|
+
if (line.indexOf('"tool_use"') === -1) return; // 大半の行はここで落ちる(速さのため)
|
|
3540
|
+
let d;
|
|
3541
|
+
try { d = JSON.parse(line); } catch { return; }
|
|
3542
|
+
if (d.type !== 'assistant') return;
|
|
3543
|
+
const content = (d.message || {}).content;
|
|
3544
|
+
if (!Array.isArray(content)) return;
|
|
3545
|
+
for (const blk of content) {
|
|
3546
|
+
if (!blk || blk.type !== 'tool_use' || blk.name !== 'Bash') continue;
|
|
3547
|
+
const cmd = (blk.input || {}).command;
|
|
3548
|
+
if (typeof cmd !== 'string' || !cmd.trim()) continue;
|
|
3549
|
+
commands.push(cmd);
|
|
3550
|
+
const ts = d.timestamp;
|
|
3551
|
+
if (ts) {
|
|
3552
|
+
if (!firstTs || ts < firstTs) firstTs = ts;
|
|
3553
|
+
if (!lastTs || ts > lastTs) lastTs = ts;
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
});
|
|
3557
|
+
rl.on('close', resolve);
|
|
3558
|
+
rl.on('error', resolve);
|
|
3559
|
+
});
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
const N = commands.length;
|
|
3563
|
+
if (N === 0) {
|
|
3564
|
+
console.log(c.yellow + ' Transcripts found, but no Bash calls in them. Nothing to compare against.' + c.reset);
|
|
3565
|
+
console.log();
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
const compound = commands.filter(x => /(&&|\|\||;|\|)/.test(x)).length;
|
|
3570
|
+
const startsCd = commands.filter(x => /^\s*cd\s/.test(x)).length;
|
|
3571
|
+
const pct = (n) => ((n / N) * 100).toFixed(1) + '%';
|
|
3572
|
+
|
|
3573
|
+
console.log();
|
|
3574
|
+
console.log(c.bold + ' Layer 4 · what you actually ask for' + c.reset);
|
|
3575
|
+
console.log(` ${c.bold}${N.toLocaleString()}${c.reset} Bash tool calls` +
|
|
3576
|
+
(firstTs && lastTs ? c.dim + ` (dated ones span ${firstTs.slice(0, 10)} → ${lastTs.slice(0, 10)})` + c.reset : ''));
|
|
3577
|
+
console.log(c.dim + ` read in ${((Date.now() - startedAt) / 1000).toFixed(1)}s` + c.reset);
|
|
3578
|
+
// 数えているのは要求であって実行ではない。PreToolUse はどちらにせよ要求の時点で
|
|
3579
|
+
// 呼ばれるので、「フックが見たか」を問う目的にはこれが正しい単位だが、
|
|
3580
|
+
// 「これだけ走った」と読まれないように明示する。
|
|
3581
|
+
// また、突き合わせているのは全プロジェクトの履歴と「いまの」フックである点も書く。
|
|
3582
|
+
console.log(c.dim + ' Counted as requested, before any hook or human allowed them, which is the' + c.reset);
|
|
3583
|
+
console.log(c.dim + ' point a PreToolUse hook is consulted. History spans every project; the' + c.reset);
|
|
3584
|
+
console.log(c.dim + ' hooks compared against it are the ones installed now.' + c.reset);
|
|
3585
|
+
console.log(` ${pct(compound).padStart(6)} contain a separator (${c.dim}&& ; || |${c.reset}) — the command a guard sees first is not the whole command`);
|
|
3586
|
+
console.log(` ${pct(startsCd).padStart(6)} begin with ${c.dim}cd ${c.reset}— anything anchored to the start of the string sees ${c.dim}cd${c.reset}, not the verb`);
|
|
3587
|
+
console.log();
|
|
3588
|
+
|
|
3589
|
+
// ---- 層2+3: 登録されているのに実体が無いフック ---------------------------
|
|
3590
|
+
// 設定は「登録した」と言い、ディスクには無い。どちらの層を単独で見ても矛盾に見えない。
|
|
3591
|
+
const zombies = [];
|
|
3592
|
+
const registeredNames = new Set();
|
|
3593
|
+
let registered = 0;
|
|
3594
|
+
let settings = {};
|
|
3595
|
+
try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
|
|
3596
|
+
const expand = (p) => p
|
|
3597
|
+
.replace(/\$\{?CLAUDE_PROJECT_DIR\}?/g, CLAUDE_BASE)
|
|
3598
|
+
.replace(/\$\{?HOME\}?/g, HOME)
|
|
3599
|
+
.replace(/^~/, HOME);
|
|
3600
|
+
for (const entries of Object.values(settings.hooks || {})) {
|
|
3601
|
+
for (const entry of entries || []) {
|
|
3602
|
+
for (const h of (entry || {}).hooks || []) {
|
|
3603
|
+
if (!h || h.type !== 'command' || typeof h.command !== 'string') continue;
|
|
3604
|
+
const m = h.command.match(/(\S*\/[\w.-]+\.(?:sh|py|js|mjs))/);
|
|
3605
|
+
if (!m) continue; // インラインのシェルは対象外(実体を持たない)
|
|
3606
|
+
registered++;
|
|
3607
|
+
const p = expand(m[1]).replace(/^["']|["']$/g, '');
|
|
3608
|
+
registeredNames.add(p.split('/').pop());
|
|
3609
|
+
if (!existsSync(p)) zombies.push(p);
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
console.log(c.bold + ' Layers 2+3 · registered vs. present on disk' + c.reset);
|
|
3614
|
+
if (registered === 0) {
|
|
3615
|
+
console.log(c.dim + ' No script-backed hooks registered in ' + SETTINGS_PATH + c.reset);
|
|
3616
|
+
} else if (zombies.length === 0) {
|
|
3617
|
+
console.log(` ${c.green}✓${c.reset} all ${registered} script-backed hook registrations point at a file that exists`);
|
|
3618
|
+
} else {
|
|
3619
|
+
console.log(` ${c.red}✗${c.reset} ${zombies.length} of ${registered} registrations point at a file that is not there.`);
|
|
3620
|
+
console.log(c.dim + ' They never run, and nothing reports it.' + c.reset);
|
|
3621
|
+
for (const z of zombies.slice(0, 6)) console.log(` ${c.red}·${c.reset} ${z}`);
|
|
3622
|
+
if (zombies.length > 6) console.log(c.dim + ` … and ${zombies.length - 6} more` + c.reset);
|
|
3623
|
+
}
|
|
3624
|
+
console.log();
|
|
3625
|
+
|
|
3626
|
+
// ---- 層1+4: 行頭に錨を置いた正規表現と、実際の形を突き合わせる -----------
|
|
3627
|
+
// これは走査であって、フックの意味解析ではない。取りこぼしも空振りもある(下に件数を出す)。
|
|
3628
|
+
const posix = (s) => s
|
|
3629
|
+
.replace(/\[\[:space:\]\]/g, '\\s')
|
|
3630
|
+
.replace(/\[\[:alpha:\]\]/g, '[A-Za-z]')
|
|
3631
|
+
.replace(/\[\[:alnum:\]\]/g, '[A-Za-z0-9]')
|
|
3632
|
+
.replace(/\[\[:digit:\]\]/g, '\\d');
|
|
3633
|
+
|
|
3634
|
+
const words = (pat) => posix(pat)
|
|
3635
|
+
.replace(/\(\?[:=!][^)]*\)/g, ' ')
|
|
3636
|
+
.replace(/\[[^\]]*\]/g, ' ')
|
|
3637
|
+
.replace(/\\[sStTwWdDbB]/g, ' ')
|
|
3638
|
+
.replace(/[*+?{}()|^$.\\]/g, ' ')
|
|
3639
|
+
.split(/\s+/)
|
|
3640
|
+
.filter(w => /^[A-Za-z][A-Za-z0-9_.-]*$/.test(w) && w.length > 1);
|
|
3641
|
+
|
|
3642
|
+
// 見つけた錨のうち、どれが「門」でどれが「免除」かを分ける。
|
|
3643
|
+
// ここを分けないと、読み取り専用の命令を素通しさせるための ^grep や ^git が
|
|
3644
|
+
// 「守れていない門」として並ぶ。それは計器の側の欠陥であって、利用者の設定の欠陥ではない。
|
|
3645
|
+
// 判定は、その錨の直後に最初に現れる終了コード: exit 2 なら門、exit 0 なら免除。
|
|
3646
|
+
const classify = (src, at, line) => {
|
|
3647
|
+
// 複合条件の一項(行末が && \ など)は、それ単独では門でも守備範囲でもない。
|
|
3648
|
+
// 例: `if ! echo "$CMD" | grep -qE '^\s*git\s' && echo "$CMD" | grep -qiE '(tweet|...)'`
|
|
3649
|
+
// ここでの ^git は「git は対象外」という除外であって、git の門ではない。
|
|
3650
|
+
if (/(&&|\|\|)\s*\\?\s*$/.test(line)) return 'exempt';
|
|
3651
|
+
const tail = src.slice(at, at + 600);
|
|
3652
|
+
const block = tail.search(/exit\s+2|sys\.exit\(2\)|EXIT_BLOCK/);
|
|
3653
|
+
const allow = tail.search(/exit\s+0|sys\.exit\(0\)|return\s+0/);
|
|
3654
|
+
// 否定つきの早期脱出(`if ! ... '^git push'; then exit 0`)は、
|
|
3655
|
+
// その錨がこのフックの守備範囲そのものであることを意味する。
|
|
3656
|
+
// 錨に当たらない命令は、拒否されないのではなく、一度も見られない。
|
|
3657
|
+
if (/(^|\s)!\s*(printf|echo|cat|grep|\[\[)/.test(line) || /grep\s+-[a-zA-Z]*v/.test(line)) {
|
|
3658
|
+
return (allow !== -1 && (block === -1 || allow < block)) ? 'subject' : 'exempt';
|
|
3659
|
+
}
|
|
3660
|
+
if (block === -1 && allow === -1) return 'unknown';
|
|
3661
|
+
if (block === -1) return 'exempt';
|
|
3662
|
+
if (allow === -1) return 'block';
|
|
3663
|
+
return block < allow ? 'block' : 'exempt';
|
|
3664
|
+
};
|
|
3665
|
+
|
|
3666
|
+
const found = [];
|
|
3667
|
+
const unregistered = [];
|
|
3668
|
+
let scanned = 0, unparsed = 0, exempt = 0;
|
|
3669
|
+
if (existsSync(HOOKS_DIR)) {
|
|
3670
|
+
for (const f of readdirSync(HOOKS_DIR)) {
|
|
3671
|
+
if (!/\.(sh|py|bash)$/.test(f)) continue;
|
|
3672
|
+
// 登録されていないスクリプトは一度も走らない。その錨の見落とし率を並べると、
|
|
3673
|
+
// 走りもしないフックを「守れていない門」として報告することになる。
|
|
3674
|
+
// 登録が1つも読めなかった時だけ、全ファイルを対象にする(設定が別の場所にある環境)。
|
|
3675
|
+
if (registeredNames.size > 0 && !registeredNames.has(f)) { unregistered.push(f); continue; }
|
|
3676
|
+
let src;
|
|
3677
|
+
try { src = readFileSync(join(HOOKS_DIR, f), 'utf-8'); } catch { continue; }
|
|
3678
|
+
const pats = new Map(); // pattern -> { multiline, kind }
|
|
3679
|
+
const rank = { exempt: 0, unknown: 1, subject: 2, block: 3 };
|
|
3680
|
+
const note = (pat, at, multiline, line) => {
|
|
3681
|
+
const kind = classify(src, at, line);
|
|
3682
|
+
const prev = pats.get(pat);
|
|
3683
|
+
// 同じ錨が複数回出るときは、いちばん強い用法(門 > 守備範囲 > 免除)を採る
|
|
3684
|
+
if (!prev) pats.set(pat, { multiline, kind });
|
|
3685
|
+
else if (rank[kind] > rank[prev.kind]) pats.set(pat, { multiline: prev.multiline || multiline, kind });
|
|
3686
|
+
};
|
|
3687
|
+
let m;
|
|
3688
|
+
// grep は行ごとに判定するので、複数行の命令では ^ が2行目以降の行頭にも当たる。
|
|
3689
|
+
// bash の =~ は文字列の先頭にしか当たらない。この差を無視すると見落としを過大に出す。
|
|
3690
|
+
const quoted = /(['"])\^([^'"\n]{2,120})\1/g; // grep -qE '^\s*git\s+push'
|
|
3691
|
+
while ((m = quoted.exec(src)) !== null) {
|
|
3692
|
+
const lineStart = src.lastIndexOf('\n', m.index) + 1;
|
|
3693
|
+
const line = src.slice(lineStart, src.indexOf('\n', m.index));
|
|
3694
|
+
note(m[2], m.index, /grep|egrep|awk|sed/.test(line), line);
|
|
3695
|
+
}
|
|
3696
|
+
const bashRe = /=~\s*\^([^\s\]]{2,120})/g; // [[ "$CMD" =~ ^rm[[:space:]] ]]
|
|
3697
|
+
while ((m = bashRe.exec(src)) !== null) {
|
|
3698
|
+
const ls = src.lastIndexOf('\n', m.index) + 1;
|
|
3699
|
+
note(m[1], m.index, false, src.slice(ls, src.indexOf('\n', m.index)));
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
for (const [pat, meta] of pats) {
|
|
3703
|
+
scanned++;
|
|
3704
|
+
if (meta.kind !== 'block' && meta.kind !== 'subject') { exempt++; continue; }
|
|
3705
|
+
const w = words(pat);
|
|
3706
|
+
if (w.length === 0) { unparsed++; continue; }
|
|
3707
|
+
let anchored, loose;
|
|
3708
|
+
try {
|
|
3709
|
+
anchored = new RegExp('^\\s*' + posix(pat), meta.multiline ? 'm' : '');
|
|
3710
|
+
// 命令として実際に呼ばれる位置だけを数える=行頭か、区切り(&& || ; | $( )の直後。
|
|
3711
|
+
// 「どこかに含まれる」で数えると、sed の置換文字列やコミット本文の中の語まで入る。
|
|
3712
|
+
const verb = w.map(x => x.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('\\s+');
|
|
3713
|
+
loose = new RegExp('(?:^|&&|\\|\\||;|\\||\\$\\()\\s*' + verb + '\\b', 'm');
|
|
3714
|
+
} catch { unparsed++; continue; }
|
|
3715
|
+
found.push({ hook: f, pat, words: w.join(' '), anchored, loose, kind: meta.kind });
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
const rows = [];
|
|
3721
|
+
for (const g of found) {
|
|
3722
|
+
let intent = 0, sees = 0;
|
|
3723
|
+
let sample = null;
|
|
3724
|
+
for (const cmd of commands) {
|
|
3725
|
+
if (!g.loose.test(cmd)) continue;
|
|
3726
|
+
intent++;
|
|
3727
|
+
if (g.anchored.test(cmd)) sees++;
|
|
3728
|
+
else if (!sample) sample = cmd;
|
|
3729
|
+
}
|
|
3730
|
+
if (intent < 20) continue; // 少なすぎる語は雑音なので出さない
|
|
3731
|
+
const blind = intent - sees;
|
|
3732
|
+
if (blind === 0) continue;
|
|
3733
|
+
rows.push({ hook: g.hook, words: g.words, intent, sees, blind, rate: blind / intent, sample, kind: g.kind });
|
|
3734
|
+
}
|
|
3735
|
+
// 同じ語を複数のフックが持つことがあるので、見落としの大きい順に一意化する
|
|
3736
|
+
rows.sort((a, b) => b.blind - a.blind);
|
|
3737
|
+
const seenWords = new Set();
|
|
3738
|
+
const top = rows.filter(r => (seenWords.has(r.words) ? false : seenWords.add(r.words)));
|
|
3739
|
+
|
|
3740
|
+
// 在るが登録されていないスクリプトは、走査の対象から外してある(走らないものに
|
|
3741
|
+
// 「守れていない」は言えない)。列挙そのものは --audit の層またぎの検査が
|
|
3742
|
+
// 精度を吟味した形で既に持っているので、ここでは除外した事実だけを伝えて渡す。
|
|
3743
|
+
if (unregistered.length > 0) {
|
|
3744
|
+
console.log(c.dim + ` ${unregistered.length} more script${unregistered.length === 1 ? '' : 's'} in ${HOOKS_DIR} ${unregistered.length === 1 ? 'is' : 'are'} registered nowhere,` +
|
|
3745
|
+
' so they are left out of the table below — they never run. `--audit` names them.' + c.reset);
|
|
3746
|
+
console.log();
|
|
3747
|
+
}
|
|
3748
|
+
|
|
3749
|
+
console.log(c.bold + ' Layers 1+4 · patterns anchored to the start vs. what you type' + c.reset);
|
|
3750
|
+
if (found.length === 0) {
|
|
3751
|
+
console.log(c.dim + ' No start-anchored patterns found in ' + HOOKS_DIR + c.reset);
|
|
3752
|
+
} else if (top.length === 0) {
|
|
3753
|
+
// 「調べたが差が無かった」と「調べられる材料が無かった」を同じ緑で出さない。
|
|
3754
|
+
// 稀にしか打たない破壊的な操作ほど traffic が薄く、judged の外へ落ちる。
|
|
3755
|
+
const judged = found.length;
|
|
3756
|
+
if (judged === 0) {
|
|
3757
|
+
console.log(` ${c.yellow}—${c.reset} no gate pattern could be evaluated. This says nothing about your setup.`);
|
|
3758
|
+
} else {
|
|
3759
|
+
console.log(` ${c.green}✓${c.reset} the ${judged} gate pattern${judged === 1 ? '' : 's'} with enough traffic to judge matched all of it`);
|
|
3760
|
+
console.log(c.dim + ' A verb you rarely type has too little traffic to judge here, and the' + c.reset);
|
|
3761
|
+
console.log(c.dim + ' rare ones are usually the destructive ones. Green is not coverage.' + c.reset);
|
|
3762
|
+
}
|
|
3763
|
+
console.log(c.dim + ` (${scanned} anchors scanned; ${exempt} allow/exempt, ${unparsed} unreadable)` + c.reset);
|
|
3764
|
+
} else {
|
|
3765
|
+
console.log(c.dim + ' intent = calls where that verb starts a command segment (line start, or after && || ; |)' + c.reset);
|
|
3766
|
+
console.log(c.dim + ' seen = calls the hook\'s start-anchored pattern actually matches' + c.reset);
|
|
3767
|
+
console.log();
|
|
3768
|
+
console.log(c.dim + ' verb hook intent seen blind effect' + c.reset);
|
|
3769
|
+
for (const r of top.slice(0, 8)) {
|
|
3770
|
+
const col = r.rate > 0.5 ? c.red : c.yellow;
|
|
3771
|
+
console.log(
|
|
3772
|
+
' ' + r.words.padEnd(17).slice(0, 17) +
|
|
3773
|
+
' ' + r.hook.padEnd(27).slice(0, 27) +
|
|
3774
|
+
' ' + String(r.intent).padStart(6) +
|
|
3775
|
+
' ' + String(r.sees).padStart(6) +
|
|
3776
|
+
' ' + col + (r.rate * 100).toFixed(1).padStart(5) + '%' + c.reset +
|
|
3777
|
+
' ' + c.dim + (r.kind === 'subject' ? 'never examined' : 'not blocked') + c.reset
|
|
3778
|
+
);
|
|
3779
|
+
}
|
|
3780
|
+
const worst = top[0];
|
|
3781
|
+
if (worst && worst.sample) {
|
|
3782
|
+
// 実物をそのまま出さない。利用者の履歴には鍵・顧客名・内部パスが混じり、
|
|
3783
|
+
// この出力は画面共有や CI のログへ流れる。読者に要るのは中身ではなく形
|
|
3784
|
+
// ——「区切りの後ろに来ていたから錨に当たらなかった」——なので、
|
|
3785
|
+
// 各区分の先頭の語(と、パスや代入でない素の副命令)だけ残して他は落とす。
|
|
3786
|
+
const shape = worst.sample
|
|
3787
|
+
.split(/\s*(&&|\|\||;|\|)\s*/)
|
|
3788
|
+
.map((seg, i) => {
|
|
3789
|
+
if (i % 2 === 1) return seg;
|
|
3790
|
+
const t = seg.trim().split(/\s+/).filter(Boolean);
|
|
3791
|
+
if (t.length === 0) return '';
|
|
3792
|
+
const sub = (t[1] && /^[a-z][a-z0-9-]*$/.test(t[1])) ? ' ' + t[1] : '';
|
|
3793
|
+
return t[0] + sub + (t.length > (sub ? 2 : 1) ? ' …' : '');
|
|
3794
|
+
})
|
|
3795
|
+
.filter(Boolean).join(' ').replace(/\s+/g, ' ').slice(0, 120);
|
|
3796
|
+
console.log();
|
|
3797
|
+
console.log(c.dim + ' the shape of one that went past ' + worst.hook + ' (arguments removed):' + c.reset);
|
|
3798
|
+
console.log(' ' + c.dim + shape + c.reset);
|
|
3799
|
+
}
|
|
3800
|
+
console.log();
|
|
3801
|
+
console.log(c.dim + ` (${scanned} anchored patterns scanned; ${exempt} are allow/exempt tests and are not counted as gaps;` +
|
|
3802
|
+
` ${unparsed} could not be read. This is a scan, not a parse of your hooks: it can miss patterns,` +
|
|
3803
|
+
' and it can name one that a later line already handles. A verb on its own line inside a' +
|
|
3804
|
+
' heredoc is counted as an invocation, so intent is an upper bound.)' + c.reset);
|
|
3805
|
+
}
|
|
3806
|
+
console.log();
|
|
3807
|
+
|
|
3808
|
+
console.log(c.bold + ' Why no single-layer check reports this' + c.reset);
|
|
3809
|
+
console.log(c.dim + ' --status sees the files. --lint sees the config. --outdated sees the versions.' + c.reset);
|
|
3810
|
+
console.log(c.dim + ' A guard can pass all three and still stand where your traffic never goes.' + c.reset);
|
|
3811
|
+
console.log(c.dim + ' The gap only exists between layers, so only a comparison across them can find it.' + c.reset);
|
|
3812
|
+
console.log();
|
|
3813
|
+
console.log(c.dim + ' Fixes: match anywhere in the command, not only at the start; register file guards on' + c.reset);
|
|
3814
|
+
console.log(c.dim + ' Bash as well as Read/Edit; and put static paths in permissions.deny, which reads' + c.reset);
|
|
3815
|
+
console.log(c.dim + ' inside Bash commands. See SAFETY_CHECKLIST.md.' + c.reset);
|
|
3816
|
+
console.log();
|
|
3817
|
+
console.log(c.dim + ' This covers two of the five layers. The same method run across all five on one real' + c.reset);
|
|
3818
|
+
console.log(c.dim + ' machine, findings and wrong turns included, is written out in full:' + c.reset);
|
|
3819
|
+
console.log(c.dim + ' docs/full-surface-audit-sample-jp.md' + c.reset);
|
|
3820
|
+
console.log();
|
|
3821
|
+
}
|
|
3822
|
+
|
|
2921
3823
|
async function opus47() {
|
|
2922
3824
|
console.log();
|
|
2923
3825
|
console.log(c.bold + ' 🚨 cc-safe-setup --opus47' + c.reset);
|
|
@@ -3009,9 +3911,15 @@ async function shield() {
|
|
|
3009
3911
|
// Run the default install
|
|
3010
3912
|
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
3011
3913
|
let installed = 0;
|
|
3914
|
+
// 今回このコマンドが *新しくディスクへ置いた* フックだけを集める。
|
|
3915
|
+
// 「自分が配ったフックか」ではなく「今回置いたか」で判定するのが肝。
|
|
3916
|
+
// 既に置いてあるファイルは、利用者が一度見て、登録するかどうかを決めた後のもので、
|
|
3917
|
+
// 登録を外してあるなら、それは決定である。決定を黙って覆さない。
|
|
3918
|
+
const freshlyInstalled = new Set();
|
|
3012
3919
|
for (const [hookId, hookMeta] of Object.entries(HOOKS)) {
|
|
3013
3920
|
const hookPath = join(HOOKS_DIR, `${hookId}.sh`);
|
|
3014
3921
|
if (!existsSync(hookPath)) {
|
|
3922
|
+
freshlyInstalled.add(`${hookId}.sh`);
|
|
3015
3923
|
writeFileSync(hookPath, SCRIPTS[hookId]);
|
|
3016
3924
|
chmodSync(hookPath, 0o755);
|
|
3017
3925
|
installed++;
|
|
@@ -3051,6 +3959,7 @@ async function shield() {
|
|
|
3051
3959
|
const exPath = join(__dirname, 'examples', `${ex}.sh`);
|
|
3052
3960
|
const hookPath = join(HOOKS_DIR, `${ex}.sh`);
|
|
3053
3961
|
if (existsSync(exPath) && !existsSync(hookPath)) {
|
|
3962
|
+
freshlyInstalled.add(`${ex}.sh`);
|
|
3054
3963
|
copyFileSync(exPath, hookPath);
|
|
3055
3964
|
chmodSync(hookPath, 0o755);
|
|
3056
3965
|
console.log(c.green + ' +' + c.reset + ` ${ex}`);
|
|
@@ -3063,16 +3972,39 @@ async function shield() {
|
|
|
3063
3972
|
// Step 4: Update settings.json
|
|
3064
3973
|
console.log();
|
|
3065
3974
|
console.log(c.bold + ' Step 4: Configure settings.json' + c.reset);
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
}
|
|
3975
|
+
const hadConfigFile = existsSync(SETTINGS_PATH);
|
|
3976
|
+
// 書き戻しは 100行以上あとの writeFileSync(SETTINGS_PATH, configNext) で起きる。
|
|
3977
|
+
// 最初にこの経路を数えたとき 60行だけ先を見て「読むだけ」と分類してしまった。
|
|
3978
|
+
// 距離が離れているだけで、壊れた設定を {} として上書きする形は他の5か所と同じ。
|
|
3979
|
+
let settings = readSettingsForWrite();
|
|
3070
3980
|
if (!settings.hooks) settings.hooks = {};
|
|
3071
|
-
|
|
3072
|
-
//
|
|
3073
|
-
|
|
3981
|
+
const configBefore = JSON.stringify(settings, null, 2);
|
|
3982
|
+
// 初回の導入(設定ファイルがまだ無い)では、まだ何の決定も下されていないので、
|
|
3983
|
+
// 途中で失敗した前回の実行を拾い直せるように、置いてあるものを全部登録してよい。
|
|
3984
|
+
const firstTimeSetup = !hadConfigFile;
|
|
3985
|
+
|
|
3986
|
+
// Collect the hooks this run is responsible for.
|
|
3987
|
+
//
|
|
3988
|
+
// Until 2026-07-27 this adopted every .sh in HOOKS_DIR regardless of origin.
|
|
3989
|
+
// Two ways that hurt the operator, both observed in the field:
|
|
3990
|
+
// - a hook another tool dropped in ~/.claude/hooks/ got registered here,
|
|
3991
|
+
// with its trigger *guessed* from the filename
|
|
3992
|
+
// - a hook the operator had deliberately unregistered came back on the next run
|
|
3993
|
+
// The second is the worse of the two: removing a registration is a decision, and
|
|
3994
|
+
// silently reversing a decision is the failure class this project exists to stop.
|
|
3995
|
+
// Default is now "only what this run newly wrote to disk", plus the first-time
|
|
3996
|
+
// setup case where no config file existed yet. --adopt-existing restores the old
|
|
3997
|
+
// sweep for anyone relying on it.
|
|
3998
|
+
const allHookFiles = existsSync(HOOKS_DIR)
|
|
3074
3999
|
? readdirSync(HOOKS_DIR).filter(f => f.endsWith('.sh'))
|
|
3075
4000
|
: [];
|
|
4001
|
+
const adoptAll = ADOPT_EXISTING || firstTimeSetup;
|
|
4002
|
+
const hookFiles = adoptAll ? allHookFiles : allHookFiles.filter(f => freshlyInstalled.has(f));
|
|
4003
|
+
const foreign = allHookFiles.filter(f => !freshlyInstalled.has(f));
|
|
4004
|
+
if (foreign.length > 0 && !adoptAll) {
|
|
4005
|
+
console.log(c.dim + ` ${foreign.length} hook(s) already on disk were left as they are` + c.reset);
|
|
4006
|
+
console.log(c.dim + ' (their registration is yours to decide \u2014 --adopt-existing registers them all)' + c.reset);
|
|
4007
|
+
}
|
|
3076
4008
|
|
|
3077
4009
|
// Build hook entries by trigger type
|
|
3078
4010
|
const preToolHooks = [];
|
|
@@ -3154,8 +4086,15 @@ async function shield() {
|
|
|
3154
4086
|
}
|
|
3155
4087
|
}
|
|
3156
4088
|
|
|
3157
|
-
|
|
3158
|
-
|
|
4089
|
+
// Write only when something actually changed. Rewriting an identical file still
|
|
4090
|
+
// bumps the mtime and still risks clobbering a concurrent edit, for no gain.
|
|
4091
|
+
const configNext = JSON.stringify(settings, null, 2);
|
|
4092
|
+
if (configNext === configBefore) {
|
|
4093
|
+
console.log(c.dim + ' ✓' + c.reset + ' settings.json already up to date (not rewritten)');
|
|
4094
|
+
} else {
|
|
4095
|
+
writeFileSync(SETTINGS_PATH, configNext);
|
|
4096
|
+
console.log(c.green + ' ✓' + c.reset + ' settings.json updated');
|
|
4097
|
+
}
|
|
3159
4098
|
|
|
3160
4099
|
// Step 5: Generate CLAUDE.md template if none exists
|
|
3161
4100
|
console.log();
|
|
@@ -3191,16 +4130,94 @@ async function shield() {
|
|
|
3191
4130
|
const totalHooks = hookFiles.length;
|
|
3192
4131
|
console.log(c.bold + c.green + ' 🛡️ Shield activated!' + c.reset);
|
|
3193
4132
|
console.log(c.dim + ` ${totalHooks} hooks installed and configured.` + c.reset);
|
|
3194
|
-
console.log(c.dim + ' Your Claude Code sessions are now protected.' + c.reset);
|
|
3195
4133
|
console.log();
|
|
3196
|
-
|
|
3197
|
-
|
|
4134
|
+
|
|
4135
|
+
// ここは以前「Your Claude Code sessions are now protected.」と言い切っていた。
|
|
4136
|
+
// それは入れた本数についての事実ではなく、守れているかについての断定で、
|
|
4137
|
+
// 測れる範囲では正しくない。入れた柵の多くは命令の先頭に錨を置くので、
|
|
4138
|
+
// 区切りの後ろ(`cd … && git push …`)には届かない。
|
|
4139
|
+
// 断定を返上して、その人自身の履歴から数えた1つの数字を置く。
|
|
4140
|
+
const shape = await commandShapeSample();
|
|
4141
|
+
if (shape) {
|
|
4142
|
+
console.log(c.bold + ' Before you trust them:' + c.reset);
|
|
4143
|
+
console.log(c.dim + ' Most guards match the start of the command string. In your most recent' + c.reset);
|
|
4144
|
+
console.log(` ${c.dim}sessions, ${c.reset}${c.bold}${shape.compoundPct.toFixed(1)}%${c.reset}${c.dim} of ${shape.calls.toLocaleString()} Bash calls are compound` +
|
|
4145
|
+
` (${c.reset}cd … && git push …${c.dim}) and ${c.reset}${shape.cdPct.toFixed(1)}%${c.dim} begin with ${c.reset}cd${c.dim}.` + c.reset);
|
|
4146
|
+
console.log(c.dim + ' A guard anchored at the start does not see what comes after the separator.' + c.reset);
|
|
4147
|
+
console.log();
|
|
4148
|
+
console.log(c.dim + ' Which of yours have that gap: ' + c.reset + 'npx github:yurukusa/cc-safe-setup --blindspots');
|
|
4149
|
+
} else {
|
|
4150
|
+
console.log(c.dim + ' Most guards match the start of the command string, so a command like' + c.reset);
|
|
4151
|
+
console.log(c.dim + ' `cd … && git push …` can go past them. Once you have some history,' + c.reset);
|
|
4152
|
+
console.log(c.dim + ' --blindspots reads it locally and shows which of yours have that gap.' + c.reset);
|
|
4153
|
+
}
|
|
4154
|
+
console.log();
|
|
4155
|
+
console.log(c.dim + ' Verify: npx github:yurukusa/cc-safe-setup --verify' + c.reset);
|
|
4156
|
+
console.log(c.dim + ' Status: npx github:yurukusa/cc-safe-setup --status' + c.reset);
|
|
3198
4157
|
console.log();
|
|
3199
4158
|
console.log(c.dim + ' Burning tokens too fast? Free diagnosis:' + c.reset);
|
|
3200
4159
|
console.log(c.blue + ' https://yurukusa.github.io/cc-safe-setup/token-checkup.html' + c.reset);
|
|
3201
4160
|
console.log();
|
|
3202
4161
|
}
|
|
3203
4162
|
|
|
4163
|
+
|
|
4164
|
+
// 導入の最後に「あなたのセッションは保護されました」と言い切っていた行の差し替えに使う。
|
|
4165
|
+
// 入れた柵の大半は命令の先頭に錨を置く(`^\s*git\s+push` の形)ので、区切りの後ろは見えない。
|
|
4166
|
+
// その割合は環境によって違うから、こちらで断定せず、その人自身の履歴から数えて見せる。
|
|
4167
|
+
// 予算を切ってあるのは、導入の待ち時間を伸ばさないため(直近の記録だけを新しい順に読む)。
|
|
4168
|
+
// 何も無い環境(初回の利用者)では null を返し、呼び出し側は何も言わない。
|
|
4169
|
+
async function commandShapeSample(budgetBytes = 20 * 1024 * 1024) {
|
|
4170
|
+
const { createReadStream, statSync } = await import('fs');
|
|
4171
|
+
const { createInterface: makeLines } = await import('readline');
|
|
4172
|
+
const dir = join(HOME, '.claude', 'projects');
|
|
4173
|
+
if (!existsSync(dir)) return null;
|
|
4174
|
+
|
|
4175
|
+
const files = [];
|
|
4176
|
+
let projects;
|
|
4177
|
+
try { projects = readdirSync(dir); } catch { return null; }
|
|
4178
|
+
for (const proj of projects) {
|
|
4179
|
+
let entries;
|
|
4180
|
+
try { entries = readdirSync(join(dir, proj)); } catch { continue; }
|
|
4181
|
+
for (const f of entries) {
|
|
4182
|
+
if (!f.endsWith('.jsonl')) continue;
|
|
4183
|
+
const full = join(dir, proj, f);
|
|
4184
|
+
try { const st = statSync(full); files.push({ full, mtime: st.mtimeMs, size: st.size }); } catch {}
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
if (files.length === 0) return null;
|
|
4188
|
+
files.sort((a, b) => b.mtime - a.mtime);
|
|
4189
|
+
|
|
4190
|
+
let budget = budgetBytes, calls = 0, compound = 0, startsCd = 0;
|
|
4191
|
+
for (const f of files) {
|
|
4192
|
+
if (budget <= 0) break;
|
|
4193
|
+
budget -= f.size;
|
|
4194
|
+
await new Promise((resolve) => {
|
|
4195
|
+
const rl = makeLines({ input: createReadStream(f.full, { encoding: 'utf-8' }), crlfDelay: Infinity });
|
|
4196
|
+
rl.on('line', (line) => {
|
|
4197
|
+
if (line.indexOf('"tool_use"') === -1) return;
|
|
4198
|
+
let d;
|
|
4199
|
+
try { d = JSON.parse(line); } catch { return; }
|
|
4200
|
+
if (d.type !== 'assistant') return;
|
|
4201
|
+
const content = (d.message || {}).content;
|
|
4202
|
+
if (!Array.isArray(content)) return;
|
|
4203
|
+
for (const blk of content) {
|
|
4204
|
+
if (!blk || blk.type !== 'tool_use' || blk.name !== 'Bash') continue;
|
|
4205
|
+
const cmd = (blk.input || {}).command;
|
|
4206
|
+
if (typeof cmd !== 'string' || !cmd.trim()) continue;
|
|
4207
|
+
calls++;
|
|
4208
|
+
if (/(&&|\|\||;|\|)/.test(cmd)) compound++;
|
|
4209
|
+
if (/^\s*cd\s/.test(cmd)) startsCd++;
|
|
4210
|
+
}
|
|
4211
|
+
});
|
|
4212
|
+
rl.on('close', resolve);
|
|
4213
|
+
rl.on('error', resolve);
|
|
4214
|
+
});
|
|
4215
|
+
}
|
|
4216
|
+
// 少なすぎる標本から割合を出すと、その数字のほうが嘘になる。
|
|
4217
|
+
if (calls < 200) return null;
|
|
4218
|
+
return { calls, compoundPct: (compound / calls) * 100, cdPct: (startsCd / calls) * 100 };
|
|
4219
|
+
}
|
|
4220
|
+
|
|
3204
4221
|
async function quickfix() {
|
|
3205
4222
|
const { execSync } = await import('child_process');
|
|
3206
4223
|
console.log();
|
|
@@ -3368,7 +4385,7 @@ async function quickfix() {
|
|
|
3368
4385
|
safetyHooks++;
|
|
3369
4386
|
} else {
|
|
3370
4387
|
console.log(c.yellow + ' △' + c.reset + ` Missing critical hook: ${name}`);
|
|
3371
|
-
console.log(c.dim + ' Fix: npx cc-safe-setup' + c.reset);
|
|
4388
|
+
console.log(c.dim + ' Fix: npx github:yurukusa/cc-safe-setup' + c.reset);
|
|
3372
4389
|
warnings++;
|
|
3373
4390
|
}
|
|
3374
4391
|
}
|
|
@@ -3389,8 +4406,8 @@ async function quickfix() {
|
|
|
3389
4406
|
}
|
|
3390
4407
|
if (warnings > 0) {
|
|
3391
4408
|
console.log();
|
|
3392
|
-
console.log(c.yellow + ' Run npx cc-safe-setup to install missing safety hooks' + c.reset);
|
|
3393
|
-
console.log(c.yellow + ' Run npx cc-safe-setup --doctor for detailed diagnosis' + c.reset);
|
|
4409
|
+
console.log(c.yellow + ' Run npx github:yurukusa/cc-safe-setup to install missing safety hooks' + c.reset);
|
|
4410
|
+
console.log(c.yellow + ' Run npx github:yurukusa/cc-safe-setup --doctor for detailed diagnosis' + c.reset);
|
|
3394
4411
|
}
|
|
3395
4412
|
console.log();
|
|
3396
4413
|
}
|
|
@@ -3430,10 +4447,7 @@ async function report() {
|
|
|
3430
4447
|
const emoji = auditScore >= 80 ? '🟢' : auditScore >= 50 ? '🟡' : '🔴';
|
|
3431
4448
|
|
|
3432
4449
|
const blockLog = join(HOME, '.claude', 'blocked-commands.log');
|
|
3433
|
-
|
|
3434
|
-
if (existsSync(blockLog)) {
|
|
3435
|
-
totalBlocks = readFileSync(blockLog, 'utf-8').split('\n').filter(l => l.trim()).length;
|
|
3436
|
-
}
|
|
4450
|
+
const totalBlocks = readBlockRecords(blockLog).length;
|
|
3437
4451
|
|
|
3438
4452
|
const md = `## ${emoji} Claude Code Safety Report
|
|
3439
4453
|
|
|
@@ -3446,8 +4460,8 @@ async function report() {
|
|
|
3446
4460
|
| Generated | ${new Date().toISOString().split('T')[0]} |
|
|
3447
4461
|
|
|
3448
4462
|
### Quick Actions
|
|
3449
|
-
- Audit: \`npx cc-safe-setup --audit\`
|
|
3450
|
-
- Dashboard: \`npx cc-safe-setup --dashboard\`
|
|
4463
|
+
- Audit: \`npx github:yurukusa/cc-safe-setup --audit\`
|
|
4464
|
+
- Dashboard: \`npx github:yurukusa/cc-safe-setup --dashboard\`
|
|
3451
4465
|
- Find hooks: \`npx cc-hook-registry recommend\`
|
|
3452
4466
|
`;
|
|
3453
4467
|
|
|
@@ -3464,7 +4478,7 @@ function generateCI() {
|
|
|
3464
4478
|
const workflowPath = join(workflowDir, 'claude-code-safety.yml');
|
|
3465
4479
|
|
|
3466
4480
|
const workflow = `# Claude Code Safety Audit
|
|
3467
|
-
# Generated by: npx cc-safe-setup --generate-ci
|
|
4481
|
+
# Generated by: npx github:yurukusa/cc-safe-setup --generate-ci
|
|
3468
4482
|
# Runs safety checks on every PR and push to main
|
|
3469
4483
|
|
|
3470
4484
|
name: Claude Code Safety
|
|
@@ -3490,7 +4504,7 @@ jobs:
|
|
|
3490
4504
|
- name: Run safety audit
|
|
3491
4505
|
id: audit
|
|
3492
4506
|
run: |
|
|
3493
|
-
npx cc-safe-setup --audit --json > /tmp/audit.json 2>&1 || true
|
|
4507
|
+
npx github:yurukusa/cc-safe-setup --audit --json > /tmp/audit.json 2>&1 || true
|
|
3494
4508
|
SCORE=\$(cat /tmp/audit.json | jq -r '.score // 0' 2>/dev/null || echo 0)
|
|
3495
4509
|
echo "score=\$SCORE" >> \$GITHUB_OUTPUT
|
|
3496
4510
|
echo "Safety score: \$SCORE/100"
|
|
@@ -3572,10 +4586,10 @@ async function migrate() {
|
|
|
3572
4586
|
|
|
3573
4587
|
// Detection patterns for other projects
|
|
3574
4588
|
const detections = [
|
|
3575
|
-
{ pattern: /safety-net|cc-safety-net|SAFETY_LEVEL/i, project: 'claude-code-safety-net', replacement: 'npx cc-safe-setup (destructive-guard, branch-guard)' },
|
|
3576
|
-
{ pattern: /karanb192|block-dangerous-commands\.js/i, project: 'karanb192/claude-code-hooks', replacement: 'npx cc-safe-setup (destructive-guard)' },
|
|
3577
|
-
{ pattern: /hooks-mastery|disler|pre_tool_use\.py/i, project: 'disler/claude-code-hooks-mastery', replacement: 'npx cc-safe-setup (multiple hooks)' },
|
|
3578
|
-
{ pattern: /cchooks|from cchooks/i, project: 'GowayLee/cchooks', replacement: 'npx cc-safe-setup (bash equivalents)' },
|
|
4589
|
+
{ pattern: /safety-net|cc-safety-net|SAFETY_LEVEL/i, project: 'claude-code-safety-net', replacement: 'npx github:yurukusa/cc-safe-setup (destructive-guard, branch-guard)' },
|
|
4590
|
+
{ pattern: /karanb192|block-dangerous-commands\.js/i, project: 'karanb192/claude-code-hooks', replacement: 'npx github:yurukusa/cc-safe-setup (destructive-guard)' },
|
|
4591
|
+
{ pattern: /hooks-mastery|disler|pre_tool_use\.py/i, project: 'disler/claude-code-hooks-mastery', replacement: 'npx github:yurukusa/cc-safe-setup (multiple hooks)' },
|
|
4592
|
+
{ pattern: /cchooks|from cchooks/i, project: 'GowayLee/cchooks', replacement: 'npx github:yurukusa/cc-safe-setup (bash equivalents)' },
|
|
3579
4593
|
{ pattern: /lasso.*security|prompt.*injection.*pattern/i, project: 'lasso-security/claude-hooks', replacement: 'No direct equivalent (unique functionality)' },
|
|
3580
4594
|
];
|
|
3581
4595
|
|
|
@@ -3619,7 +4633,7 @@ async function migrate() {
|
|
|
3619
4633
|
if (found === 0 && suggestions.length === 0) {
|
|
3620
4634
|
console.log(c.green + ' No migration needed. All hooks are cc-safe-setup native.' + c.reset);
|
|
3621
4635
|
} else {
|
|
3622
|
-
console.log(c.dim + ' Run npx cc-safe-setup to install built-in replacements.' + c.reset);
|
|
4636
|
+
console.log(c.dim + ' Run npx github:yurukusa/cc-safe-setup to install built-in replacements.' + c.reset);
|
|
3623
4637
|
}
|
|
3624
4638
|
console.log();
|
|
3625
4639
|
}
|
|
@@ -3633,7 +4647,7 @@ async function compare(hookA, hookB) {
|
|
|
3633
4647
|
console.log();
|
|
3634
4648
|
|
|
3635
4649
|
if (!hookA || !hookB) {
|
|
3636
|
-
console.log(c.red + ' Usage: npx cc-safe-setup --compare <hook-a.sh> <hook-b.sh>' + c.reset);
|
|
4650
|
+
console.log(c.red + ' Usage: npx github:yurukusa/cc-safe-setup --compare <hook-a.sh> <hook-b.sh>' + c.reset);
|
|
3637
4651
|
process.exit(1);
|
|
3638
4652
|
}
|
|
3639
4653
|
|
|
@@ -4085,6 +5099,67 @@ function share() {
|
|
|
4085
5099
|
console.log();
|
|
4086
5100
|
}
|
|
4087
5101
|
|
|
5102
|
+
// Shareable, screenshot-worthy safety posture card. The reach mechanic that
|
|
5103
|
+
// works in this niche is a compact artifact users *want* to share (the ccusage
|
|
5104
|
+
// cost scorecard, Karpathy's CLAUDE.md), not launch announcements. The score is
|
|
5105
|
+
// honest — it reports real installed-hook coverage (X of N core hooks), never an
|
|
5106
|
+
// invented points total — so it stays credible when screenshotted.
|
|
5107
|
+
function scorecard() {
|
|
5108
|
+
const PRIORITY = ['destructive-guard', 'secret-guard', 'branch-guard', 'syntax-check', 'context-monitor', 'api-error-alert', 'comment-strip', 'cd-git-allow'];
|
|
5109
|
+
const LABELS = {
|
|
5110
|
+
'destructive-guard': 'Destructive commands (rm -rf, force-push)',
|
|
5111
|
+
'secret-guard': 'Secret leaks (git add .env, key commits)',
|
|
5112
|
+
'branch-guard': 'Direct pushes to main / master',
|
|
5113
|
+
'syntax-check': 'Syntax errors after every edit',
|
|
5114
|
+
'context-monitor': 'Silent context loss past tool call 150',
|
|
5115
|
+
'api-error-alert': 'Silent autonomous-session death (rate limits)',
|
|
5116
|
+
'comment-strip': 'Allowlist bypass via bash comments',
|
|
5117
|
+
'cd-git-allow': 'Permission-prompt spam on read-only git',
|
|
5118
|
+
};
|
|
5119
|
+
|
|
5120
|
+
const active = Object.keys(HOOKS).filter(id => existsSync(join(HOOKS_DIR, id + '.sh')));
|
|
5121
|
+
const total = Object.keys(HOOKS).length;
|
|
5122
|
+
const n = active.length;
|
|
5123
|
+
|
|
5124
|
+
if (JSON_OUTPUT) {
|
|
5125
|
+
console.log(JSON.stringify({ tool: 'cc-safe-setup', coreHooksActive: n, coreHooksTotal: total, active }, null, 2));
|
|
5126
|
+
return;
|
|
5127
|
+
}
|
|
5128
|
+
|
|
5129
|
+
const filled = Math.round((n / total) * 12);
|
|
5130
|
+
const bar = '█'.repeat(filled) + '░'.repeat(12 - filled);
|
|
5131
|
+
const INNER = 52;
|
|
5132
|
+
const visLen = (s) => s.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
5133
|
+
const line = (s = '') => console.log(' ' + c.dim + '│' + c.reset + ' ' + s + ' '.repeat(Math.max(0, INNER - visLen(s))) + c.dim + '│' + c.reset);
|
|
5134
|
+
const rule = (l, r) => console.log(' ' + c.dim + l + '─'.repeat(INNER + 2) + r + c.reset);
|
|
5135
|
+
|
|
5136
|
+
console.log();
|
|
5137
|
+
rule('┌', '┐');
|
|
5138
|
+
line(c.bold + 'cc-safe-setup · Safety Scorecard' + c.reset);
|
|
5139
|
+
line();
|
|
5140
|
+
line('Coverage ' + c.green + bar + c.reset + ' ' + c.bold + n + '/' + total + c.reset + ' core hooks active');
|
|
5141
|
+
line();
|
|
5142
|
+
if (n === 0) {
|
|
5143
|
+
line(c.yellow + 'No core hooks installed yet.' + c.reset);
|
|
5144
|
+
line('Run ' + c.bold + 'npx github:yurukusa/cc-safe-setup' + c.reset + ' to install in ~10s.');
|
|
5145
|
+
} else {
|
|
5146
|
+
line('Now blocking:');
|
|
5147
|
+
const shown = PRIORITY.filter(id => active.includes(id)).slice(0, 4);
|
|
5148
|
+
for (const id of shown) line(' ' + c.green + '✓' + c.reset + ' ' + LABELS[id]);
|
|
5149
|
+
if (n > shown.length) line(' ' + c.dim + '… and ' + (n - shown.length) + ' more' + c.reset);
|
|
5150
|
+
}
|
|
5151
|
+
line();
|
|
5152
|
+
line(c.dim + 'Part of a 746-hook library mapped to 73+' + c.reset);
|
|
5153
|
+
line(c.dim + 'documented Claude Code incidents.' + c.reset);
|
|
5154
|
+
line();
|
|
5155
|
+
line(c.blue + 'npx github:yurukusa/cc-safe-setup' + c.reset + c.dim + ' MIT · free' + c.reset);
|
|
5156
|
+
rule('└', '┘');
|
|
5157
|
+
console.log();
|
|
5158
|
+
console.log(' ' + c.dim + 'Screenshot it · share your score · github.com/yurukusa/cc-safe-setup' + c.reset);
|
|
5159
|
+
console.log(' ' + c.dim + 'Prefer a clickable card? ' + c.reset + c.blue + 'yurukusa.github.io/cc-safe-setup/safety-scorecard.html' + c.reset);
|
|
5160
|
+
console.log();
|
|
5161
|
+
}
|
|
5162
|
+
|
|
4088
5163
|
function diff(otherFile) {
|
|
4089
5164
|
console.log();
|
|
4090
5165
|
console.log(c.bold + ' cc-safe-setup --diff' + c.reset);
|
|
@@ -4528,10 +5603,7 @@ exit 0`,
|
|
|
4528
5603
|
console.log(c.dim + ' Trigger: ' + matched.trigger + ', Matcher: ' + matched.matcher + c.reset);
|
|
4529
5604
|
|
|
4530
5605
|
// Register in settings.json
|
|
4531
|
-
let settings =
|
|
4532
|
-
if (existsSync(SETTINGS_PATH)) {
|
|
4533
|
-
try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
|
|
4534
|
-
}
|
|
5606
|
+
let settings = readSettingsForWrite();
|
|
4535
5607
|
if (!settings.hooks) settings.hooks = {};
|
|
4536
5608
|
if (!settings.hooks[matched.trigger]) settings.hooks[matched.trigger] = [];
|
|
4537
5609
|
|
|
@@ -4695,7 +5767,7 @@ async function exportConfig() {
|
|
|
4695
5767
|
console.log();
|
|
4696
5768
|
|
|
4697
5769
|
if (!existsSync(SETTINGS_PATH)) {
|
|
4698
|
-
console.log(c.red + ' No settings.json found. Run npx cc-safe-setup first.' + c.reset);
|
|
5770
|
+
console.log(c.red + ' No settings.json found. Run npx github:yurukusa/cc-safe-setup first.' + c.reset);
|
|
4699
5771
|
process.exit(1);
|
|
4700
5772
|
}
|
|
4701
5773
|
|
|
@@ -4739,7 +5811,7 @@ async function exportConfig() {
|
|
|
4739
5811
|
console.log(c.dim + ' Contains: ' + Object.keys(exportData.hooks).length + ' trigger types, ' + scriptPaths.size + ' hook scripts' + c.reset);
|
|
4740
5812
|
console.log();
|
|
4741
5813
|
console.log(c.dim + ' Share this file with your team. They can import with:' + c.reset);
|
|
4742
|
-
console.log(c.bold + ' npx cc-safe-setup --import ' + outputFile + c.reset);
|
|
5814
|
+
console.log(c.bold + ' npx github:yurukusa/cc-safe-setup --import ' + outputFile + c.reset);
|
|
4743
5815
|
console.log();
|
|
4744
5816
|
}
|
|
4745
5817
|
|
|
@@ -5050,7 +6122,7 @@ async function compileRules(rulesFile) {
|
|
|
5050
6122
|
const exampleDir = rulesFile.includes('/') ? rulesFile.substring(0, rulesFile.lastIndexOf('/')) : '.';
|
|
5051
6123
|
mkdirSync(exampleDir, { recursive: true });
|
|
5052
6124
|
writeFileSync(rulesFile, `# Claude Code Safety Rules
|
|
5053
|
-
# Run: npx cc-safe-setup --rules ${rulesFile}
|
|
6125
|
+
# Run: npx github:yurukusa/cc-safe-setup --rules ${rulesFile}
|
|
5054
6126
|
|
|
5055
6127
|
# Block dangerous commands
|
|
5056
6128
|
- block: "rm -rf on root or home"
|
|
@@ -5119,7 +6191,7 @@ async function compileRules(rulesFile) {
|
|
|
5119
6191
|
# Auto-generated from: ${rulesFile}
|
|
5120
6192
|
# Generated: $(date -Iseconds)
|
|
5121
6193
|
# Rules: ${rules.length}
|
|
5122
|
-
# Regenerate: npx cc-safe-setup --rules ${rulesFile}
|
|
6194
|
+
# Regenerate: npx github:yurukusa/cc-safe-setup --rules ${rulesFile}
|
|
5123
6195
|
|
|
5124
6196
|
INPUT=$(cat)
|
|
5125
6197
|
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
@@ -5207,10 +6279,7 @@ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
|
5207
6279
|
}
|
|
5208
6280
|
|
|
5209
6281
|
// Register in settings.json
|
|
5210
|
-
let settings =
|
|
5211
|
-
if (existsSync(SETTINGS_PATH)) {
|
|
5212
|
-
try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
|
|
5213
|
-
}
|
|
6282
|
+
let settings = readSettingsForWrite();
|
|
5214
6283
|
if (!settings.hooks) settings.hooks = {};
|
|
5215
6284
|
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
5216
6285
|
|
|
@@ -5270,7 +6339,7 @@ async function protect(targetPath) {
|
|
|
5270
6339
|
const pattern = isDir ? `*${normalized}*` : normalized;
|
|
5271
6340
|
|
|
5272
6341
|
const script = `#!/bin/bash
|
|
5273
|
-
# Auto-generated by: npx cc-safe-setup --protect ${targetPath}
|
|
6342
|
+
# Auto-generated by: npx github:yurukusa/cc-safe-setup --protect ${targetPath}
|
|
5274
6343
|
# Blocks Edit/Write to: ${normalized}
|
|
5275
6344
|
INPUT=$(cat)
|
|
5276
6345
|
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
@@ -5328,41 +6397,25 @@ exit 0
|
|
|
5328
6397
|
|
|
5329
6398
|
// Register in settings.json — use "Bash" matcher ONLY (never "")
|
|
5330
6399
|
// "" matcher affects ALL tools and a broken hook would lock out the session
|
|
5331
|
-
let settings =
|
|
5332
|
-
if (existsSync(SETTINGS_PATH)) {
|
|
5333
|
-
try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
|
|
5334
|
-
}
|
|
6400
|
+
let settings = readSettingsForWrite();
|
|
5335
6401
|
if (!settings.hooks) settings.hooks = {};
|
|
5336
6402
|
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
5337
6403
|
|
|
5338
|
-
// Register under specific
|
|
6404
|
+
// Register under a specific matcher (NEVER use "" matcher).
|
|
6405
|
+
// --protect installs exactly one rule: block Edit/Write on `targetPath`, so the
|
|
6406
|
+
// only matcher it needs is Edit|Write. The block/approve branches that used to
|
|
6407
|
+
// stand here were copied from guard(), which parses a user-supplied rule list;
|
|
6408
|
+
// `rules` never existed in this function. The result was a ReferenceError right
|
|
6409
|
+
// after the hook file had been written — the hook was left on disk and never
|
|
6410
|
+
// registered, and --protect failed every single time it was run.
|
|
5339
6411
|
const hookCmd = `bash ${toBashPath(hookPath)}`;
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
// Block and approve rules need Bash matcher
|
|
5345
|
-
if (hasBlocks || hasApproves) {
|
|
5346
|
-
let bashMatcher = settings.hooks.PreToolUse.find(e => e.matcher === 'Bash');
|
|
5347
|
-
if (!bashMatcher) {
|
|
5348
|
-
bashMatcher = { matcher: 'Bash', hooks: [] };
|
|
5349
|
-
settings.hooks.PreToolUse.push(bashMatcher);
|
|
5350
|
-
}
|
|
5351
|
-
if (!bashMatcher.hooks.some(h => h.command === hookCmd)) {
|
|
5352
|
-
bashMatcher.hooks.push({ type: 'command', command: hookCmd });
|
|
5353
|
-
}
|
|
6412
|
+
let editMatcher = settings.hooks.PreToolUse.find(e => e.matcher === 'Edit|Write');
|
|
6413
|
+
if (!editMatcher) {
|
|
6414
|
+
editMatcher = { matcher: 'Edit|Write', hooks: [] };
|
|
6415
|
+
settings.hooks.PreToolUse.push(editMatcher);
|
|
5354
6416
|
}
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
if (hasProtects) {
|
|
5358
|
-
let editMatcher = settings.hooks.PreToolUse.find(e => e.matcher === 'Edit|Write');
|
|
5359
|
-
if (!editMatcher) {
|
|
5360
|
-
editMatcher = { matcher: 'Edit|Write', hooks: [] };
|
|
5361
|
-
settings.hooks.PreToolUse.push(editMatcher);
|
|
5362
|
-
}
|
|
5363
|
-
if (!editMatcher.hooks.some(h => h.command === hookCmd)) {
|
|
5364
|
-
editMatcher.hooks.push({ type: 'command', command: hookCmd });
|
|
5365
|
-
}
|
|
6417
|
+
if (!editMatcher.hooks.some(h => h.command === hookCmd)) {
|
|
6418
|
+
editMatcher.hooks.push({ type: 'command', command: hookCmd });
|
|
5366
6419
|
}
|
|
5367
6420
|
|
|
5368
6421
|
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
@@ -5530,6 +6583,35 @@ async function doctor() {
|
|
|
5530
6583
|
const fail = (msg) => { console.log(c.red + ' ✗ ' + c.reset + msg); issues++; };
|
|
5531
6584
|
const warn = (msg) => { console.log(c.yellow + ' ! ' + c.reset + msg); warnings++; };
|
|
5532
6585
|
|
|
6586
|
+
// 0. Environment variables that stop hooks loading at all.
|
|
6587
|
+
//
|
|
6588
|
+
// This runs before everything else because it is the one state where every check
|
|
6589
|
+
// below can pass and still nothing runs — which is precisely the false reassurance
|
|
6590
|
+
// this command exists to prevent.
|
|
6591
|
+
//
|
|
6592
|
+
// Measured 2026-09-19 on Claude Code 2.1.278 (Linux/WSL2) with a control: with
|
|
6593
|
+
// CLAUDE_CODE_RESTRICTED=1 or CLAUDE_CODE_SAFE_MODE=1 set, a UserPromptSubmit hook
|
|
6594
|
+
// installed in the user, project and local files did not fire once, while the same
|
|
6595
|
+
// hook fired in all three scopes without them. Only the value "1" was measured; any
|
|
6596
|
+
// other non-empty, non-false value is flagged on the assumption that someone who
|
|
6597
|
+
// typed it meant to turn it on.
|
|
6598
|
+
const envOn = (v) => typeof v === 'string' && v !== '' && v !== '0' && v.toLowerCase() !== 'false';
|
|
6599
|
+
const safeModeEnv = process.env.CLAUDE_CODE_SAFE_MODE;
|
|
6600
|
+
const restrictedEnv = process.env.CLAUDE_CODE_RESTRICTED;
|
|
6601
|
+
|
|
6602
|
+
if (envOn(safeModeEnv)) {
|
|
6603
|
+
fail('CLAUDE_CODE_SAFE_MODE=' + safeModeEnv + ' — Claude Code loads none of your hooks');
|
|
6604
|
+
console.log(c.dim + ' Bash and the other tools keep working normally, so nothing looks wrong.' + c.reset);
|
|
6605
|
+
console.log(c.dim + ' --settings does NOT restore hooks in this mode.' + c.reset);
|
|
6606
|
+
console.log(c.dim + ' Fix: unset CLAUDE_CODE_SAFE_MODE (check your shell profile, it is read once)' + c.reset);
|
|
6607
|
+
} else if (envOn(restrictedEnv)) {
|
|
6608
|
+
fail('CLAUDE_CODE_RESTRICTED=' + restrictedEnv + ' — user, project and local hooks are not loaded');
|
|
6609
|
+
console.log(c.dim + ' Bash is removed too, so Bash-matched guards could not fire regardless.' + c.reset);
|
|
6610
|
+
console.log(c.dim + ' Fix: unset CLAUDE_CODE_RESTRICTED, or pass your hooks with --settings' + c.reset);
|
|
6611
|
+
} else {
|
|
6612
|
+
pass('no hook-disabling environment variable set');
|
|
6613
|
+
}
|
|
6614
|
+
|
|
5533
6615
|
// 1. Check jq
|
|
5534
6616
|
try {
|
|
5535
6617
|
execSync('which jq', { stdio: 'pipe' });
|
|
@@ -5543,7 +6625,7 @@ async function doctor() {
|
|
|
5543
6625
|
// 2. Check settings.json exists
|
|
5544
6626
|
if (!existsSync(SETTINGS_PATH)) {
|
|
5545
6627
|
fail('~/.claude/settings.json does not exist');
|
|
5546
|
-
console.log(c.dim + ' Fix: npx cc-safe-setup' + c.reset);
|
|
6628
|
+
console.log(c.dim + ' Fix: npx github:yurukusa/cc-safe-setup' + c.reset);
|
|
5547
6629
|
} else {
|
|
5548
6630
|
pass('settings.json exists');
|
|
5549
6631
|
|
|
@@ -5554,7 +6636,7 @@ async function doctor() {
|
|
|
5554
6636
|
pass('settings.json is valid JSON');
|
|
5555
6637
|
} catch (e) {
|
|
5556
6638
|
fail('settings.json has invalid JSON: ' + e.message);
|
|
5557
|
-
console.log(c.dim + ' Fix: npx cc-safe-setup --uninstall && npx cc-safe-setup' + c.reset);
|
|
6639
|
+
console.log(c.dim + ' Fix: npx github:yurukusa/cc-safe-setup --uninstall && npx github:yurukusa/cc-safe-setup' + c.reset);
|
|
5558
6640
|
}
|
|
5559
6641
|
|
|
5560
6642
|
if (settings) {
|
|
@@ -5582,7 +6664,7 @@ async function doctor() {
|
|
|
5582
6664
|
if (cmd.includes('\\')) {
|
|
5583
6665
|
fail('Windows backslash in hook command: ' + cmd);
|
|
5584
6666
|
const fixed = cmd.replace(/\\/g, '/');
|
|
5585
|
-
console.log(c.dim + ' Fix: npx cc-safe-setup --uninstall && npx cc-safe-setup
|
|
6667
|
+
console.log(c.dim + ' Fix: npx github:yurukusa/cc-safe-setup --uninstall && npx github:yurukusa/cc-safe-setup' + c.reset);
|
|
5586
6668
|
console.log(c.dim + ' Or manually change to: ' + fixed + c.reset);
|
|
5587
6669
|
continue;
|
|
5588
6670
|
}
|
|
@@ -5657,13 +6739,213 @@ async function doctor() {
|
|
|
5657
6739
|
// 12. Check hooks directory
|
|
5658
6740
|
if (!existsSync(HOOKS_DIR)) {
|
|
5659
6741
|
fail('~/.claude/hooks/ directory does not exist');
|
|
5660
|
-
console.log(c.dim + ' Fix: npx cc-safe-setup' + c.reset);
|
|
6742
|
+
console.log(c.dim + ' Fix: npx github:yurukusa/cc-safe-setup' + c.reset);
|
|
5661
6743
|
} else {
|
|
5662
6744
|
const files = readdirSync(HOOKS_DIR).filter(f => f.endsWith('.sh'));
|
|
5663
6745
|
pass('hooks directory exists (' + files.length + ' scripts)');
|
|
5664
6746
|
}
|
|
5665
6747
|
|
|
5666
|
-
// 13. Check
|
|
6748
|
+
// 13. Check that each installed hook sits on the event it declares.
|
|
6749
|
+
// A hook wired to the wrong event installs cleanly, reports success, and then
|
|
6750
|
+
// never fires — the worst failure this project has, because the operator
|
|
6751
|
+
// believes they are covered. Two ways it happens: the header form that older
|
|
6752
|
+
// installs did not parse (fixed in installExample, but settings.json written
|
|
6753
|
+
// before that keeps the wrong entry), and hand-edited settings. Read-only:
|
|
6754
|
+
// report the mismatch and how to correct it, never rewrite settings here.
|
|
6755
|
+
try {
|
|
6756
|
+
const declaredFor = (scriptPath) => {
|
|
6757
|
+
if (!existsSync(scriptPath)) return null;
|
|
6758
|
+
const body = readFileSync(scriptPath, 'utf8');
|
|
6759
|
+
const t = body.match(/^#\s*[Tt][Rr][Ii][Gg][Gg][Ee][Rr]:\s*(\S+)/m);
|
|
6760
|
+
if (!t) return null;
|
|
6761
|
+
const mRaw =
|
|
6762
|
+
body.match(/^#\s*[Mm][Aa][Tt][Cc][Hh][Ee][Rr]:\s*(.+)$/m) ||
|
|
6763
|
+
body.match(/^#\s*[Tt][Rr][Ii][Gg][Gg][Ee][Rr]:\s*\S+\s+[Mm][Aa][Tt][Cc][Hh][Ee][Rr]:\s*(.+)$/m);
|
|
6764
|
+
let matcher = null;
|
|
6765
|
+
if (mRaw) {
|
|
6766
|
+
const q = mRaw[1].match(/^"([^"]*)"/);
|
|
6767
|
+
matcher = q ? q[1] : mRaw[1].trim();
|
|
6768
|
+
}
|
|
6769
|
+
// 宣言と登録先が一致していても、本体が別のイベント名で決定を返していれば
|
|
6770
|
+
// その決定は宛先の違う封筒になり、静かに効かない。ここまで読んで初めて
|
|
6771
|
+
// 「宣言どおりの場所に居る」が「宣言どおりに効く」になる。
|
|
6772
|
+
// 終了コードだけで判断するフックは封筒を出さないので、その場合は null。
|
|
6773
|
+
const envs = [...new Set([...body.matchAll(/"hookEventName"\s*:\s*"([A-Za-z]+)"/g)].map(m => m[1]))];
|
|
6774
|
+
return { trigger: t[1], matcher, emitted: envs.length ? envs : null };
|
|
6775
|
+
};
|
|
6776
|
+
|
|
6777
|
+
const settings = existsSync(SETTINGS_PATH)
|
|
6778
|
+
? JSON.parse(readFileSync(SETTINGS_PATH, 'utf8'))
|
|
6779
|
+
: {};
|
|
6780
|
+
const mismatched = [];
|
|
6781
|
+
let inspected = 0;
|
|
6782
|
+
for (const [event, entries] of Object.entries(settings.hooks || {})) {
|
|
6783
|
+
for (const entry of (entries || [])) {
|
|
6784
|
+
const registered = entry.matcher;
|
|
6785
|
+
for (const h of (entry.hooks || [])) {
|
|
6786
|
+
const hit = String(h.command || '').match(/([^/\\ "']+\.sh)/);
|
|
6787
|
+
if (!hit) continue;
|
|
6788
|
+
const decl = declaredFor(join(HOOKS_DIR, hit[1]));
|
|
6789
|
+
if (!decl) continue;
|
|
6790
|
+
inspected++;
|
|
6791
|
+
// "Edit|Write" and "Write|Edit" select the same tools. Comparing the
|
|
6792
|
+
// raw strings would report a difference that changes nothing, and a
|
|
6793
|
+
// diagnostic that cries wolf stops being read.
|
|
6794
|
+
const asSet = (v) => (v == null ? null :
|
|
6795
|
+
String(v).split('|').map(s => s.trim()).filter(Boolean).sort().join('|'));
|
|
6796
|
+
const wrongEvent = decl.trigger !== event;
|
|
6797
|
+
const wrongMatcher = decl.matcher !== null &&
|
|
6798
|
+
asSet(decl.matcher) !== asSet(registered ?? '');
|
|
6799
|
+
// 2026-09-03 に足した3本目。prefer-builtin-tools.sh は
|
|
6800
|
+
// ヘッダも登録先も PermissionRequest で一致していたのに、本体は
|
|
6801
|
+
// PreToolUse の封筒を返しており、隔離して測ると3条件とも一度も呼ばれなかった。
|
|
6802
|
+
// この検査は当時「every installed hook sits on the event it declares」と
|
|
6803
|
+
// 緑の印を出していた。一致していたのは2つだけで、3つ目を見ていなかった。
|
|
6804
|
+
const wrongEnvelope = Array.isArray(decl.emitted) && !decl.emitted.includes(event);
|
|
6805
|
+
if (wrongEvent || wrongMatcher || wrongEnvelope) {
|
|
6806
|
+
mismatched.push({ name: hit[1], event, registered, decl, wrongEnvelope });
|
|
6807
|
+
}
|
|
6808
|
+
}
|
|
6809
|
+
}
|
|
6810
|
+
}
|
|
6811
|
+
if (inspected === 0) {
|
|
6812
|
+
pass('no installed hook declares an event to verify');
|
|
6813
|
+
} else if (mismatched.length === 0) {
|
|
6814
|
+
pass('every installed hook sits on the event it declares, and answers in that event\'s envelope (' + inspected + ' checked)');
|
|
6815
|
+
} else {
|
|
6816
|
+
fail(mismatched.length + ' of ' + inspected + ' installed hooks are wired to the wrong event or tool — they run at the wrong time, or never');
|
|
6817
|
+
for (const m of mismatched) {
|
|
6818
|
+
console.log(c.dim + ' ' + m.name + ': registered ' + m.event +
|
|
6819
|
+
' matcher ' + JSON.stringify(m.registered ?? null) +
|
|
6820
|
+
', declares ' + m.decl.trigger + ' matcher ' + JSON.stringify(m.decl.matcher) + c.reset);
|
|
6821
|
+
if (m.wrongEnvelope) {
|
|
6822
|
+
console.log(c.dim + ' and it answers with hookEventName ' + JSON.stringify(m.decl.emitted) +
|
|
6823
|
+
', not ' + JSON.stringify(m.event) + ' — the decision is addressed to a different event,' +
|
|
6824
|
+
' so it is ignored even when the hook runs.' + c.reset);
|
|
6825
|
+
}
|
|
6826
|
+
}
|
|
6827
|
+
console.log(c.dim + ' Fix: npx github:yurukusa/cc-safe-setup --install-example <name> rewrites the entry from the header,' + c.reset);
|
|
6828
|
+
console.log(c.dim + ' or correct the event and matcher in ~/.claude/settings.json by hand.' + c.reset);
|
|
6829
|
+
}
|
|
6830
|
+
} catch (e) {
|
|
6831
|
+
warn('could not verify hook events against their headers (' + e.message + ')');
|
|
6832
|
+
}
|
|
6833
|
+
|
|
6834
|
+
// 15. Check for near-miss (misspelled) keys in settings.json.
|
|
6835
|
+
//
|
|
6836
|
+
// Claude Code ignores unknown settings keys in silence: no warning, no error,
|
|
6837
|
+
// exit code 0. Measured on v2.1.220 with isolated --settings files:
|
|
6838
|
+
// permissions.denyy -> the denied command ran, exit 0, zero warnings
|
|
6839
|
+
// hooks.PreToolUsee -> the hook was never invoked, zero warnings
|
|
6840
|
+
// sandbox.failIfUnavailble -> started unprotected, exit 0
|
|
6841
|
+
// The hook body was unchanged in the second case; only the event key differed.
|
|
6842
|
+
// `claude doctor` does not check spelling, and there is no non-interactive
|
|
6843
|
+
// command that prints the effective settings. So a control can be void from
|
|
6844
|
+
// the day it was written and look exactly like a control that works: both
|
|
6845
|
+
// produce "nothing bad happened".
|
|
6846
|
+
//
|
|
6847
|
+
// Design: only report keys that are ONE or TWO edits away from a known key.
|
|
6848
|
+
// An unknown key that is far from every known one is probably a setting this
|
|
6849
|
+
// release of cc-safe-setup has not heard of yet, and shouting about it would
|
|
6850
|
+
// train the operator to ignore this diagnostic. Read-only: never rewrite.
|
|
6851
|
+
const KNOWN_TOP = [
|
|
6852
|
+
'permissions', 'hooks', 'env', 'model', 'defaultMode', 'sandbox', 'statusLine',
|
|
6853
|
+
'apiKeyHelper', 'includeCoAuthoredBy', 'cleanupPeriodDays', 'forceLoginMethod',
|
|
6854
|
+
'outputStyle', 'autoUpdates', 'alwaysThinkingEnabled', 'spinnerTipsEnabled',
|
|
6855
|
+
'enableAllProjectMcpServers', 'enabledMcpjsonServers', 'disabledMcpjsonServers',
|
|
6856
|
+
'awsAuthRefresh', 'awsCredentialExport', 'otelHeadersHelper',
|
|
6857
|
+
];
|
|
6858
|
+
const KNOWN_EVENTS = [
|
|
6859
|
+
'PreToolUse', 'PostToolUse', 'Notification', 'Stop', 'SubagentStop',
|
|
6860
|
+
'SubagentStart', 'UserPromptSubmit', 'PreCompact', 'PostCompact',
|
|
6861
|
+
'SessionStart', 'SessionEnd', 'DirectoryAdded',
|
|
6862
|
+
];
|
|
6863
|
+
const KNOWN_PERMISSION_KEYS = ['allow', 'deny', 'ask', 'additionalDirectories', 'defaultMode'];
|
|
6864
|
+
// sandbox sub-keys. `failIfUnavailable` is the one that decides whether a
|
|
6865
|
+
// missing dependency stops the session or silently drops the sandbox, so a
|
|
6866
|
+
// typo here is the difference between "refused to start" and "ran unprotected".
|
|
6867
|
+
const KNOWN_SANDBOX_KEYS = [
|
|
6868
|
+
'enabled', 'failIfUnavailable', 'network', 'allowedDomains', 'strictAllowlist',
|
|
6869
|
+
'autoAllowBashIfSandboxed', 'allowManagedDomainsOnly', 'allowUnixSockets',
|
|
6870
|
+
'allowLocalBinding', 'excludedPaths',
|
|
6871
|
+
];
|
|
6872
|
+
|
|
6873
|
+
const editDistance = (a, b) => {
|
|
6874
|
+
const m = a.length, n = b.length;
|
|
6875
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
6876
|
+
for (let i = 1; i <= m; i++) {
|
|
6877
|
+
const cur = [i];
|
|
6878
|
+
for (let j = 1; j <= n; j++) {
|
|
6879
|
+
cur[j] = Math.min(
|
|
6880
|
+
prev[j] + 1,
|
|
6881
|
+
cur[j - 1] + 1,
|
|
6882
|
+
prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
6883
|
+
);
|
|
6884
|
+
}
|
|
6885
|
+
prev = cur;
|
|
6886
|
+
}
|
|
6887
|
+
return prev[n];
|
|
6888
|
+
};
|
|
6889
|
+
|
|
6890
|
+
// Near-miss only. Requires the key to be long enough that a 1-edit match is
|
|
6891
|
+
// meaningful: on a 3-letter key, distance 1 hits unrelated words constantly.
|
|
6892
|
+
const nearMiss = (key, known) => {
|
|
6893
|
+
if (known.includes(key)) return null;
|
|
6894
|
+
let best = null;
|
|
6895
|
+
for (const k of known) {
|
|
6896
|
+
if (Math.abs(k.length - key.length) > 2) continue;
|
|
6897
|
+
const d = editDistance(key.toLowerCase(), k.toLowerCase());
|
|
6898
|
+
const limit = k.length >= 8 ? 2 : 1;
|
|
6899
|
+
if (d > 0 && d <= limit && (best === null || d < best.d)) best = { k, d };
|
|
6900
|
+
}
|
|
6901
|
+
return best;
|
|
6902
|
+
};
|
|
6903
|
+
|
|
6904
|
+
try {
|
|
6905
|
+
const settings = existsSync(SETTINGS_PATH)
|
|
6906
|
+
? JSON.parse(readFileSync(SETTINGS_PATH, 'utf8'))
|
|
6907
|
+
: null;
|
|
6908
|
+
if (settings === null) {
|
|
6909
|
+
// Check 2 already reported the missing file; do not repeat it here.
|
|
6910
|
+
} else {
|
|
6911
|
+
const typos = [];
|
|
6912
|
+
for (const key of Object.keys(settings)) {
|
|
6913
|
+
const hit = nearMiss(key, KNOWN_TOP);
|
|
6914
|
+
if (hit) typos.push({ where: '', key, suggest: hit.k });
|
|
6915
|
+
}
|
|
6916
|
+
for (const key of Object.keys(settings.permissions || {})) {
|
|
6917
|
+
const hit = nearMiss(key, KNOWN_PERMISSION_KEYS);
|
|
6918
|
+
if (hit) typos.push({ where: 'permissions.', key, suggest: hit.k });
|
|
6919
|
+
}
|
|
6920
|
+
for (const key of Object.keys(settings.hooks || {})) {
|
|
6921
|
+
const hit = nearMiss(key, KNOWN_EVENTS);
|
|
6922
|
+
if (hit) typos.push({ where: 'hooks.', key, suggest: hit.k });
|
|
6923
|
+
}
|
|
6924
|
+
const sandboxRoot = settings.sandbox || {};
|
|
6925
|
+
for (const key of Object.keys(sandboxRoot)) {
|
|
6926
|
+
const hit = nearMiss(key, KNOWN_SANDBOX_KEYS);
|
|
6927
|
+
if (hit) typos.push({ where: 'sandbox.', key, suggest: hit.k });
|
|
6928
|
+
}
|
|
6929
|
+
for (const key of Object.keys(sandboxRoot.network || {})) {
|
|
6930
|
+
const hit = nearMiss(key, KNOWN_SANDBOX_KEYS);
|
|
6931
|
+
if (hit) typos.push({ where: 'sandbox.network.', key, suggest: hit.k });
|
|
6932
|
+
}
|
|
6933
|
+
if (typos.length === 0) {
|
|
6934
|
+
pass('no misspelled settings keys (checked top level, permissions, hook events, sandbox)');
|
|
6935
|
+
} else {
|
|
6936
|
+
fail(typos.length + ' settings key(s) look like typos — Claude Code ignores unknown keys in silence, so these controls are not in force');
|
|
6937
|
+
for (const t of typos) {
|
|
6938
|
+
console.log(c.dim + ' "' + t.where + t.key + '" — did you mean "' + t.where + t.suggest + '"?' + c.reset);
|
|
6939
|
+
}
|
|
6940
|
+
console.log(c.dim + ' Nothing warns you about this: a misspelled key and a working control both look like "nothing bad happened".' + c.reset);
|
|
6941
|
+
console.log(c.dim + ' After fixing, confirm by tripping the control on purpose and watching it stop.' + c.reset);
|
|
6942
|
+
}
|
|
6943
|
+
}
|
|
6944
|
+
} catch (e) {
|
|
6945
|
+
warn('could not check settings keys for typos (' + e.message + ')');
|
|
6946
|
+
}
|
|
6947
|
+
|
|
6948
|
+
// 14. Check Claude Code version (needs hooks support)
|
|
5667
6949
|
try {
|
|
5668
6950
|
const ver = execSync('claude --version 2>/dev/null || echo "not found"', { stdio: 'pipe' }).toString().trim();
|
|
5669
6951
|
if (ver === 'not found') {
|
|
@@ -5799,9 +7081,9 @@ function scan() {
|
|
|
5799
7081
|
|
|
5800
7082
|
// Display recommendations
|
|
5801
7083
|
console.log(c.bold + ' Recommended hooks:' + c.reset);
|
|
5802
|
-
console.log(' ' + c.dim + 'npx cc-safe-setup' + c.reset + ' (8 built-in hooks)');
|
|
7084
|
+
console.log(' ' + c.dim + 'npx github:yurukusa/cc-safe-setup' + c.reset + ' (8 built-in hooks)');
|
|
5803
7085
|
for (const ex of examples) {
|
|
5804
|
-
console.log(' ' + c.dim + 'npx cc-safe-setup --install-example ' + ex + c.reset);
|
|
7086
|
+
console.log(' ' + c.dim + 'npx github:yurukusa/cc-safe-setup --install-example ' + ex + c.reset);
|
|
5805
7087
|
}
|
|
5806
7088
|
|
|
5807
7089
|
// Generate CLAUDE.md
|
|
@@ -5825,13 +7107,167 @@ function scan() {
|
|
|
5825
7107
|
console.log(' ' + c.dim + rule + c.reset);
|
|
5826
7108
|
}
|
|
5827
7109
|
console.log();
|
|
5828
|
-
console.log(c.dim + ' Run with --apply to create: npx cc-safe-setup --scan --apply' + c.reset);
|
|
7110
|
+
console.log(c.dim + ' Run with --apply to create: npx github:yurukusa/cc-safe-setup --scan --apply' + c.reset);
|
|
5829
7111
|
}
|
|
5830
7112
|
}
|
|
5831
7113
|
|
|
5832
7114
|
console.log();
|
|
5833
7115
|
}
|
|
5834
7116
|
|
|
7117
|
+
// Installing a hook copies the file. Nothing ever copies it back, so a hook
|
|
7118
|
+
// installed in March keeps running its March logic forever — including bugs that
|
|
7119
|
+
// were fixed here months later. Measured on a real four-month-old install:
|
|
7120
|
+
// 27 of the 31 hooks that came from this project no longer matched the shipped
|
|
7121
|
+
// version, and the oldest were 4.5 months behind. Three of those were missing the
|
|
7122
|
+
// `jq` guard that was added after they were installed, which is the difference
|
|
7123
|
+
// between "blocks the dangerous command" and "silently approves it".
|
|
7124
|
+
//
|
|
7125
|
+
// This command only reports. It never overwrites, because a file that differs may
|
|
7126
|
+
// be the user's own edit, and replacing that silently would be worse than stale.
|
|
7127
|
+
// Prints the shipped body of a core guard to stdout and nothing else, so it can
|
|
7128
|
+
// be piped into diff. It never writes to the hooks directory: an installed copy
|
|
7129
|
+
// that differs may be the user's own edit, and replacing that silently would be
|
|
7130
|
+
// worse than leaving it stale.
|
|
7131
|
+
function showCore(id) {
|
|
7132
|
+
const key = id && id.replace(/\.sh$/, '');
|
|
7133
|
+
if (!key || !Object.prototype.hasOwnProperty.call(SCRIPTS, key)) {
|
|
7134
|
+
console.error('Unknown core guard: ' + (id || '(none given)'));
|
|
7135
|
+
console.error('Available: ' + Object.keys(SCRIPTS).sort().join(', '));
|
|
7136
|
+
process.exit(1);
|
|
7137
|
+
}
|
|
7138
|
+
process.stdout.write(SCRIPTS[key]);
|
|
7139
|
+
}
|
|
7140
|
+
|
|
7141
|
+
function outdated() {
|
|
7142
|
+
const examplesDir = join(__dirname, 'examples');
|
|
7143
|
+
console.log();
|
|
7144
|
+
console.log(c.bold + ' cc-safe-setup --outdated' + c.reset);
|
|
7145
|
+
console.log(c.dim + ' Comparing your installed hooks against the versions shipped today.' + c.reset);
|
|
7146
|
+
console.log();
|
|
7147
|
+
|
|
7148
|
+
if (!existsSync(HOOKS_DIR)) {
|
|
7149
|
+
console.log(' ' + c.dim + 'No hooks directory at ' + HOOKS_DIR + c.reset);
|
|
7150
|
+
console.log();
|
|
7151
|
+
return;
|
|
7152
|
+
}
|
|
7153
|
+
|
|
7154
|
+
const sha = (p) => createHash('sha256').update(readFileSync(p)).digest('hex');
|
|
7155
|
+
const shaText = (s) => createHash('sha256').update(Buffer.from(s)).digest('hex');
|
|
7156
|
+
const hasJqGuard = (s) => /command -v jq|which jq|hash jq/.test(s);
|
|
7157
|
+
const shipped = new Set(
|
|
7158
|
+
existsSync(examplesDir) ? readdirSync(examplesDir).filter((f) => f.endsWith('.sh')) : []
|
|
7159
|
+
);
|
|
7160
|
+
// The default install writes the core guards from scripts.json, NOT from
|
|
7161
|
+
// examples/. Comparing only against examples/ made this command structurally
|
|
7162
|
+
// blind to exactly the hooks it installs by default: every core guard landed in
|
|
7163
|
+
// `foreign` and was reported as "not shipped by this project — not checked".
|
|
7164
|
+
// Measured on 2026-08-12: three core guards on one machine were 2.5 months
|
|
7165
|
+
// behind, and the largest was 8,307 bytes installed against 32,857 shipped —
|
|
7166
|
+
// six dangerous command shapes that the shipped version blocks were passing.
|
|
7167
|
+
const core = new Map(Object.keys(SCRIPTS).map((id) => [id + '.sh', SCRIPTS[id]]));
|
|
7168
|
+
const installed = readdirSync(HOOKS_DIR).filter((f) => f.endsWith('.sh')).sort();
|
|
7169
|
+
|
|
7170
|
+
const same = [];
|
|
7171
|
+
const differs = [];
|
|
7172
|
+
const foreign = [];
|
|
7173
|
+
|
|
7174
|
+
for (const f of installed) {
|
|
7175
|
+
const isCore = core.has(f);
|
|
7176
|
+
if (!shipped.has(f) && !isCore) { foreign.push(f); continue; }
|
|
7177
|
+
const instPath = join(HOOKS_DIR, f);
|
|
7178
|
+
let instText, shipText, shipSha;
|
|
7179
|
+
try {
|
|
7180
|
+
instText = readFileSync(instPath, 'utf8');
|
|
7181
|
+
if (isCore) {
|
|
7182
|
+
shipText = core.get(f);
|
|
7183
|
+
shipSha = shaText(shipText);
|
|
7184
|
+
} else {
|
|
7185
|
+
shipText = readFileSync(join(examplesDir, f), 'utf8');
|
|
7186
|
+
shipSha = sha(join(examplesDir, f));
|
|
7187
|
+
}
|
|
7188
|
+
} catch {
|
|
7189
|
+
foreign.push(f);
|
|
7190
|
+
continue;
|
|
7191
|
+
}
|
|
7192
|
+
if (sha(instPath) === shipSha) { same.push(f); continue; }
|
|
7193
|
+
differs.push({
|
|
7194
|
+
name: f,
|
|
7195
|
+
core: isCore,
|
|
7196
|
+
// Only flag this when the installed copy actually parses with jq. A hook
|
|
7197
|
+
// that never touches jq is not made unsafe by lacking the guard.
|
|
7198
|
+
missingJqGuard: /\bjq\b/.test(instText) && hasJqGuard(shipText) && !hasJqGuard(instText),
|
|
7199
|
+
});
|
|
7200
|
+
}
|
|
7201
|
+
// Core guards first: they are installed by default, so a stale one is the
|
|
7202
|
+
// difference between "blocks the dangerous command" and "silently approves it".
|
|
7203
|
+
differs.sort((a, b) => (b.core === true) - (a.core === true));
|
|
7204
|
+
|
|
7205
|
+
if (installed.length === 0) {
|
|
7206
|
+
console.log(' ' + c.dim + 'No .sh hooks installed.' + c.reset);
|
|
7207
|
+
console.log();
|
|
7208
|
+
return;
|
|
7209
|
+
}
|
|
7210
|
+
|
|
7211
|
+
if (differs.length === 0) {
|
|
7212
|
+
console.log(' ' + c.green + '✓' + c.reset + ' All ' + same.length +
|
|
7213
|
+
' hook(s) from this project match the shipped version.');
|
|
7214
|
+
} else {
|
|
7215
|
+
console.log(' ' + c.yellow + '!' + c.reset + ' ' + differs.length +
|
|
7216
|
+
' of ' + (same.length + differs.length) +
|
|
7217
|
+
' hook(s) from this project differ from what ships today.');
|
|
7218
|
+
console.log();
|
|
7219
|
+
for (const d of differs) {
|
|
7220
|
+
const flag = d.missingJqGuard
|
|
7221
|
+
? ' ' + c.red + '← reads input with jq but has no jq guard' + c.reset
|
|
7222
|
+
: '';
|
|
7223
|
+
const tag = d.core ? ' ' + c.red + '[core guard]' + c.reset : '';
|
|
7224
|
+
console.log(' ' + c.yellow + '•' + c.reset + ' ' + d.name + tag + flag);
|
|
7225
|
+
}
|
|
7226
|
+
console.log();
|
|
7227
|
+
console.log(' ' + c.dim + 'A difference means one of two things, and this command cannot tell them apart:' + c.reset);
|
|
7228
|
+
console.log(' ' + c.dim + ' 1. the shipped hook was fixed after you installed it, or' + c.reset);
|
|
7229
|
+
console.log(' ' + c.dim + ' 2. you edited your copy on purpose.' + c.reset);
|
|
7230
|
+
const coreDiffs = differs.filter((d) => d.core);
|
|
7231
|
+
if (coreDiffs.length > 0) {
|
|
7232
|
+
console.log();
|
|
7233
|
+
console.log(' ' + c.red + coreDiffs.length + ' of these are core guards' + c.reset +
|
|
7234
|
+
c.dim + ' — installed by default, so a stale one is the difference between' + c.reset);
|
|
7235
|
+
console.log(' ' + c.dim + 'blocking a dangerous command and silently approving it.' + c.reset);
|
|
7236
|
+
}
|
|
7237
|
+
const ex = differs.find((d) => !d.core);
|
|
7238
|
+
if (ex) {
|
|
7239
|
+
console.log();
|
|
7240
|
+
console.log(' ' + c.dim + 'Look at the difference before replacing anything:' + c.reset);
|
|
7241
|
+
console.log(' ' + c.dim + ' diff ' + join(HOOKS_DIR, ex.name) +
|
|
7242
|
+
' ' + join(examplesDir, ex.name) + c.reset);
|
|
7243
|
+
console.log(' ' + c.dim + 'Take the shipped version of one hook:' + c.reset);
|
|
7244
|
+
console.log(' ' + c.dim + ' npx github:yurukusa/cc-safe-setup --install-example ' +
|
|
7245
|
+
ex.name.replace(/\.sh$/, '') + c.reset);
|
|
7246
|
+
}
|
|
7247
|
+
if (coreDiffs.length > 0) {
|
|
7248
|
+
console.log();
|
|
7249
|
+
console.log(' ' + c.dim + 'Core guards do not live in examples/, so --install-example cannot fetch' + c.reset);
|
|
7250
|
+
console.log(' ' + c.dim + 'them. Print the shipped body and compare it yourself:' + c.reset);
|
|
7251
|
+
console.log(' ' + c.dim + ' npx github:yurukusa/cc-safe-setup --show-core ' +
|
|
7252
|
+
coreDiffs[0].name.replace(/\.sh$/, '') + c.reset);
|
|
7253
|
+
}
|
|
7254
|
+
}
|
|
7255
|
+
|
|
7256
|
+
if (same.length > 0 && differs.length > 0) {
|
|
7257
|
+
console.log();
|
|
7258
|
+
console.log(' ' + c.green + '✓' + c.reset + c.dim + ' ' + same.length +
|
|
7259
|
+
' other hook(s) already match.' + c.reset);
|
|
7260
|
+
}
|
|
7261
|
+
if (foreign.length > 0) {
|
|
7262
|
+
console.log();
|
|
7263
|
+
console.log(' ' + c.dim + foreign.length +
|
|
7264
|
+
' hook(s) are not shipped by this project — not checked.' + c.reset);
|
|
7265
|
+
}
|
|
7266
|
+
console.log();
|
|
7267
|
+
// Exit code for CI: 0 = nothing differs, 1 = something differs.
|
|
7268
|
+
if (differs.length > 0) process.exit(1);
|
|
7269
|
+
}
|
|
7270
|
+
|
|
5835
7271
|
async function main() {
|
|
5836
7272
|
if (UNINSTALL) return uninstall();
|
|
5837
7273
|
if (VERIFY) return verify();
|
|
@@ -5845,6 +7281,8 @@ async function main() {
|
|
|
5845
7281
|
if (VALIDATE) return validateHooks();
|
|
5846
7282
|
if (SAFE_MODE) return safeMode(!SAFE_MODE_OFF);
|
|
5847
7283
|
if (DOCTOR) return doctor();
|
|
7284
|
+
if (SHOW_CORE_IDX !== -1) return showCore(SHOW_CORE);
|
|
7285
|
+
if (OUTDATED) return outdated();
|
|
5848
7286
|
if (SIMULATE_CMD) return simulate(SIMULATE_CMD);
|
|
5849
7287
|
if (PROTECT_PATH) return protect(PROTECT_PATH);
|
|
5850
7288
|
if (RULES_FILE) return compileRules(RULES_FILE);
|
|
@@ -5865,6 +7303,7 @@ async function main() {
|
|
|
5865
7303
|
if (TEAM) return team();
|
|
5866
7304
|
if (PROFILE_IDX !== -1) return profile(PROFILE);
|
|
5867
7305
|
if (ANALYZE) return analyze();
|
|
7306
|
+
if (BLINDSPOTS) return blindspots();
|
|
5868
7307
|
if (OPUS47) return opus47();
|
|
5869
7308
|
if (SHIELD) return shield();
|
|
5870
7309
|
if (QUICKFIX) return quickfix();
|
|
@@ -5876,6 +7315,7 @@ async function main() {
|
|
|
5876
7315
|
if (DASHBOARD) return dashboard();
|
|
5877
7316
|
if (BENCHMARK) return benchmark();
|
|
5878
7317
|
if (SHARE) return share();
|
|
7318
|
+
if (SCORECARD) return scorecard();
|
|
5879
7319
|
if (DIFF_FILE) return diff(DIFF_FILE);
|
|
5880
7320
|
if (LINT) return lint();
|
|
5881
7321
|
if (CREATE_DESC) return createHook(CREATE_DESC);
|
|
@@ -5976,20 +7416,96 @@ async function main() {
|
|
|
5976
7416
|
console.log(c.bold + ' Done.' + c.reset + ' ' + Object.keys(HOOKS).length + ' safety hooks installed.');
|
|
5977
7417
|
console.log(' ' + c.dim + 'Restart Claude Code to activate.' + c.reset);
|
|
5978
7418
|
console.log();
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
console.log();
|
|
5985
|
-
console.log(' ' + c.
|
|
5986
|
-
console.log(' ' + c.
|
|
5987
|
-
console.log(' ' + c.
|
|
5988
|
-
console.log(' ' + c.
|
|
5989
|
-
console.log();
|
|
5990
|
-
console.log(
|
|
5991
|
-
|
|
5992
|
-
|
|
7419
|
+
// 以前は「You are now protected against:」と書いてこの5つを並べていた。
|
|
7420
|
+
// 並べている危険は合っているが、「守られている」は断定しすぎで、測れる範囲では正しくない。
|
|
7421
|
+
// 入れた柵の多くは命令の先頭に錨を置くので、`cd x && git push -f` の形には届かない。
|
|
7422
|
+
// ここに並んでいる強制push も .env も同じ。いちばん具体的に安心させている場所だったので、
|
|
7423
|
+
// 「何を狙う柵か」という事実に戻し、覆えているかは利用者自身の履歴から数えて見せる。
|
|
7424
|
+
console.log(' ' + c.bold + 'What these guards are aimed at:' + c.reset);
|
|
7425
|
+
console.log(' ' + c.green + ' ✓' + c.reset + ' rm -rf / git reset --hard / git clean -fd');
|
|
7426
|
+
console.log(' ' + c.green + ' ✓' + c.reset + ' Force-push to main/master');
|
|
7427
|
+
console.log(' ' + c.green + ' ✓' + c.reset + ' .env / credential files committed to git');
|
|
7428
|
+
console.log(' ' + c.green + ' ✓' + c.reset + ' Syntax errors cascading through files');
|
|
7429
|
+
console.log(' ' + c.green + ' ✓' + c.reset + ' PowerShell Remove-Item -Recurse -Force');
|
|
7430
|
+
console.log();
|
|
7431
|
+
const shapeDefault = await commandShapeSample();
|
|
7432
|
+
if (shapeDefault) {
|
|
7433
|
+
console.log(' ' + c.dim + 'Aimed at is not the same as covering. Most of these match the start of the' + c.reset);
|
|
7434
|
+
console.log(' ' + c.dim + 'command string, and in your most recent sessions ' + c.reset +
|
|
7435
|
+
c.bold + shapeDefault.compoundPct.toFixed(1) + '%' + c.reset + c.dim +
|
|
7436
|
+
' of ' + shapeDefault.calls.toLocaleString() + ' Bash' + c.reset);
|
|
7437
|
+
console.log(' ' + c.dim + 'calls are compound (' + c.reset + 'cd … && git push …' + c.dim + '), which they do not see.' + c.reset);
|
|
7438
|
+
console.log(' ' + c.dim + 'Which of yours have that gap:' + c.reset + ' npx github:yurukusa/cc-safe-setup --blindspots');
|
|
7439
|
+
} else {
|
|
7440
|
+
console.log(' ' + c.dim + 'Aimed at is not the same as covering. Most of these match the start of the' + c.reset);
|
|
7441
|
+
console.log(' ' + c.dim + 'command string, so a command that reaches them only after a separator can' + c.reset);
|
|
7442
|
+
console.log(' ' + c.dim + 'go past. Once you have some history:' + c.reset + ' npx github:yurukusa/cc-safe-setup --blindspots');
|
|
7443
|
+
}
|
|
7444
|
+
console.log();
|
|
7445
|
+
console.log(' ' + c.dim + 'Verify:' + c.reset + ' npx github:yurukusa/cc-safe-setup --verify');
|
|
7446
|
+
console.log(' ' + c.dim + 'More:' + c.reset + ' npx github:yurukusa/cc-safe-setup --shield (maximum safety)');
|
|
7447
|
+
console.log(' ' + c.dim + 'Diagnose:' + c.reset + ' https://yurukusa.github.io/cc-safe-setup/token-checkup.html');
|
|
7448
|
+
console.log(' ' + c.dim + 'Risk:' + c.reset + ' https://yurukusa.github.io/cc-safe-setup/risk-assessment.html');
|
|
7449
|
+
console.log(' ' + c.dim + 'Clusters:' + c.reset + ' https://yurukusa.github.io/cc-safe-setup/cluster-tracker.html (7 open clusters, ~11,500 user reactions across 60+ issues)');
|
|
7450
|
+
console.log(' ' + c.dim + 'AGENTS.md:' + c.reset + ' https://yurukusa.github.io/cc-safe-setup/agents-md-setup-generator.html (pick your tools → exact CLAUDE.md/AGENTS.md setup; the #6235 gap, EN/日本語)');
|
|
7451
|
+
console.log();
|
|
7452
|
+
const daysLeft2 = daysUntilJune15();
|
|
7453
|
+
const june15msg2 = daysLeft2 > 0
|
|
7454
|
+
? 'Anthropic splits programmatic billing in ' + daysLeft2 + (daysLeft2 === 1 ? ' day' : ' days') + '. claude -p invocations route to a separate credit bucket.'
|
|
7455
|
+
: 'Anthropic split programmatic billing on June 15, 2026; claude -p invocations route to a separate credit bucket.';
|
|
7456
|
+
console.log(' ' + c.yellow + '⚠ June 15:' + c.reset + ' ' + june15msg2);
|
|
7457
|
+
console.log(' ' + c.dim + ' Free 90-sec diagnostic (refund template if needed):' + c.reset);
|
|
7458
|
+
console.log(' ' + c.dim + ' https://htmlpreview.github.io/?https://gist.githubusercontent.com/yurukusa/d3a0e2403cc4078aa0183400c137d824/raw/wrong-charge-diagnostic.html' + c.reset);
|
|
7459
|
+
console.log(' ' + c.dim + ' Decision framework (stay / switch / hybridize):' + c.reset);
|
|
7460
|
+
console.log(' ' + c.dim + ' https://yurukusa.gumroad.com/l/claude-code-migration-playbook?utm_source=cc-safe-setup-cli&utm_medium=post-install&utm_campaign=migration-decision' + c.reset);
|
|
7461
|
+
console.log();
|
|
7462
|
+
// 2026-08-13: 導入直後は「自分の設定に何の守りが有って何が無いか」を見た直後で、
|
|
7463
|
+
// 書面監査の意図がいちばん高い瞬間。それなのにこのブロックは監査3件に一度も触れていなかった。
|
|
7464
|
+
// README には在るが深さ73〜96%の位置で、この面(全インストール者が見る)より読まれていない。
|
|
7465
|
+
console.log(' ' + c.blue + 'Written audits:' + c.reset + ' the part a hook cannot check — whether your own files contradict each other');
|
|
7466
|
+
console.log(' ' + c.dim + ' CLAUDE.md Audit / Token Burn Audit — $29 (¥3,980) each, one kind of file, report in 48h' + c.reset);
|
|
7467
|
+
console.log(' ' + c.dim + ' Full-Surface Audit — $219 (¥29,800), CLAUDE.md x settings.json x hooks x session logs x CI, 72h' + c.reset);
|
|
7468
|
+
console.log(' ' + c.dim + ' All three publish their full deliverable before you buy, each one run against my own setup' + c.reset);
|
|
7469
|
+
console.log(' ' + c.dim + ' (including where it found my own numbers and my own hooks to be wrong):' + c.reset);
|
|
7470
|
+
console.log(' ' + c.dim + ' https://github.com/yurukusa/cc-safe-setup/blob/main/SERVICES.md' + c.reset);
|
|
7471
|
+
console.log();
|
|
7472
|
+
// 2026-08-28: 「¥15,600 at list price」を数字ごと落とした。この額は
|
|
7473
|
+
// 「いまの定価 × 累計の冊数」で、8/28 に事故防止本を ¥800 → ¥1,500 へ上げた時点で
|
|
7474
|
+
// 旧価格の派生値として腐っていた(6冊 × ¥700 = ¥4,200 のずれ)。¥19,800 へ更新はしない。
|
|
7475
|
+
// 次の値上げでも次の1冊でも同じように腐るうえ、この額は「売れた当時の値段の合計」ではないので、
|
|
7476
|
+
// 社会的証明として掲げると買い手に実態より大きく見せることになる。冊数だけなら腐らない。
|
|
7477
|
+
console.log(' ' + c.blue + 'Zenn books (JP):' + c.reset + ' Author\'s own playbooks — 13 purchases since March 2026');
|
|
7478
|
+
console.log(' ' + c.dim + ' Anthropic公式ガイドにない事故防止 (best-seller, 97 chapters, buyers get every later revision free)' + c.reset);
|
|
7479
|
+
console.log(' ' + c.dim + ' https://zenn.dev/yurukusa/books/6076c23b1cb18b (free Chapters 1-3)' + c.reset);
|
|
7480
|
+
console.log(' ' + c.dim + ' Claude Codeのトークン消費を半分にする (¥2,500, revenue #1)' + c.reset);
|
|
7481
|
+
console.log(' ' + c.dim + ' https://zenn.dev/yurukusa/books/token-savings-guide (free Chapter 1)' + c.reset);
|
|
7482
|
+
// 2026-08-13: 12冊の露出と実売を突き合わせたら、いちばん高い ¥3,000 の企業パックは
|
|
7483
|
+
// Qiita 234本のうち0本からしか指されておらず、この面でも名前が出ず「10 more titles」に埋もれていた。
|
|
7484
|
+
// 生涯0件は市場の答えではなく、一度も機会を与えていないだけ。
|
|
7485
|
+
// しかもこの画面は「自分の設定に何の守りが有るか」を見た直後=チームへ配ってよいかを
|
|
7486
|
+
// 判断している人がいる瞬間で、宛先が一致している。
|
|
7487
|
+
// 実測=~/ops/economics/exposure-vs-sales-2026-08-13.md
|
|
7488
|
+
console.log(' ' + c.dim + ' Claude Code チーム/企業導入 安全パック (¥3,000, 9 chapters — for whoever has to sign off on team use)' + c.reset);
|
|
7489
|
+
console.log(' ' + c.dim + ' https://zenn.dev/yurukusa/books/cc-team-safety-pack' + c.reset);
|
|
7490
|
+
console.log(' ' + c.dim + ' 9 more titles at https://zenn.dev/yurukusa (Skills recipes, 800h log, AGENTS.md interop)' + c.reset);
|
|
7491
|
+
console.log();
|
|
7492
|
+
console.log(' ' + c.blue + 'Recurring track:' + c.reset + ' CC Safety Lab Founder (¥500/mo, Ko-fi, grandfathered)');
|
|
7493
|
+
console.log(' ' + c.dim + ' Monthly digest of new failure clusters + cc-safe-setup hooks shipped that month.' + c.reset);
|
|
7494
|
+
// 2026-08-13: ここは以前「June 2026: … July: …」と、配信していない号を配信済みのように書いていた。
|
|
7495
|
+
// Ko-fi の投稿一覧を実測すると号は6本(04-23/04-24/05-08/05-15/05-22/08-08)で、6月と7月は落としている。
|
|
7496
|
+
// tier の説明側は 2026-08-12 にそう直したのに、いちばん読まれるこの面に古い主張が残っていた。
|
|
7497
|
+
console.log(' ' + c.dim + ' 6 issues so far: 2026-04-23 / 04-24 / 05-08 / 05-15 / 05-22 / 08-08. June and July were missed.' + c.reset);
|
|
7498
|
+
console.log(' ' + c.dim + ' https://yurukusa.github.io/cc-safe-setup/safety-lab.html#en' + c.reset);
|
|
7499
|
+
console.log();
|
|
7500
|
+
// 2026-07-27: Claim-Verify Handbook はいま未公開(商品ページの is_published が false)。
|
|
7501
|
+
// 買えない商品を、導入直後の全利用者へ「live, $19」と案内していたので2行を落とした。
|
|
7502
|
+
// URLは HTTP 200 を返すため、応答の番号だけでは死んでいると分からない。
|
|
7503
|
+
console.log(' ' + c.blue + 'Sister handbook:' + c.reset + ' Sub-Agent Observability Handbook preview (full PDF pending)');
|
|
7504
|
+
console.log(' ' + c.dim + ' Sub-Agent Observability Handbook (7-case sub-agent cluster + 4 defense hooks) — free preview, full PDF pending' + c.reset);
|
|
7505
|
+
console.log(' ' + c.dim + ' https://gist.github.com/yurukusa/bc61438321b369dbd3765e2bd9702a80 (free Chapter 1 preview, EN)' + c.reset);
|
|
7506
|
+
console.log();
|
|
7507
|
+
console.log(' ' + c.dim + 'Feedback:' + c.reset + ' https://github.com/yurukusa/cc-safe-setup/discussions (questions / patterns that worked)');
|
|
7508
|
+
console.log(' ' + c.dim + 'Bug:' + c.reset + ' https://github.com/yurukusa/cc-safe-setup/issues/new/choose (false positives / install issues)');
|
|
5993
7509
|
console.log();
|
|
5994
7510
|
}
|
|
5995
7511
|
|