cc-safe-setup 29.8.0 → 30.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1017) hide show
  1. package/.claude-plugin/marketplace.json +72 -19
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CONTRIBUTING.md +7 -2
  4. package/COOKBOOK.md +50 -46
  5. package/LICENSE +21 -0
  6. package/MIGRATION.md +8 -8
  7. package/README.md +421 -512
  8. package/SAFETY_CHECKLIST.md +15 -7
  9. package/SERVICES.md +194 -0
  10. package/SETTINGS_REFERENCE.md +236 -17
  11. package/SKILL.md +5 -5
  12. package/TROUBLESHOOTING.md +35 -14
  13. package/action.yml +12 -4
  14. package/audit/README.md +84 -0
  15. package/audit/audit-checklist.md +152 -0
  16. package/audit/boundary-selftest.sh +85 -0
  17. package/audit/boundary.sh +123 -0
  18. package/audit/count-hooks.py +42 -0
  19. package/audit/find-dead-hooks.sh +82 -0
  20. package/audit/find-unfirable-rules.sh +142 -0
  21. package/audit/fire.sh +97 -0
  22. package/audit/selftest.sh +42 -0
  23. package/audit/unfirable-selftest.sh +81 -0
  24. package/audit-web/index.html +9 -9
  25. package/examples/README.md +54 -17
  26. package/examples/account-billing-log.sh +98 -0
  27. package/examples/account-routing-preflight.sh +123 -0
  28. package/examples/agent-sdk-credit-pool-monitor.sh +145 -0
  29. package/examples/agent-view-quota-decrement.sh +32 -0
  30. package/examples/agent-view-quota-warn.sh +94 -0
  31. package/examples/agents-md-edit-drift-warner.sh +223 -0
  32. package/examples/agents-md-loader.sh +160 -0
  33. package/examples/agents-md-sync-checker.sh +102 -0
  34. package/examples/ai-slop-punctuation-arrest.sh +210 -0
  35. package/examples/allowlist.sh +70 -10
  36. package/examples/always-allow-pattern-suggester.sh +125 -0
  37. package/examples/ansible-vault-guard.sh +2 -1
  38. package/examples/anthropic-base-url-guard.sh +76 -0
  39. package/examples/api-busyloop-guard.sh +123 -0
  40. package/examples/api-endpoint-guard.sh +12 -0
  41. package/examples/api-key-in-url-guard.sh +17 -1
  42. package/examples/api-rate-limit-guard.sh +36 -1
  43. package/examples/api-rate-limit-tracker.sh +12 -0
  44. package/examples/articulated-scope-capture.sh +101 -0
  45. package/examples/askuserquestion-autonomy-gate.sh +102 -0
  46. package/examples/attachment-traversal-guard.sh +118 -0
  47. package/examples/aup-block-pattern-logger.sh +161 -0
  48. package/examples/aup-false-positive-helper.sh +124 -0
  49. package/examples/aup-large-tool-output-warner.sh +282 -0
  50. package/examples/aup-retry-loop-guard.sh +229 -0
  51. package/examples/auth-expiry-reminder.sh +157 -0
  52. package/examples/auth-macos-sleep-detector.sh +185 -0
  53. package/examples/auth-path-detector.sh +171 -0
  54. package/examples/auth-status-checker.sh +157 -0
  55. package/examples/authorized-reconfirmation-detector.sh +195 -0
  56. package/examples/auto-approve-build.sh +39 -0
  57. package/examples/auto-approve-cargo.sh +31 -1
  58. package/examples/auto-approve-docker.sh +35 -0
  59. package/examples/auto-approve-git-read.sh +37 -14
  60. package/examples/auto-approve-go.sh +30 -1
  61. package/examples/auto-approve-gradle.sh +29 -1
  62. package/examples/auto-approve-make.sh +29 -1
  63. package/examples/auto-approve-maven.sh +29 -1
  64. package/examples/auto-approve-python.sh +41 -1
  65. package/examples/auto-approve-readonly-tools.sh +1 -0
  66. package/examples/auto-approve-readonly.sh +110 -33
  67. package/examples/auto-approve-ssh.sh +30 -1
  68. package/examples/auto-approve-test.sh +44 -0
  69. package/examples/auto-checkpoint.sh +1 -1
  70. package/examples/auto-compact-prep.sh +0 -0
  71. package/examples/auto-git-checkpoint.sh +0 -0
  72. package/examples/auto-mode-safe-commands.sh +53 -1
  73. package/examples/auto-mode-safety-enforcer.sh +151 -4
  74. package/examples/auto-push-session-branch.sh +22 -0
  75. package/examples/aws-production-guard.sh +13 -1
  76. package/examples/aws-region-guard.sh +1 -1
  77. package/examples/background-cost-launch-guard.sh +82 -0
  78. package/examples/background-task-guard.sh +12 -0
  79. package/examples/backup-before-refactor.sh +0 -0
  80. package/examples/banned-command-guard.sh +12 -0
  81. package/examples/bash-allowlist-secondary-check.sh +157 -0
  82. package/examples/bash-domain-allowlist.sh +12 -0
  83. package/examples/bash-fanout-bounded-rewriter.sh +191 -0
  84. package/examples/bash-heuristic-approver.sh +112 -10
  85. package/examples/bash-project-fence.sh +194 -0
  86. package/examples/bash-safety-auto-deny.sh +12 -0
  87. package/examples/bash-secret-output-detector.sh +44 -3
  88. package/examples/bash-trace-guard.sh +12 -0
  89. package/examples/bashrc-safety-check.sh +0 -0
  90. package/examples/bg-permission-prompt-warner.sh +182 -0
  91. package/examples/bg-task-cooldown-guard.sh +12 -0
  92. package/examples/binary-read-detector.sh +135 -0
  93. package/examples/binary-upload-guard.sh +12 -0
  94. package/examples/block-database-wipe.sh +35 -10
  95. package/examples/branch-name-check.sh +8 -3
  96. package/examples/branch-naming-convention.sh +0 -0
  97. package/examples/broad-find-guard.sh +12 -0
  98. package/examples/broad-prefix-session-trap-warner.sh +106 -0
  99. package/examples/bulk-file-delete-guard.sh +45 -0
  100. package/examples/bypass-mode-effective-verifier.sh +203 -0
  101. package/examples/cache-creation-drift-detector.sh +120 -0
  102. package/examples/cache-residue-detector.sh +195 -0
  103. package/examples/cache-tier-logger.sh +70 -0
  104. package/examples/cache-ttl-eviction-detector.sh +149 -0
  105. package/examples/cargo-publish-guard.sh +14 -1
  106. package/examples/case-insensitive-path-guard.sh +21 -2
  107. package/examples/case-sensitive-guard.sh +23 -2
  108. package/examples/cch-cache-guard.sh +0 -0
  109. package/examples/cch-sentinel-precommit-guard.sh +47 -0
  110. package/examples/check-abort-controller.sh +1 -1
  111. package/examples/check-accessibility.sh +0 -0
  112. package/examples/check-aria-labels.sh +0 -0
  113. package/examples/check-before-act-enforcer.sh +12 -0
  114. package/examples/check-charset-meta.sh +0 -0
  115. package/examples/check-cookie-flags.sh +0 -0
  116. package/examples/check-cors-config.sh +0 -0
  117. package/examples/check-csp-headers.sh +0 -0
  118. package/examples/check-csrf-protection.sh +0 -0
  119. package/examples/check-debounce.sh +1 -1
  120. package/examples/check-dependency-age.sh +0 -0
  121. package/examples/check-dependency-license.sh +0 -0
  122. package/examples/check-dockerfile-best-practice.sh +0 -0
  123. package/examples/check-error-boundaries.sh +0 -0
  124. package/examples/check-error-class.sh +1 -1
  125. package/examples/check-error-handling.sh +0 -0
  126. package/examples/check-error-logging.sh +1 -1
  127. package/examples/check-error-message.sh +0 -0
  128. package/examples/check-error-stack.sh +0 -0
  129. package/examples/check-favicon.sh +0 -0
  130. package/examples/check-git-hooks-compat.sh +0 -0
  131. package/examples/check-gitattributes.sh +0 -0
  132. package/examples/check-https-redirect.sh +0 -0
  133. package/examples/check-input-validation.sh +0 -0
  134. package/examples/check-lang-attribute.sh +0 -0
  135. package/examples/check-npm-scripts-exist.sh +0 -0
  136. package/examples/check-package-size.sh +0 -0
  137. package/examples/check-port-availability.sh +0 -0
  138. package/examples/check-promise-all.sh +1 -1
  139. package/examples/check-rate-limiting.sh +0 -0
  140. package/examples/check-return-types.sh +0 -0
  141. package/examples/check-semantic-versioning.sh +0 -0
  142. package/examples/check-suspense-fallback.sh +1 -1
  143. package/examples/check-test-exists.sh +0 -0
  144. package/examples/check-test-naming.sh +0 -0
  145. package/examples/check-timeout-cleanup.sh +1 -1
  146. package/examples/check-tls-version.sh +0 -0
  147. package/examples/check-unsubscribe.sh +1 -1
  148. package/examples/check-viewport-meta.sh +0 -0
  149. package/examples/check-worker-terminate.sh +1 -1
  150. package/examples/checkpoint-tamper-guard.sh +12 -0
  151. package/examples/chmod-guard.sh +12 -0
  152. package/examples/chown-guard.sh +24 -0
  153. package/examples/ci-workflow-guard.sh +6 -1
  154. package/examples/claim-verify-detector.sh +109 -0
  155. package/examples/claim-vs-caveat-checker.sh +77 -0
  156. package/examples/classifier-fallback-allow.sh +114 -40
  157. package/examples/claude-cache-gc.sh +1 -0
  158. package/examples/claude-code-version-pin-advisor.sh +183 -0
  159. package/examples/claude-md-reinjector.sh +129 -0
  160. package/examples/claude-md-size-monitor.sh +66 -0
  161. package/examples/claude-update-budget-guard.sh +123 -0
  162. package/examples/claude-update-smart.sh +113 -0
  163. package/examples/claudeignore-enforce-guard.sh +12 -0
  164. package/examples/claudemd-enforcer.sh +0 -0
  165. package/examples/claudemd-tool-prohibition.sh +128 -0
  166. package/examples/clear-command-confirm-guard.sh +32 -15
  167. package/examples/cli-config-pinning-detector.sh +162 -0
  168. package/examples/cliff-countdown-advisor.sh +96 -0
  169. package/examples/clipboard-secret-guard.sh +12 -0
  170. package/examples/closure-word-verify-gate.sh +150 -0
  171. package/examples/cloud-cli-guard.sh +39 -2
  172. package/examples/commit-all-scope-guard.sh +44 -0
  173. package/examples/commit-message-check.sh +1 -1
  174. package/examples/commit-message-quality.sh +6 -1
  175. package/examples/commitment-carry-forward-arrest.sh +361 -0
  176. package/examples/compact-alert-notification.sh +0 -0
  177. package/examples/compact-dispatch-watchdog.sh +149 -0
  178. package/examples/completion-claim-without-verification-detector.sh +207 -0
  179. package/examples/composer-guard.sh +12 -0
  180. package/examples/compound-bash-permission-resolver.sh +156 -0
  181. package/examples/compound-command-allow.sh +1 -1
  182. package/examples/compound-command-deny-enforcer.sh +183 -0
  183. package/examples/compound-inject-guard.sh +12 -0
  184. package/examples/concurrent-edit-lock.sh +12 -0
  185. package/examples/conflict-marker-guard.sh +12 -0
  186. package/examples/consecutive-error-breaker.sh +0 -0
  187. package/examples/console-log-count.sh +1 -0
  188. package/examples/context-compact-advisor.sh +29 -2
  189. package/examples/context-length-budget.sh +69 -0
  190. package/examples/context-threshold-alert.sh +12 -0
  191. package/examples/context-usage-drift-alert.sh +41 -13
  192. package/examples/conversation-history-guard.sh +12 -0
  193. package/examples/core-file-protect-guard.sh +12 -0
  194. package/examples/cors-star-warn.sh +0 -0
  195. package/examples/cost-incident-self-audit.sh +162 -0
  196. package/examples/cowork-claude-md-load-checker.sh +100 -0
  197. package/examples/cowork-fuse-staleness-watcher.sh +140 -0
  198. package/examples/cowork-hook-absence-warner.sh +167 -0
  199. package/examples/cowork-model-picker-advisor.sh +115 -0
  200. package/examples/credential-exfil-guard.sh +65 -4
  201. package/examples/credential-file-cat-guard.sh +17 -1
  202. package/examples/cron-create-receipt.sh +112 -0
  203. package/examples/cron-modification-guard.sh +12 -0
  204. package/examples/cross-product-mcp-spawn-detector.sh +178 -0
  205. package/examples/cwd-project-boundary-guard.sh +0 -0
  206. package/examples/daemon-zombie-worker-surfacer.sh +118 -0
  207. package/examples/daily-cost-guard.sh +164 -0
  208. package/examples/daily-usage-tracker.sh +0 -0
  209. package/examples/dangerous-pip-flag-guard.sh +12 -0
  210. package/examples/db-connect-guard.sh +13 -1
  211. package/examples/denied-action-retry-guard.sh +12 -0
  212. package/examples/deny-bypass-detector.sh +12 -0
  213. package/examples/deny-rule-integrity-verifier.sh +181 -0
  214. package/examples/dependency-install-guard.sh +12 -0
  215. package/examples/deploy-guard.sh +14 -2
  216. package/examples/deploy-path-verify-guard.sh +12 -0
  217. package/examples/deployment-readback-gate.sh +135 -0
  218. package/examples/deployment-readback-gh-adapter.sh +247 -0
  219. package/examples/deployment-verify-guard.sh +25 -2
  220. package/examples/destructive-db-script-write-guard.sh +166 -0
  221. package/examples/destructive-migration-write-guard.sh +163 -0
  222. package/examples/diff-size-guard.sh +12 -0
  223. package/examples/disabled-feature-toggle-advisor.sh +88 -0
  224. package/examples/disk-partition-guard.sh +12 -0
  225. package/examples/dispatch-allowlist-preflight.sh +301 -0
  226. package/examples/dispatch-liveness-watchdog.sh +216 -0
  227. package/examples/dispatch-receipt.sh +174 -0
  228. package/examples/django-migrate-guard.sh +12 -0
  229. package/examples/dns-config-guard.sh +12 -0
  230. package/examples/docker-dangerous-guard.sh +12 -0
  231. package/examples/docker-volume-guard.sh +0 -0
  232. package/examples/dockerfile-latest-guard.sh +1 -0
  233. package/examples/dotenv-anthropic-key-billing-guard.sh +99 -0
  234. package/examples/dotenv-commit-guard.sh +12 -0
  235. package/examples/dotenv-read-guard.sh +12 -0
  236. package/examples/dotfile-protection-guard.sh +12 -0
  237. package/examples/dotnet-build-on-edit.sh +1 -1
  238. package/examples/drizzle-migrate-guard.sh +27 -2
  239. package/examples/echo-probe-spam-detector.sh +176 -0
  240. package/examples/edit-counter-test-gate.sh +12 -0
  241. package/examples/edit-error-counter.sh +1 -1
  242. package/examples/edit-guard.sh +12 -0
  243. package/examples/edit-old-string-validator.sh +12 -0
  244. package/examples/edit-quote-ambiguity-guard.sh +97 -0
  245. package/examples/edit-retry-loop-guard.sh +0 -0
  246. package/examples/edit-verify.sh +0 -0
  247. package/examples/env-inline-secret-guard.sh +14 -2
  248. package/examples/env-naming-convention.sh +0 -0
  249. package/examples/env-prod-guard.sh +0 -0
  250. package/examples/env-required-check.sh +0 -0
  251. package/examples/env-source-guard.sh +12 -0
  252. package/examples/env-var-check.sh +24 -1
  253. package/examples/ephemeral-container-detector.sh +147 -0
  254. package/examples/error-memory-guard.sh +12 -0
  255. package/examples/evidence-claim-gate.sh +187 -0
  256. package/examples/exploration-budget-guard.sh +12 -0
  257. package/examples/expo-eject-guard.sh +13 -0
  258. package/examples/extended-thinking-loop-guard.sh +139 -0
  259. package/examples/extended-thinking-resume-warning.sh +119 -0
  260. package/examples/extended-thinking-tool-use-mismatch-detector.sh +212 -0
  261. package/examples/fabricated-command-detector.sh +145 -0
  262. package/examples/false-disk-full-guard.sh +63 -0
  263. package/examples/feature-deprecation-detector.sh +109 -0
  264. package/examples/file-change-tracker.sh +1 -1
  265. package/examples/file-change-undo-tracker.sh +0 -0
  266. package/examples/file-edit-backup.sh +63 -14
  267. package/examples/file-reference-check.sh +0 -0
  268. package/examples/file-size-limit.sh +12 -0
  269. package/examples/financial-operation-guard.sh +12 -0
  270. package/examples/firewall-guard.sh +12 -0
  271. package/examples/fish-shell-wrapper.sh +125 -7
  272. package/examples/five-hundred-milestone.sh +1 -0
  273. package/examples/flask-static-route-guard.sh +89 -0
  274. package/examples/forbidden-response-phrase-detector.sh +161 -0
  275. package/examples/gem-push-guard.sh +13 -0
  276. package/examples/gh-cli-destructive-guard.sh +13 -1
  277. package/examples/git-checkout-safety-guard.sh +12 -0
  278. package/examples/git-checkout-uncommitted-guard.sh +12 -0
  279. package/examples/git-config-guard.sh +12 -0
  280. package/examples/git-crypt-worktree-guard.sh +12 -0
  281. package/examples/git-filter-repo-guard.sh +56 -0
  282. package/examples/git-history-rewrite-guard.sh +12 -0
  283. package/examples/git-hook-bypass-guard.sh +0 -0
  284. package/examples/git-maintenance-guard.sh +83 -0
  285. package/examples/git-merge-conflict-prevent.sh +0 -0
  286. package/examples/git-message-length-check.sh +0 -0
  287. package/examples/git-message-length.sh +0 -0
  288. package/examples/git-operations-require-approval.sh +68 -14
  289. package/examples/git-pack-temp-cleanup-guard.sh +102 -0
  290. package/examples/git-push-blast-radius-guard.sh +218 -0
  291. package/examples/git-remote-guard.sh +24 -3
  292. package/examples/git-rm-orphan-wipe-guard.sh +85 -0
  293. package/examples/git-stash-before-checkout.sh +12 -0
  294. package/examples/git-stash-before-danger.sh +0 -0
  295. package/examples/git-submodule-guard.sh +69 -2
  296. package/examples/git-tag-guard.sh +12 -0
  297. package/examples/github-actions-secret-guard.sh +0 -0
  298. package/examples/gitignore-check.sh +0 -0
  299. package/examples/gitops-drift-guard.sh +12 -0
  300. package/examples/go-mod-tidy-warn.sh +1 -0
  301. package/examples/go-vet-after-edit.sh +12 -0
  302. package/examples/goal-iteration-limit-warner.sh +152 -0
  303. package/examples/grep-nul-false-negative-guard.sh +30 -0
  304. package/examples/growthbook-flag-monitor.sh +257 -0
  305. package/examples/hallucination-url-check.sh +1 -1
  306. package/examples/halt-signal-detector.sh +135 -0
  307. package/examples/handoff-on-clear.sh +65 -0
  308. package/examples/hardcoded-ip-guard.sh +1 -0
  309. package/examples/headless-empty-result-guard.sh +0 -0
  310. package/examples/headless-stop-guard.sh +0 -0
  311. package/examples/helm-install-guard.sh +2 -1
  312. package/examples/home-critical-bash-guard.sh +17 -1
  313. package/examples/hook-debug-wrapper.sh +6 -2
  314. package/examples/hook-permission-fixer.sh +0 -0
  315. package/examples/hook-self-disable-detector.sh +99 -0
  316. package/examples/hook-stdout-sanitizer.sh +6 -2
  317. package/examples/hook-tamper-guard.sh +12 -0
  318. package/examples/idle-session-cost-alert.sh +0 -0
  319. package/examples/image-dimension-guard.sh +109 -0
  320. package/examples/image-file-validator.sh +0 -0
  321. package/examples/iteration-quality-degradation-detector.sh +170 -0
  322. package/examples/java-compile-on-edit.sh +1 -1
  323. package/examples/json-syntax-check.sh +0 -0
  324. package/examples/k8s-production-guard.sh +13 -1
  325. package/examples/kill-process-guard.sh +12 -0
  326. package/examples/kubernetes-guard.sh +13 -1
  327. package/examples/laravel-artisan-guard.sh +13 -0
  328. package/examples/large-file-guard.sh +1 -1
  329. package/examples/log-level-guard.sh +0 -0
  330. package/examples/log-truncation-guard.sh +12 -0
  331. package/examples/long-session-malformed-tool-call-detector.sh +183 -0
  332. package/examples/loop-detector.sh +12 -0
  333. package/examples/magic-number-warn.sh +1 -0
  334. package/examples/main-branch-warn.sh +12 -0
  335. package/examples/markdown-link-check.sh +0 -0
  336. package/examples/max-concurrent-agents.sh +12 -0
  337. package/examples/max-edit-size-guard.sh +13 -0
  338. package/examples/max-file-count-guard.sh +0 -0
  339. package/examples/max-file-delete-count.sh +0 -0
  340. package/examples/max-function-length.sh +1 -0
  341. package/examples/max-import-count.sh +0 -0
  342. package/examples/max-subagent-count.sh +0 -0
  343. package/examples/mcp-config-freeze.sh +12 -0
  344. package/examples/mcp-config-poisoning-audit.sh +150 -0
  345. package/examples/mcp-data-boundary.sh +1 -1
  346. package/examples/mcp-misdiagnosis-arrest.sh +209 -0
  347. package/examples/mcp-orphan-leak-warner.sh +49 -0
  348. package/examples/mcp-permissions-ask-warner.sh +151 -0
  349. package/examples/mcp-routine-approval-detector.sh +154 -0
  350. package/examples/mcp-server-allowlist.sh +12 -0
  351. package/examples/mcp-server-guard.sh +12 -0
  352. package/examples/mcp-startup-bloat-detector.sh +87 -0
  353. package/examples/mcp-stdio-compatibility-test.sh +153 -0
  354. package/examples/mcp-tool-audit-log.sh +0 -0
  355. package/examples/mcp-tool-guard.sh +101 -1
  356. package/examples/mcp-warmup-wait.sh +0 -0
  357. package/examples/memory-chain-audit.sh +56 -0
  358. package/examples/memory-orphan-detector.sh +188 -0
  359. package/examples/memory-write-guard.sh +99 -17
  360. package/examples/migration-verify-guard.sh +12 -0
  361. package/examples/model-swap-suggester.sh +177 -0
  362. package/examples/model-version-alert.sh +5 -1
  363. package/examples/model-version-lock.sh +63 -0
  364. package/examples/move-delete-sequence-guard.sh +33 -2
  365. package/examples/multi-file-plan-routing-gate.sh +192 -0
  366. package/examples/multi-vendor-concurrent-warner.sh +108 -0
  367. package/examples/multi-window-auth-drift-detector.sh +183 -0
  368. package/examples/multiline-command-approver.sh +161 -23
  369. package/examples/nested-background-agent-guard.sh +109 -0
  370. package/examples/nested-spawn-inflight-guard.sh +178 -0
  371. package/examples/network-exfil-guard.sh +12 -0
  372. package/examples/network-guard.sh +12 -0
  373. package/examples/network-interface-guard.sh +12 -0
  374. package/examples/nextjs-env-guard.sh +1 -1
  375. package/examples/no-absolute-import.sh +0 -0
  376. package/examples/no-alert-confirm-prompt.sh +0 -0
  377. package/examples/no-anonymous-default-export.sh +0 -0
  378. package/examples/no-any-type.sh +0 -0
  379. package/examples/no-any-typescript.sh +1 -0
  380. package/examples/no-ask-human.sh +12 -0
  381. package/examples/no-assignment-in-condition.sh +0 -0
  382. package/examples/no-base64-exfil.sh +12 -0
  383. package/examples/no-callback-hell.sh +0 -0
  384. package/examples/no-circular-dependency.sh +0 -0
  385. package/examples/no-cleartext-storage.sh +0 -0
  386. package/examples/no-commented-code.sh +0 -0
  387. package/examples/no-commit-fixup.sh +0 -0
  388. package/examples/no-console-assert.sh +0 -0
  389. package/examples/no-console-error-swallow.sh +0 -0
  390. package/examples/no-console-in-prod.sh +0 -0
  391. package/examples/no-console-log-commit.sh +12 -0
  392. package/examples/no-console-log.sh +0 -0
  393. package/examples/no-console-time.sh +0 -0
  394. package/examples/no-cors-wildcard.sh +4 -1
  395. package/examples/no-curl-upload.sh +0 -0
  396. package/examples/no-dangerouslySetInnerHTML.sh +0 -0
  397. package/examples/no-dangling-await.sh +1 -0
  398. package/examples/no-debug-commit.sh +12 -0
  399. package/examples/no-debug-in-commit.sh +0 -0
  400. package/examples/no-deep-nesting.sh +0 -0
  401. package/examples/no-deep-relative-import.sh +1 -0
  402. package/examples/no-default-credentials.sh +0 -0
  403. package/examples/no-deploy-friday.sh +13 -1
  404. package/examples/no-disabled-test.sh +0 -0
  405. package/examples/no-document-write.sh +0 -0
  406. package/examples/no-empty-function.sh +0 -0
  407. package/examples/no-eval-in-template.sh +0 -0
  408. package/examples/no-eval-template.sh +3 -1
  409. package/examples/no-eval.sh +0 -0
  410. package/examples/no-exec-user-input.sh +0 -0
  411. package/examples/no-fixme-ship.sh +12 -0
  412. package/examples/no-floating-promises.sh +0 -0
  413. package/examples/no-force-flag.sh +22 -5
  414. package/examples/no-force-install.sh +0 -0
  415. package/examples/no-git-amend.sh +12 -0
  416. package/examples/no-git-rebase-public.sh +0 -0
  417. package/examples/no-global-install.sh +12 -0
  418. package/examples/no-global-state.sh +0 -0
  419. package/examples/no-hardcoded-port.sh +1 -0
  420. package/examples/no-hardcoded-url.sh +0 -0
  421. package/examples/no-helmet-missing.sh +0 -0
  422. package/examples/no-http-url.sh +1 -0
  423. package/examples/no-http-without-https.sh +0 -0
  424. package/examples/no-inline-style.sh +0 -0
  425. package/examples/no-inline-styles.sh +1 -0
  426. package/examples/no-innerhtml.sh +0 -0
  427. package/examples/no-install-global.sh +12 -0
  428. package/examples/no-jwt-in-url.sh +0 -0
  429. package/examples/no-large-commit.sh +0 -0
  430. package/examples/no-localhost-expose.sh +0 -0
  431. package/examples/no-magic-number.sh +0 -0
  432. package/examples/no-md5-sha1.sh +0 -0
  433. package/examples/no-mixed-line-endings.sh +8 -1
  434. package/examples/no-mutation-in-reducer.sh +0 -0
  435. package/examples/no-nested-ternary.sh +0 -0
  436. package/examples/no-network-exfil.sh +0 -0
  437. package/examples/no-open-redirect.sh +0 -0
  438. package/examples/no-output-truncation.sh +12 -0
  439. package/examples/no-package-downgrade.sh +0 -0
  440. package/examples/no-package-lock-edit.sh +12 -0
  441. package/examples/no-path-join-user-input.sh +0 -0
  442. package/examples/no-port-bind.sh +0 -0
  443. package/examples/no-process-exit.sh +0 -0
  444. package/examples/no-prototype-pollution.sh +0 -0
  445. package/examples/no-push-without-ci.sh +0 -0
  446. package/examples/no-push-without-tests.sh +12 -0
  447. package/examples/no-raw-password-in-url.sh +0 -0
  448. package/examples/no-root-user-docker.sh +1 -0
  449. package/examples/no-root-write.sh +12 -0
  450. package/examples/no-secrets-in-args.sh +4 -1
  451. package/examples/no-secrets-in-logs.sh +0 -0
  452. package/examples/no-sensitive-log.sh +0 -0
  453. package/examples/no-sleep-in-hooks.sh +0 -0
  454. package/examples/no-star-import-python.sh +1 -0
  455. package/examples/no-string-concat-sql.sh +0 -0
  456. package/examples/no-sudo-guard.sh +12 -0
  457. package/examples/no-sync-fs.sh +0 -0
  458. package/examples/no-todo-in-merge.sh +0 -0
  459. package/examples/no-todo-in-production.sh +1 -0
  460. package/examples/no-todo-without-issue.sh +0 -0
  461. package/examples/no-unused-import.sh +0 -0
  462. package/examples/no-var-keyword.sh +0 -0
  463. package/examples/no-verify-blocker.sh +12 -0
  464. package/examples/no-wget-piped-bash.sh +12 -0
  465. package/examples/no-wildcard-cors.sh +0 -0
  466. package/examples/no-wildcard-delete.sh +0 -0
  467. package/examples/no-wildcard-import.sh +0 -0
  468. package/examples/no-with-statement.sh +0 -0
  469. package/examples/no-write-outside-src.sh +0 -0
  470. package/examples/no-xml-external-entity.sh +0 -0
  471. package/examples/non-english-quality-warner.sh +156 -0
  472. package/examples/notify-waiting.sh +1 -1
  473. package/examples/npm-audit-warn.sh +0 -0
  474. package/examples/npm-global-install-guard.sh +12 -0
  475. package/examples/npm-publish-guard.sh +12 -0
  476. package/examples/npm-script-injection.sh +0 -0
  477. package/examples/npm-supply-chain-guard.sh +0 -0
  478. package/examples/nuxt-config-guard.sh +1 -0
  479. package/examples/oauth-refresh-monitor.sh +210 -0
  480. package/examples/opus-degradation-tracker.sh +187 -0
  481. package/examples/opus48-routine-task-warning.sh +196 -0
  482. package/examples/opus48-thinking-wedge-advisor.sh +143 -0
  483. package/examples/output-credential-scan.sh +1 -1
  484. package/examples/output-secret-mask.sh +31 -9
  485. package/examples/output-token-spike-detector.sh +145 -0
  486. package/examples/package-json-guard.sh +12 -0
  487. package/examples/package-lock-frozen.sh +12 -0
  488. package/examples/parallel-batch-size-limiter.sh +121 -0
  489. package/examples/parallel-cascade-detector.sh +116 -0
  490. package/examples/partial-view-claim-arrest.sh +174 -0
  491. package/examples/path-deny-bash-guard.sh +12 -0
  492. package/examples/path-traversal-guard.sh +12 -0
  493. package/examples/per-prompt-token-cap.sh +124 -0
  494. package/examples/permission-audit-log.sh +1 -1
  495. package/examples/permission-denial-enforcer.sh +12 -0
  496. package/examples/permission-entry-validator.sh +0 -0
  497. package/examples/persisted-1m-model-advisor.sh +88 -0
  498. package/examples/php-lint-on-edit.sh +1 -1
  499. package/examples/pii-upload-guard.sh +12 -0
  500. package/examples/pip-publish-guard.sh +13 -0
  501. package/examples/pip-requirements-guard.sh +12 -0
  502. package/examples/pip-venv-required.sh +12 -0
  503. package/examples/plan-mode-edit-guard.sh +12 -0
  504. package/examples/plan-mode-enforcer.sh +12 -0
  505. package/examples/plan-mode-strict-guard.sh +12 -0
  506. package/examples/plugin-hooks-json-bloat-detector.sh +169 -0
  507. package/examples/plugin-process-cleanup.sh +0 -0
  508. package/examples/polyglot-rm-guard.sh +12 -0
  509. package/examples/post-compact-safety.sh +12 -0
  510. package/examples/post-edit-disk-verify.sh +152 -0
  511. package/examples/powershell-remove-item-guard.sh +85 -0
  512. package/examples/pr-description-check.sh +0 -0
  513. package/examples/pr-duplicate-guard.sh +13 -0
  514. package/examples/pre-bash-sed-line-ending-windows.sh +87 -0
  515. package/examples/pre-compact-knowledge-save.sh +0 -0
  516. package/examples/pre-compact-transcript-backup.sh +0 -0
  517. package/examples/pre-execution-claim-detector.sh +179 -0
  518. package/examples/prefer-builtin-tools.sh +10 -1
  519. package/examples/prefer-const.sh +0 -0
  520. package/examples/prefer-dedicated-tools.sh +12 -0
  521. package/examples/prefer-optional-chaining.sh +0 -0
  522. package/examples/prisma-migrate-guard.sh +24 -7
  523. package/examples/production-port-kill-guard.sh +12 -0
  524. package/examples/prompt-injection-guard.sh +2 -1
  525. package/examples/prompt-usage-logger.sh +0 -0
  526. package/examples/protect-claudemd.sh +12 -0
  527. package/examples/protect-dotfiles.sh +12 -0
  528. package/examples/proxy-capture-suggester.sh +159 -0
  529. package/examples/public-artefact-socratic-narrowing.sh +232 -0
  530. package/examples/public-repo-push-guard.sh +12 -0
  531. package/examples/push-requires-test-pass-record.sh +0 -0
  532. package/examples/push-requires-test-pass.sh +12 -0
  533. package/examples/quota-anomaly-detector.sh +173 -0
  534. package/examples/quota-reset-audit.sh +58 -0
  535. package/examples/quoted-flag-approver.sh +99 -10
  536. package/examples/rails-migration-guard.sh +12 -0
  537. package/examples/rate-limit-guard.sh +0 -0
  538. package/examples/read-all-files-enforcer.sh +0 -0
  539. package/examples/read-budget-guard.sh +12 -0
  540. package/examples/read-loop-detector.sh +59 -0
  541. package/examples/read-once-guard.sh +96 -0
  542. package/examples/read-only-mode.sh +12 -0
  543. package/examples/readme-exists-check.sh +0 -0
  544. package/examples/record-read-coverage.sh +73 -0
  545. package/examples/redirect-fragment-warner.sh +149 -0
  546. package/examples/redis-flushall-guard.sh +13 -0
  547. package/examples/redundant-read-blocker.sh +132 -0
  548. package/examples/refusal-arrest-gate.sh +248 -0
  549. package/examples/registry-publish-guard.sh +13 -1
  550. package/examples/remote-control-billing-classifier.sh +135 -0
  551. package/examples/replace-all-guard.sh +12 -0
  552. package/examples/repo-visibility-guard.sh +12 -0
  553. package/examples/reroute-after-block-guard.sh +150 -0
  554. package/examples/response-budget-guard.sh +0 -0
  555. package/examples/resume-drift-watcher.sh +57 -0
  556. package/examples/rg-replace-flag-detector.sh +125 -0
  557. package/examples/rhetorical-verification-prompt-detector.sh +102 -0
  558. package/examples/ripgrep-permission-fix.sh +0 -0
  559. package/examples/rm-safety-net.sh +151 -8
  560. package/examples/role-tool-guard.sh +12 -0
  561. package/examples/route-handler-emptiness-gate.sh +324 -0
  562. package/examples/ruby-lint-on-edit.sh +1 -1
  563. package/examples/runtime-binary-change-detector.sh +102 -0
  564. package/examples/same-command-repeat-detector.sh +196 -0
  565. package/examples/same-correction-arrest.sh +169 -0
  566. package/examples/sandbox-denywrite-enforcer.sh +78 -0
  567. package/examples/sandbox-relative-path-audit.sh +83 -25
  568. package/examples/sandbox-write-verify.sh +12 -0
  569. package/examples/schema-migration-guard.sh +0 -0
  570. package/examples/scope-expansion-receipt.sh +178 -0
  571. package/examples/scope-guard.sh +88 -9
  572. package/examples/secret-file-read-guard.sh +12 -0
  573. package/examples/secret-file-write-guard.sh +150 -0
  574. package/examples/self-modify-bypass-guard.sh +0 -0
  575. package/examples/sensitive-file-read-guard.sh +23 -3
  576. package/examples/sensitive-log-guard.sh +1 -0
  577. package/examples/server-side-prompt-injection-detector.sh +69 -0
  578. package/examples/session-agent-cost-limiter.sh +12 -0
  579. package/examples/session-backup-on-start.sh +1 -1
  580. package/examples/session-cost-alert.sh +12 -0
  581. package/examples/session-drift-guard.sh +12 -0
  582. package/examples/session-duration-guard.sh +25 -2
  583. package/examples/session-end-logger.sh +0 -0
  584. package/examples/session-health-monitor.sh +0 -0
  585. package/examples/session-index-repair.sh +1 -1
  586. package/examples/session-memory-watchdog.sh +1 -0
  587. package/examples/session-permission-reset-guard.sh +12 -0
  588. package/examples/session-persistence-verifier.sh +93 -0
  589. package/examples/session-plugin-pin-watch.sh +68 -0
  590. package/examples/session-rate-monitor.sh +176 -0
  591. package/examples/session-resume-env-fix.sh +0 -0
  592. package/examples/session-start-quota-status.sh +193 -0
  593. package/examples/session-start-safety-check.sh +55 -3
  594. package/examples/session-state-saver.sh +0 -0
  595. package/examples/session-summary-stop.sh +1 -1
  596. package/examples/session-summary.sh +0 -0
  597. package/examples/session-token-counter.sh +1 -1
  598. package/examples/settings-hooks-key-restorer.sh +99 -0
  599. package/examples/settings-json-health-check.sh +57 -0
  600. package/examples/settings-json-model-guard.sh +0 -0
  601. package/examples/settings-regression-tester.sh +188 -0
  602. package/examples/shell-config-truncation-guard.sh +12 -0
  603. package/examples/shell-wrapper-guard.sh +12 -0
  604. package/examples/skill-cumulative-size-detector.sh +104 -0
  605. package/examples/skill-description-drop-detector.sh +148 -0
  606. package/examples/skill-gate.sh +0 -0
  607. package/examples/skill-truncation-detector.sh +154 -0
  608. package/examples/skills-context-recorder.sh +167 -0
  609. package/examples/skills-load-verifier.sh +96 -0
  610. package/examples/skills-settings-validator.sh +139 -0
  611. package/examples/sonnet-45-deprecation-helper.sh +85 -0
  612. package/examples/spec-file-scope-guard.sh +12 -0
  613. package/examples/spring-profile-guard.sh +1 -0
  614. package/examples/spurious-malformed-notice-detector.sh +247 -0
  615. package/examples/sql-bulk-delete-warn.sh +88 -0
  616. package/examples/sql-injection-detect.sh +0 -0
  617. package/examples/ssh-key-protect.sh +12 -0
  618. package/examples/staged-secret-scan.sh +22 -1
  619. package/examples/stale-temp-settings-detector.sh +86 -0
  620. package/examples/standing-rule-session-surfacer.sh +137 -0
  621. package/examples/stop-hook-sigterm-wrapper.sh +182 -0
  622. package/examples/stop-transcript-write-health.sh +64 -0
  623. package/examples/strict-allowlist.sh +57 -15
  624. package/examples/strip-coauthored-by.sh +50 -10
  625. package/examples/subagent-blast-radius-guard.sh +184 -0
  626. package/examples/subagent-boundary-precheck.sh +126 -0
  627. package/examples/subagent-budget-guard.sh +12 -0
  628. package/examples/subagent-claudemd-inject.sh +0 -0
  629. package/examples/subagent-closure-verify-gate.sh +164 -0
  630. package/examples/subagent-destructive-git-guard.sh +120 -0
  631. package/examples/subagent-error-detector.sh +0 -0
  632. package/examples/subagent-forged-system-reminder-guard.sh +156 -0
  633. package/examples/subagent-identity-leak-guard.sh +102 -0
  634. package/examples/subagent-identity-reassertion.sh +51 -0
  635. package/examples/subagent-inheritance-tester.sh +179 -0
  636. package/examples/subagent-permission-mode-guard.sh +110 -0
  637. package/examples/subagent-scope-guard.sh +12 -0
  638. package/examples/subagent-spawn-verification-enforcer.sh +102 -0
  639. package/examples/subagent-tool-allowlist-enforcer.sh +101 -0
  640. package/examples/subagent-tool-call-limiter.sh +87 -15
  641. package/examples/subcommand-chain-guard.sh +12 -0
  642. package/examples/subscription-api-billing-warner.sh +160 -0
  643. package/examples/subscription-bypass-detector.sh +152 -0
  644. package/examples/svelte-lint-on-edit.sh +1 -0
  645. package/examples/swift-build-on-edit.sh +1 -1
  646. package/examples/symlink-guard.sh +27 -2
  647. package/examples/symlink-protect.sh +1 -0
  648. package/examples/system-dir-protection-guard.sh +73 -8
  649. package/examples/system-message-workaround.sh +38 -13
  650. package/examples/system-package-guard.sh +12 -0
  651. package/examples/systemd-service-guard.sh +12 -0
  652. package/examples/task-integrity-guard.sh +12 -0
  653. package/examples/temp-file-cleanup.sh +1 -1
  654. package/examples/temporal-suggestion-detector.sh +135 -0
  655. package/examples/terraform-guard.sh +27 -6
  656. package/examples/test-after-edit.sh +1 -1
  657. package/examples/test-before-push.sh +12 -0
  658. package/examples/test-coverage-reminder.sh +24 -1
  659. package/examples/thinking-budget-effort-mismatch-detector.sh +149 -0
  660. package/examples/timezone-guard.sh +0 -0
  661. package/examples/tmp-output-size-guard.sh +0 -0
  662. package/examples/todo-check.sh +1 -1
  663. package/examples/token-budget-guard.sh +12 -0
  664. package/examples/token-budget-per-task.sh +12 -0
  665. package/examples/token-usage-tracker.sh +7 -1
  666. package/examples/tokenizer-ratio-alert-corpus.sh +90 -0
  667. package/examples/tokenizer-ratio-alert.sh +104 -0
  668. package/examples/tool-call-rate-limiter.sh +0 -0
  669. package/examples/tool-result-correlation-checker.sh +167 -0
  670. package/examples/tool-result-size-guard.sh +132 -0
  671. package/examples/tool-retry-budget-guard.sh +12 -0
  672. package/examples/transcript-contamination-detector.sh +138 -0
  673. package/examples/trustfall-mcp-injection-guard.sh +103 -0
  674. package/examples/ultrareview-large-diff-advisor.sh +184 -0
  675. package/examples/unbounded-output-guard.sh +94 -0
  676. package/examples/uncommitted-changes-stop.sh +1 -0
  677. package/examples/uncommitted-discard-guard.sh +23 -5
  678. package/examples/uncommitted-work-guard.sh +12 -0
  679. package/examples/usage-warn.sh +0 -0
  680. package/examples/user-account-guard.sh +12 -0
  681. package/examples/userprompt-submit-receipt.sh +123 -0
  682. package/examples/variable-expansion-guard.sh +12 -0
  683. package/examples/verify-after-publish-reminder.sh +93 -0
  684. package/examples/verify-before-commit.sh +12 -0
  685. package/examples/version-bump-detector.sh +156 -0
  686. package/examples/version-regression-warner.sh +144 -0
  687. package/examples/vue-lint-on-edit.sh +1 -0
  688. package/examples/warn-cron-cost-trap.sh +173 -0
  689. package/examples/webfetch-runaway-guard.sh +89 -0
  690. package/examples/wellness-break-reminder.sh +118 -0
  691. package/examples/windows-destructive-command-guard.sh +177 -0
  692. package/examples/windows-path-guard.sh +12 -0
  693. package/examples/windows-python-stub-detector.sh +63 -0
  694. package/examples/work-hours-guard.sh +12 -0
  695. package/examples/working-directory-fence.sh +12 -0
  696. package/examples/workspace-lease-guard.sh +205 -0
  697. package/examples/worktree-create-log.sh +1 -0
  698. package/examples/worktree-delete-guard.sh +12 -0
  699. package/examples/worktree-edit-boundary-guard.sh +127 -0
  700. package/examples/worktree-escape-write-guard.sh +114 -0
  701. package/examples/worktree-hook-linker.sh +0 -0
  702. package/examples/worktree-hooks-path-fix.sh +111 -0
  703. package/examples/worktree-parent-write-guard.sh +43 -0
  704. package/examples/worktree-project-unify.sh +2 -1
  705. package/examples/worktree-remove-uncommitted-guard.sh +13 -0
  706. package/examples/worktree-unmerged-guard.sh +7 -1
  707. package/examples/write-byte-integrity-verifier.sh +190 -0
  708. package/examples/write-empty-content-guard.sh +67 -0
  709. package/examples/write-nul-corruption-detector.sh +61 -0
  710. package/examples/write-overwrite-confirm.sh +160 -14
  711. package/examples/write-secret-guard.sh +27 -5
  712. package/examples/write-shrink-guard.sh +12 -0
  713. package/examples/write-test-ratio.sh +0 -0
  714. package/examples/wsl-host-disk-space-guard.sh +97 -0
  715. package/examples/xml-format-leak-detector.sh +266 -0
  716. package/examples/yaml-syntax-check.sh +12 -0
  717. package/hooks/hooks.json +6 -6
  718. package/index.mjs +1718 -183
  719. package/mission.md +2 -0
  720. package/package.json +4 -4
  721. package/plugins/credential-guard/.claude-plugin/plugin.json +9 -7
  722. package/plugins/git-protection/.claude-plugin/plugin.json +10 -8
  723. package/plugins/safety-essentials/.claude-plugin/plugin.json +9 -7
  724. package/plugins/token-guard/.claude-plugin/plugin.json +10 -8
  725. package/scripts/agents-md-sync-setup.sh +156 -0
  726. package/scripts/autonomy-blocked-summary.sh +148 -0
  727. package/scripts/build-search-index.py +43 -0
  728. package/scripts/build-sitemap.py +119 -0
  729. package/scripts/cc-doctor.sh +192 -0
  730. package/scripts/check-hook-event-names.py +127 -0
  731. package/scripts/check-misfire-instrument.sh +64 -0
  732. package/scripts/claim-verify-audit.sh +467 -0
  733. package/scripts/cowork-claudemd-helper.sh +136 -0
  734. package/scripts/june-15-deprecated-model-scan.sh +111 -0
  735. package/scripts/measure-heredoc-shapes.sh +67 -0
  736. package/scripts/measure-literal-match-misfire.sh +110 -0
  737. package/scripts/probe-scope-guard-branches.sh +48 -0
  738. package/scripts/probe-skip-line-both-ways.sh +65 -0
  739. package/scripts/receipts-aggregate.py +199 -0
  740. package/scripts/session-forensic-audit.py +360 -0
  741. package/scripts.json +9 -9
  742. package/scripts.json.bak-2026-07-27-find +13 -0
  743. package/skills/safety-setup/SKILL.md +5 -5
  744. package/tests/approve-side-remaining-two.test.sh +129 -0
  745. package/tests/approver-list-granularity.test.sh +145 -0
  746. package/tests/askuserquestion-autonomy-gate.test.sh +181 -0
  747. package/tests/audit-ci-gate.test.sh +87 -0
  748. package/tests/audit-cross-layer-claudemd.test.sh +170 -0
  749. package/tests/audit-fail-open-parse.test.sh +100 -0
  750. package/tests/auto-approve-compound-tail.test.sh +194 -0
  751. package/tests/auto-mode-path-invoked-commands.test.sh +89 -0
  752. package/tests/auto-mode-quoted-targets.test.sh +127 -0
  753. package/tests/auto-mode-unquoted-targets.test.sh +112 -0
  754. package/tests/bash-secret-output-detector-detects.test.sh +102 -0
  755. package/tests/blindspots-cross-layer.test.sh +177 -0
  756. package/tests/block-database-wipe.test.sh +64 -0
  757. package/tests/bundled-short-flags-bypass.test.sh +85 -0
  758. package/tests/cd-git-allow-compound-tail.test.sh +67 -0
  759. package/tests/chown-guard-filesystem-root.test.sh +64 -0
  760. package/tests/ci-workflow-guard-skip-rule.test.sh +115 -0
  761. package/tests/compound-deny-safe-rm-targets.test.sh +105 -0
  762. package/tests/core-scripts-parser-fallback.test.sh +140 -0
  763. package/tests/destructive-guard-bare-glob.test.sh +74 -0
  764. package/tests/destructive-guard-every-operand.test.sh +100 -0
  765. package/tests/destructive-guard-find-deletion.test.sh +114 -0
  766. package/tests/destructive-guard-flag-order.test.sh +106 -0
  767. package/tests/destructive-guard-mentions-vs-invocations.test.sh +107 -0
  768. package/tests/destructive-guard-quoted-target.test.sh +89 -0
  769. package/tests/destructive-guard-separators.test.sh +105 -0
  770. package/tests/destructive-guard-variable-target.test.sh +98 -0
  771. package/tests/destructive-guard-wrapped-command.test.sh +91 -0
  772. package/tests/doctor-hook-disabling-env.test.sh +92 -0
  773. package/tests/doctor-misspelled-settings-keys.test.sh +106 -0
  774. package/tests/example-guards-wrapped-command.test.sh +169 -0
  775. package/tests/file-edit-backup.test.sh +93 -0
  776. package/tests/git-filter-repo-guard.test.sh +60 -0
  777. package/tests/git-global-options-bypass.test.sh +141 -0
  778. package/tests/git-invocation-prefixes-all-guards.test.sh +114 -0
  779. package/tests/git-maintenance-guard.test.sh +83 -0
  780. package/tests/git-pack-temp-cleanup-guard.test.sh +133 -0
  781. package/tests/git-rm-orphan-wipe-guard.test.sh +66 -0
  782. package/tests/git-submodule-guard.test.sh +50 -0
  783. package/tests/grep-pcre-portability.test.sh +155 -0
  784. package/tests/hook-registration-matches-input.test.sh +73 -0
  785. package/tests/hook-trigger-matches-envelope.test.sh +178 -0
  786. package/tests/install-does-not-claim-protection.test.sh +110 -0
  787. package/tests/install-example-standalone-tools.test.sh +139 -0
  788. package/tests/line-continuation-bypass.test.sh +114 -0
  789. package/tests/mcp-config-poisoning-audit.test.sh +106 -0
  790. package/tests/nested-background-agent-guard.test.sh +58 -0
  791. package/tests/no-unqualified-safety-claims.test.sh +114 -0
  792. package/tests/option-eating-grep-patterns.test.sh +130 -0
  793. package/tests/outdated-core-guards.test.sh +134 -0
  794. package/tests/outdated-report.test.sh +155 -0
  795. package/tests/output-secret-mask-warns.test.sh +73 -0
  796. package/tests/plugin-hooks-fail-open-under-sh.test.sh +165 -0
  797. package/tests/plugins-word-order.test.sh +126 -0
  798. package/tests/powershell-remove-item-guard.test.sh +85 -0
  799. package/tests/project-dir-naming.test.sh +103 -0
  800. package/tests/quoted-safe-targets-and-mentions.test.sh +190 -0
  801. package/tests/reroute-after-block-guard.test.sh +104 -0
  802. package/tests/secret-guard-documented-promises.test.sh +97 -0
  803. package/tests/session-plugin-pin-watch.test.sh +45 -0
  804. package/tests/settings-unreadable-refuses-to-write.test.sh +95 -0
  805. package/tests/shield-no-silent-adoption.test.py +110 -0
  806. package/tests/shield-no-silent-adoption.test.sh +46 -0
  807. package/tests/stop-transcript-write-health.test.sh +46 -0
  808. package/tests/system-dir-guard-root-erased.test.sh +66 -0
  809. package/tests/test-account-billing-log.sh +100 -0
  810. package/tests/test-account-routing-preflight.sh +77 -0
  811. package/tests/test-agent-sdk-credit-pool-monitor.sh +157 -0
  812. package/tests/test-agent-view-quota-warn.sh +184 -0
  813. package/tests/test-agents-md-edit-drift-warner.sh +176 -0
  814. package/tests/test-agents-md-loader.sh +204 -0
  815. package/tests/test-agents-md-sync-checker.sh +125 -0
  816. package/tests/test-ai-slop-punctuation-arrest.sh +280 -0
  817. package/tests/test-allowlist.sh +60 -0
  818. package/tests/test-always-allow-pattern-suggester.sh +147 -0
  819. package/tests/test-anthropic-base-url-guard.sh +88 -0
  820. package/tests/test-articulated-scope-capture.sh +139 -0
  821. package/tests/test-attachment-traversal-guard.sh +168 -0
  822. package/tests/test-aup-block-pattern-logger.sh +315 -0
  823. package/tests/test-aup-false-positive-helper.sh +188 -0
  824. package/tests/test-aup-large-tool-output-warner.sh +370 -0
  825. package/tests/test-aup-retry-loop-guard.sh +341 -0
  826. package/tests/test-auth-expiry-reminder.sh +303 -0
  827. package/tests/test-auth-macos-sleep-detector.sh +254 -0
  828. package/tests/test-auth-path-detector.sh +232 -0
  829. package/tests/test-auth-status-checker.sh +233 -0
  830. package/tests/test-authorized-reconfirmation-detector.sh +280 -0
  831. package/tests/test-background-cost-launch-guard.sh +62 -0
  832. package/tests/test-bash-allowlist-secondary-check.sh +143 -0
  833. package/tests/test-bash-fanout-bounded-rewriter.sh +186 -0
  834. package/tests/test-bash-project-fence.sh +127 -0
  835. package/tests/test-bg-permission-prompt-warner.sh +221 -0
  836. package/tests/test-binary-read-detector.sh +173 -0
  837. package/tests/test-broad-prefix-session-trap-warner.sh +167 -0
  838. package/tests/test-bulk-delete-target-resolution.sh +49 -0
  839. package/tests/test-bypass-mode-effective-verifier.sh +179 -0
  840. package/tests/test-cache-creation-drift-detector.sh +244 -0
  841. package/tests/test-cache-residue-detector.sh +429 -0
  842. package/tests/test-cache-tier-logger.sh +59 -0
  843. package/tests/test-cache-ttl-eviction-detector.sh +282 -0
  844. package/tests/test-cch-sentinel-precommit-guard.sh +64 -0
  845. package/tests/test-claim-verify-detector.sh +222 -0
  846. package/tests/test-claim-vs-caveat-checker.sh +134 -0
  847. package/tests/test-claude-code-version-pin-advisor.sh +353 -0
  848. package/tests/test-claude-md-reinjector.sh +173 -0
  849. package/tests/test-claude-md-size-monitor.sh +107 -0
  850. package/tests/test-claude-update-budget-guard.sh +132 -0
  851. package/tests/test-claude-update-smart.sh +91 -0
  852. package/tests/test-claudemd-tool-prohibition.sh +239 -0
  853. package/tests/test-cli-config-pinning-detector.sh +233 -0
  854. package/tests/test-cliff-countdown-advisor.sh +138 -0
  855. package/tests/test-closure-word-verify-gate.sh +158 -0
  856. package/tests/test-commit-all-scope-guard.sh +73 -0
  857. package/tests/test-commitment-carry-forward-arrest.sh +250 -0
  858. package/tests/test-compact-dispatch-watchdog.sh +147 -0
  859. package/tests/test-completion-claim-without-verification-detector.sh +326 -0
  860. package/tests/test-compound-bash-permission-resolver.sh +264 -0
  861. package/tests/test-compound-command-deny-enforcer.sh +67 -0
  862. package/tests/test-context-length-budget.sh +68 -0
  863. package/tests/test-context-usage-drift-alert.sh +52 -22
  864. package/tests/test-cost-incident-self-audit.sh +94 -0
  865. package/tests/test-cowork-claude-md-load-checker.sh +257 -0
  866. package/tests/test-cowork-claudemd-helper.sh +141 -0
  867. package/tests/test-cowork-fuse-staleness-watcher.sh +287 -0
  868. package/tests/test-cowork-hook-absence-warner.sh +230 -0
  869. package/tests/test-cowork-model-picker-advisor.sh +317 -0
  870. package/tests/test-credential-exfil-guard.sh +62 -0
  871. package/tests/test-cron-create-receipt.sh +165 -0
  872. package/tests/test-cross-product-mcp-spawn-detector.sh +112 -0
  873. package/tests/test-daemon-zombie-worker-surfacer.sh +71 -0
  874. package/tests/test-daily-cost-guard.sh +131 -0
  875. package/tests/test-deny-rule-integrity-verifier.sh +288 -0
  876. package/tests/test-deployment-readback-gate.sh +92 -0
  877. package/tests/test-deployment-readback-gh-adapter.sh +163 -0
  878. package/tests/test-disabled-feature-toggle-advisor.sh +78 -0
  879. package/tests/test-dispatch-allowlist-preflight.sh +385 -0
  880. package/tests/test-dispatch-liveness-watchdog.sh +319 -0
  881. package/tests/test-dispatch-receipt.sh +314 -0
  882. package/tests/test-dotenv-anthropic-key-billing-guard.sh +160 -0
  883. package/tests/test-echo-probe-spam-detector.sh +143 -0
  884. package/tests/test-edit-quote-ambiguity-guard.sh +75 -0
  885. package/tests/test-ephemeral-container-detector.sh +251 -0
  886. package/tests/test-evidence-claim-gate.sh +291 -0
  887. package/tests/test-extended-thinking-loop-guard.sh +289 -0
  888. package/tests/test-extended-thinking-resume-warning.sh +240 -0
  889. package/tests/test-extended-thinking-tool-use-mismatch-detector.sh +236 -0
  890. package/tests/test-fabricated-command-detector.sh +296 -0
  891. package/tests/test-false-disk-full-guard.sh +36 -0
  892. package/tests/test-feature-deprecation-detector.sh +117 -0
  893. package/tests/test-forbidden-response-phrase-detector.sh +246 -0
  894. package/tests/test-git-operations-require-approval.sh +53 -0
  895. package/tests/test-git-push-blast-radius-guard.sh +115 -0
  896. package/tests/test-goal-iteration-limit-warner.sh +128 -0
  897. package/tests/test-growthbook-flag-monitor.sh +428 -0
  898. package/tests/test-halt-signal-detector.sh +120 -0
  899. package/tests/test-hook-self-disable-detector.sh +232 -0
  900. package/tests/test-iteration-quality-degradation-detector.sh +244 -0
  901. package/tests/test-long-session-malformed-tool-call-detector.sh +185 -0
  902. package/tests/test-marketplace-plugins.sh +193 -0
  903. package/tests/test-mcp-misdiagnosis-arrest.sh +235 -0
  904. package/tests/test-mcp-orphan-leak-warner.sh +71 -0
  905. package/tests/test-mcp-permissions-ask-warner.sh +207 -0
  906. package/tests/test-mcp-routine-approval-detector.sh +229 -0
  907. package/tests/test-mcp-startup-bloat-detector.sh +141 -0
  908. package/tests/test-mcp-stdio-compatibility-test.sh +112 -0
  909. package/tests/test-memory-chain-audit.sh +59 -0
  910. package/tests/test-memory-orphan-detector.sh +227 -0
  911. package/tests/test-model-swap-suggester.sh +368 -0
  912. package/tests/test-model-version-lock.sh +83 -0
  913. package/tests/test-multi-file-plan-routing-gate.sh +277 -0
  914. package/tests/test-multi-window-auth-drift-detector.sh +262 -0
  915. package/tests/test-nested-spawn-inflight-guard.sh +281 -0
  916. package/tests/test-no-force-flag.sh +49 -0
  917. package/tests/test-non-english-quality-warner.sh +229 -0
  918. package/tests/test-oauth-refresh-monitor.sh +311 -0
  919. package/tests/test-opus-degradation-tracker.sh +274 -0
  920. package/tests/test-opus48-routine-task-warning.sh +201 -0
  921. package/tests/test-opus48-thinking-wedge-advisor.sh +249 -0
  922. package/tests/test-output-token-spike-detector.sh +315 -0
  923. package/tests/test-parallel-batch-size-limiter.sh +228 -0
  924. package/tests/test-parallel-cascade-detector.sh +191 -0
  925. package/tests/test-partial-view-claim-arrest.sh +272 -0
  926. package/tests/test-per-prompt-token-cap.sh +136 -0
  927. package/tests/test-persisted-1m-model-advisor.sh +71 -0
  928. package/tests/test-plugin-hooks-json-bloat-detector.sh +267 -0
  929. package/tests/test-post-edit-disk-verify.sh +294 -0
  930. package/tests/test-pre-bash-sed-line-ending-windows.sh +108 -0
  931. package/tests/test-pre-execution-claim-detector.sh +366 -0
  932. package/tests/test-private-key-backup-and-git-credentials.sh +100 -0
  933. package/tests/test-proxy-capture-suggester.sh +214 -0
  934. package/tests/test-public-artefact-socratic-narrowing.sh +346 -0
  935. package/tests/test-quota-anomaly-detector.sh +321 -0
  936. package/tests/test-quota-reset-audit.sh +81 -0
  937. package/tests/test-read-loop-detector.sh +137 -0
  938. package/tests/test-read-once-guard.sh +85 -0
  939. package/tests/test-receipts-aggregate.sh +143 -0
  940. package/tests/test-redirect-fragment-warner.sh +130 -0
  941. package/tests/test-redundant-read-blocker.sh +155 -0
  942. package/tests/test-refusal-arrest-gate.sh +303 -0
  943. package/tests/test-remote-control-billing-classifier.sh +125 -0
  944. package/tests/test-resume-drift-watcher.sh +65 -0
  945. package/tests/test-rg-replace-flag-detector.sh +175 -0
  946. package/tests/test-rhetorical-verification-prompt-detector.sh +237 -0
  947. package/tests/test-rm-safety-net.sh +115 -0
  948. package/tests/test-route-handler-emptiness-gate.sh +300 -0
  949. package/tests/test-runtime-binary-change-detector.sh +196 -0
  950. package/tests/test-same-command-repeat-detector.sh +165 -0
  951. package/tests/test-same-correction-arrest.sh +165 -0
  952. package/tests/test-scope-expansion-receipt.sh +207 -0
  953. package/tests/test-server-side-prompt-injection-detector.sh +181 -0
  954. package/tests/test-session-persistence-verifier.sh +128 -0
  955. package/tests/test-session-rate-monitor.sh +284 -0
  956. package/tests/test-session-start-quota-status.sh +210 -0
  957. package/tests/test-session-start-safety-check.sh +138 -0
  958. package/tests/test-settings-hooks-key-restorer.sh +257 -0
  959. package/tests/test-settings-json-health-check.sh +70 -0
  960. package/tests/test-settings-regression-tester.sh +197 -0
  961. package/tests/test-skill-cumulative-size-detector.sh +198 -0
  962. package/tests/test-skill-description-drop-detector.sh +236 -0
  963. package/tests/test-skill-truncation-detector.sh +263 -0
  964. package/tests/test-skills-context-recorder.sh +329 -0
  965. package/tests/test-skills-load-verifier.sh +184 -0
  966. package/tests/test-skills-settings-validator.sh +242 -0
  967. package/tests/test-sonnet-45-deprecation-helper.sh +150 -0
  968. package/tests/test-spurious-malformed-notice-detector.sh +290 -0
  969. package/tests/test-sql-bulk-delete-warn.sh +123 -0
  970. package/tests/test-ssh-key-protect.sh +81 -0
  971. package/tests/test-stale-temp-settings-detector.sh +161 -0
  972. package/tests/test-standing-rule-session-surfacer.sh +215 -0
  973. package/tests/test-starter-hooks-manifest.sh +96 -0
  974. package/tests/test-stop-hook-sigterm-wrapper.sh +258 -0
  975. package/tests/test-strict-allowlist.sh +72 -0
  976. package/tests/test-subagent-boundary-precheck.sh +137 -0
  977. package/tests/test-subagent-closure-verify-gate.sh +193 -0
  978. package/tests/test-subagent-destructive-git-guard.sh +86 -0
  979. package/tests/test-subagent-identity-leak-guard.sh +62 -0
  980. package/tests/test-subagent-identity-reassertion.sh +61 -0
  981. package/tests/test-subagent-inheritance-tester.sh +319 -0
  982. package/tests/test-subagent-permission-mode-guard.sh +61 -0
  983. package/tests/test-subagent-spawn-verification-enforcer.sh +60 -0
  984. package/tests/test-subagent-tool-allowlist-enforcer.sh +62 -0
  985. package/tests/test-subagent-tool-call-limiter.sh +97 -0
  986. package/tests/test-subscription-api-billing-warner.sh +138 -0
  987. package/tests/test-subscription-bypass-detector.sh +255 -0
  988. package/tests/test-temporal-suggestion-detector.sh +183 -0
  989. package/tests/test-thinking-budget-effort-mismatch-detector.sh +327 -0
  990. package/tests/test-tokenizer-ratio-alert-corpus.sh +77 -0
  991. package/tests/test-tokenizer-ratio-alert.sh +93 -0
  992. package/tests/test-tool-result-correlation-checker.sh +264 -0
  993. package/tests/test-tool-result-size-guard.sh +121 -0
  994. package/tests/test-transcript-contamination-detector.sh +264 -0
  995. package/tests/test-trigger-header-parsing.sh +151 -0
  996. package/tests/test-ultrareview-large-diff-advisor.sh +192 -0
  997. package/tests/test-userprompt-submit-receipt.sh +181 -0
  998. package/tests/test-verify-after-publish-reminder.sh +184 -0
  999. package/tests/test-version-bump-detector.sh +212 -0
  1000. package/tests/test-version-regression-warner.sh +165 -0
  1001. package/tests/test-warn-cron-cost-trap.sh +82 -0
  1002. package/tests/test-webfetch-runaway-guard.sh +77 -0
  1003. package/tests/test-wellness-break-reminder.sh +138 -0
  1004. package/tests/test-windows-destructive-command-guard.sh +169 -0
  1005. package/tests/test-windows-python-stub-detector.sh +146 -0
  1006. package/tests/test-worktree-edit-boundary-guard.sh +219 -0
  1007. package/tests/test-write-byte-integrity-verifier.sh +192 -0
  1008. package/tests/test-xml-format-leak-detector.sh +299 -0
  1009. package/tests/trustfall-mcp-injection-guard.test.sh +183 -0
  1010. package/tests/workspace-lease-guard.test.sh +144 -0
  1011. package/tests/worktree-escape-write-guard.test.sh +68 -0
  1012. package/tests/worktree-hooks-path-fix.test.sh +181 -0
  1013. package/tests/worktree-parent-write-guard.test.sh +51 -0
  1014. package/tests/write-empty-content-guard.test.sh +39 -0
  1015. package/tests/write-nul-corruption-detector.test.sh +29 -0
  1016. package/tests/write-overwrite-unread-content.test.sh +178 -0
  1017. 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
- const HOOKS_DIR = join(HOME, '.claude', 'hooks');
12
- const SETTINGS_PATH = join(HOME, '.claude', 'settings.json');
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 Install 8 safety hooks (30 sec)
141
- npx cc-safe-setup --shield Maximum safety — one command
142
- npx cc-safe-setup --doctor Diagnose hook problems
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 300+ example hooks
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
- npx cc-safe-setup --quickfix Auto-detect and fix common Claude Code problems
177
- npx cc-safe-setup --stats Block statistics and patterns report
178
- npx cc-safe-setup --export Export hooks config for team sharing
179
- npx cc-safe-setup --import <file> Import hooks from exported config
180
- npx cc-safe-setup --help Show this help
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 · --simulate "cmd" to test · --shield for max safety' + c.reset);
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': 'Remove or warn about Co-Authored-By trailers',
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': 'Ensure hook warnings reach both user and model',
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
- const totalExamples = Object.values(CATEGORIES).reduce((sum, cat) => sum + Object.keys(cat).length, 0);
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
- const triggerMatch = content.match(/^#\s*[Tt][Rr][Ii][Gg][Gg][Ee][Rr]:\s*(\S+)/m);
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
- const t = triggerMatch[1];
1136
- if (['PermissionRequest','PostToolUse','Notification','Stop','SessionStart','PreCompact','SessionEnd','UserPromptSubmit','CwdChanged','FileChanged'].includes(t)) {
1137
- trigger = t;
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 { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch(e) {}
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 claudeMdPaths = ['CLAUDE.md', '.claude/CLAUDE.md', join(CC_DIR, 'CLAUDE.md')];
1271
- const hasClaudeMd = claudeMdPaths.some(p => existsSync(p));
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@latest'
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 && process.platform === 'win32') {
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: 'LOW',
1320
- issue: 'No SessionStart permission fixer — plugin updates may break hook permissions on Windows',
1321
- fix: 'npx cc-safe-setup --install-example hook-permission-fixer'
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
- console.log(c.green + c.bold + ' No risks detected. Your setup looks solid.' + c.reset);
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
- console.log();
1402
- process.exit(score < (parseInt(process.env.CC_AUDIT_THRESHOLD) || 0) ? 1 : 0);
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 log = readFileSync(logPath, 'utf-8');
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 = join(HOME, '.claude', 'hooks');
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 = join(HOME, '.claude', 'hooks');
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
- console.log(c.green + ' No risks detected. Your project looks safe!' + c.reset);
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
- const content = readFileSync(blockLog, 'utf-8');
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 = join(HOME, '.claude', 'hooks');
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
- let settings = {};
3067
- if (existsSync(SETTINGS_PATH)) {
3068
- try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
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
- // Collect all installed hooks
3073
- const hookFiles = existsSync(HOOKS_DIR)
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
- writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
3158
- console.log(c.green + ' ✓' + c.reset + ' settings.json updated');
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
- console.log(c.dim + ' Verify: npx cc-safe-setup --verify' + c.reset);
3197
- console.log(c.dim + ' Status: npx cc-safe-setup --status' + c.reset);
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
- let totalBlocks = 0;
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 matchers based on rule types (NEVER use "" matcher)
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
- const hasBlocks = rules.some(r => r.type === 'block');
5341
- const hasApproves = rules.some(r => r.type === 'approve');
5342
- const hasProtects = rules.some(r => r.type === 'protect');
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
- // Protect rules need Edit|Write matcher
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@latest' + c.reset);
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 Claude Code version (needs hooks support)
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,115 @@ 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
- console.log(' ' + c.dim + 'Next steps:' + c.reset);
5980
- console.log(' ' + c.blue + ' --doctor' + c.reset + ' Verify hooks work');
5981
- console.log(' ' + c.blue + ' --simulate "cmd"' + c.reset + ' Test how hooks react');
5982
- console.log(' ' + c.blue + ' --shield' + c.reset + ' Maximum safety (recommended)');
5983
- console.log(' ' + c.blue + ' --opus47' + c.reset + ' Opus 4.7 crisis protection');
5984
- console.log();
5985
- console.log(' ' + c.dim + 'Free tools:' + c.reset);
5986
- console.log(' ' + c.blue + ' Token Checkup' + c.reset + ' https://yurukusa.github.io/cc-safe-setup/token-checkup.html');
5987
- console.log(' ' + c.blue + ' Token Book' + c.reset + ' https://yurukusa.github.io/cc-safe-setup/token-book.html');
5988
- console.log(' ' + c.dim + ' 28 web tools: https://yurukusa.github.io/cc-safe-setup/hub.html' + c.reset);
5989
- console.log();
5990
- console.log(' ' + c.dim + 'Tokens disappearing too fast?' + c.reset);
5991
- console.log(' ' + c.blue + ' Token Book' + c.reset + ' Cut consumption in half — https://yurukusa.github.io/cc-safe-setup/token-book.html');
5992
- console.log(' ' + c.dim + ' Safety Guide: https://zenn.dev/yurukusa/books/6076c23b1cb18b' + c.reset);
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
+ // 2026-09-25: この面(14日で clone 971回・607人が通る)が案内していた本は、
7478
+ // 日本語の Zenn だけだった。npx を走らせる人の大半は英語圏で、同じ事故の記録の
7479
+ // 英語版が Leanpub に7冊 在るのに1本もリンクが無かった。実測の裏づけ=
7480
+ // 9月の売上 ¥15,173 の100%が Leanpub の束で、Zenn は2か月 ¥0。
7481
+ // 並べ替えの原則=(1)いちばん役に立つ無料の物を先頭に置く
7482
+ // (2)有料は「読む前に判断できる材料」(頁数と無料見本)だけ添えて1段 下げる。
7483
+ // 全文=~/ops/economics/leanpub-who-actually-paid-2026-09-25.md
7484
+ console.log(' ' + c.blue + 'Field Manual (free):' + c.reset + ' this repository\'s documentation laid out as a path');
7485
+ console.log(' ' + c.dim + ' The pre-flight checklist, what each guard actually refuses, how to make one fire on' + c.reset);
7486
+ console.log(' ' + c.dim + ' purpose so you can watch it work, and what to read in the log afterwards.' + c.reset);
7487
+ console.log(' ' + c.dim + ' 124 pages, minimum price zero. Everything in it is also in this repository.' + c.reset);
7488
+ console.log(' ' + c.dim + ' https://leanpub.com/claude-code-safety-field-manual' + c.reset);
7489
+ console.log();
7490
+ console.log(' ' + c.blue + 'Longer write-ups (EN):' + c.reset + ' the incident behind each hook, at length — six more titles');
7491
+ console.log(' ' + c.dim + ' Safety Mastery 66p / Migration Playbook 251p / Token Savings 89p /' + c.reset);
7492
+ console.log(' ' + c.dim + ' AGENTS.md Interop 49p / CLAUDE.md Under Test 86p / Claude Code Safety 68p.' + c.reset);
7493
+ console.log(' ' + c.dim + ' Every one has a free sample you can read before deciding.' + c.reset);
7494
+ console.log(' ' + c.dim + ' https://leanpub.com/b/cc-operators-complete (all seven together, from $39)' + c.reset);
7495
+ console.log();
7496
+ console.log(' ' + c.blue + 'Japanese editions:' + c.reset + ' 13 purchases since March 2026 — https://zenn.dev/yurukusa');
7497
+ console.log(' ' + c.dim + ' Anthropic公式ガイドにない事故防止 (97 chapters, free Chapters 1-3)' + c.reset);
7498
+ console.log(' ' + c.dim + ' https://zenn.dev/yurukusa/books/6076c23b1cb18b' + c.reset);
7499
+ console.log(' ' + c.dim + ' Claude Codeのトークン消費を半分にする (¥2,500, free Chapter 1)' + c.reset);
7500
+ console.log(' ' + c.dim + ' https://zenn.dev/yurukusa/books/token-savings-guide' + c.reset);
7501
+ // 2026-08-13: 12冊の露出と実売を突き合わせたら、いちばん高い ¥3,000 の企業パックは
7502
+ // Qiita 234本のうち0本からしか指されておらず、この面でも名前が出ず「10 more titles」に埋もれていた。
7503
+ // 生涯0件は市場の答えではなく、一度も機会を与えていないだけ。
7504
+ // しかもこの画面は「自分の設定に何の守りが有るか」を見た直後=チームへ配ってよいかを
7505
+ // 判断している人がいる瞬間で、宛先が一致している。
7506
+ // 実測=~/ops/economics/exposure-vs-sales-2026-08-13.md
7507
+ console.log(' ' + c.dim + ' Claude Code チーム/企業導入 安全パック (¥3,000, 9 chapters — for whoever has to sign off on team use)' + c.reset);
7508
+ console.log(' ' + c.dim + ' https://zenn.dev/yurukusa/books/cc-team-safety-pack' + c.reset);
7509
+ console.log(' ' + c.dim + ' 9 more titles at the same place (Skills recipes, 800h log, AGENTS.md interop)' + c.reset);
7510
+ console.log();
7511
+ console.log(' ' + c.blue + 'Recurring track:' + c.reset + ' CC Safety Lab Founder (¥500/mo, Ko-fi, grandfathered)');
7512
+ console.log(' ' + c.dim + ' Monthly digest of new failure clusters + cc-safe-setup hooks shipped that month.' + c.reset);
7513
+ // 2026-08-13: ここは以前「June 2026: … July: …」と、配信していない号を配信済みのように書いていた。
7514
+ // Ko-fi の投稿一覧を実測すると号は6本(04-23/04-24/05-08/05-15/05-22/08-08)で、6月と7月は落としている。
7515
+ // tier の説明側は 2026-08-12 にそう直したのに、いちばん読まれるこの面に古い主張が残っていた。
7516
+ 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);
7517
+ console.log(' ' + c.dim + ' https://yurukusa.github.io/cc-safe-setup/safety-lab.html#en' + c.reset);
7518
+ console.log();
7519
+ // 2026-07-27: Claim-Verify Handbook はいま未公開(商品ページの is_published が false)。
7520
+ // 買えない商品を、導入直後の全利用者へ「live, $19」と案内していたので2行を落とした。
7521
+ // URLは HTTP 200 を返すため、応答の番号だけでは死んでいると分からない。
7522
+ console.log(' ' + c.blue + 'Sister handbook:' + c.reset + ' Sub-Agent Observability Handbook preview (full PDF pending)');
7523
+ console.log(' ' + c.dim + ' Sub-Agent Observability Handbook (7-case sub-agent cluster + 4 defense hooks) — free preview, full PDF pending' + c.reset);
7524
+ console.log(' ' + c.dim + ' https://gist.github.com/yurukusa/bc61438321b369dbd3765e2bd9702a80 (free Chapter 1 preview, EN)' + c.reset);
7525
+ console.log();
7526
+ console.log(' ' + c.dim + 'Feedback:' + c.reset + ' https://github.com/yurukusa/cc-safe-setup/discussions (questions / patterns that worked)');
7527
+ console.log(' ' + c.dim + 'Bug:' + c.reset + ' https://github.com/yurukusa/cc-safe-setup/issues/new/choose (false positives / install issues)');
5993
7528
  console.log();
5994
7529
  }
5995
7530