cc-safe-setup 29.7.0 → 30.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +72 -19
- package/.claude-plugin/plugin.json +1 -1
- package/CONTRIBUTING.md +7 -2
- package/COOKBOOK.md +50 -46
- package/LICENSE +21 -0
- package/MIGRATION.md +8 -8
- package/README.md +419 -512
- package/SAFETY_CHECKLIST.md +15 -7
- package/SERVICES.md +194 -0
- package/SETTINGS_REFERENCE.md +236 -17
- package/SKILL.md +5 -5
- package/TROUBLESHOOTING.md +35 -14
- package/action.yml +12 -4
- package/audit/README.md +84 -0
- package/audit/audit-checklist.md +152 -0
- package/audit/boundary-selftest.sh +85 -0
- package/audit/boundary.sh +123 -0
- package/audit/count-hooks.py +42 -0
- package/audit/find-dead-hooks.sh +82 -0
- package/audit/find-unfirable-rules.sh +142 -0
- package/audit/fire.sh +97 -0
- package/audit/selftest.sh +42 -0
- package/audit/unfirable-selftest.sh +81 -0
- package/audit-web/index.html +9 -9
- package/examples/README.md +54 -17
- package/examples/account-billing-log.sh +98 -0
- package/examples/account-routing-preflight.sh +123 -0
- package/examples/agent-sdk-credit-pool-monitor.sh +145 -0
- package/examples/agent-view-quota-decrement.sh +32 -0
- package/examples/agent-view-quota-warn.sh +94 -0
- package/examples/agents-md-edit-drift-warner.sh +223 -0
- package/examples/agents-md-loader.sh +160 -0
- package/examples/agents-md-sync-checker.sh +102 -0
- package/examples/ai-slop-punctuation-arrest.sh +210 -0
- package/examples/allowlist.sh +70 -10
- package/examples/always-allow-pattern-suggester.sh +125 -0
- package/examples/ansible-vault-guard.sh +2 -1
- package/examples/anthropic-base-url-guard.sh +76 -0
- package/examples/api-busyloop-guard.sh +123 -0
- package/examples/api-endpoint-guard.sh +12 -0
- package/examples/api-key-in-url-guard.sh +17 -1
- package/examples/api-rate-limit-guard.sh +36 -1
- package/examples/api-rate-limit-tracker.sh +12 -0
- package/examples/articulated-scope-capture.sh +101 -0
- package/examples/askuserquestion-autonomy-gate.sh +102 -0
- package/examples/attachment-traversal-guard.sh +118 -0
- package/examples/aup-block-pattern-logger.sh +161 -0
- package/examples/aup-false-positive-helper.sh +124 -0
- package/examples/aup-large-tool-output-warner.sh +282 -0
- package/examples/aup-retry-loop-guard.sh +229 -0
- package/examples/auth-expiry-reminder.sh +157 -0
- package/examples/auth-macos-sleep-detector.sh +185 -0
- package/examples/auth-path-detector.sh +171 -0
- package/examples/auth-status-checker.sh +157 -0
- package/examples/authorized-reconfirmation-detector.sh +195 -0
- package/examples/auto-approve-build.sh +39 -0
- package/examples/auto-approve-cargo.sh +31 -1
- package/examples/auto-approve-docker.sh +35 -0
- package/examples/auto-approve-git-read.sh +37 -14
- package/examples/auto-approve-go.sh +30 -1
- package/examples/auto-approve-gradle.sh +29 -1
- package/examples/auto-approve-make.sh +29 -1
- package/examples/auto-approve-maven.sh +29 -1
- package/examples/auto-approve-python.sh +41 -1
- package/examples/auto-approve-readonly-tools.sh +1 -0
- package/examples/auto-approve-readonly.sh +110 -33
- package/examples/auto-approve-ssh.sh +30 -1
- package/examples/auto-approve-test.sh +44 -0
- package/examples/auto-checkpoint.sh +1 -1
- package/examples/auto-compact-prep.sh +0 -0
- package/examples/auto-git-checkpoint.sh +0 -0
- package/examples/auto-mode-safe-commands.sh +53 -1
- package/examples/auto-mode-safety-enforcer.sh +151 -4
- package/examples/auto-push-session-branch.sh +22 -0
- package/examples/aws-production-guard.sh +13 -1
- package/examples/aws-region-guard.sh +1 -1
- package/examples/background-cost-launch-guard.sh +82 -0
- package/examples/background-task-guard.sh +12 -0
- package/examples/backup-before-refactor.sh +0 -0
- package/examples/banned-command-guard.sh +12 -0
- package/examples/bash-allowlist-secondary-check.sh +157 -0
- package/examples/bash-domain-allowlist.sh +12 -0
- package/examples/bash-fanout-bounded-rewriter.sh +191 -0
- package/examples/bash-heuristic-approver.sh +112 -10
- package/examples/bash-project-fence.sh +194 -0
- package/examples/bash-safety-auto-deny.sh +12 -0
- package/examples/bash-secret-output-detector.sh +44 -3
- package/examples/bash-trace-guard.sh +12 -0
- package/examples/bashrc-safety-check.sh +0 -0
- package/examples/bg-permission-prompt-warner.sh +182 -0
- package/examples/bg-task-cooldown-guard.sh +12 -0
- package/examples/binary-read-detector.sh +135 -0
- package/examples/binary-upload-guard.sh +12 -0
- package/examples/block-database-wipe.sh +35 -10
- package/examples/branch-name-check.sh +8 -3
- package/examples/branch-naming-convention.sh +0 -0
- package/examples/broad-find-guard.sh +12 -0
- package/examples/broad-prefix-session-trap-warner.sh +106 -0
- package/examples/bulk-file-delete-guard.sh +45 -0
- package/examples/bypass-mode-effective-verifier.sh +203 -0
- package/examples/cache-creation-drift-detector.sh +120 -0
- package/examples/cache-residue-detector.sh +195 -0
- package/examples/cache-tier-logger.sh +70 -0
- package/examples/cache-ttl-eviction-detector.sh +149 -0
- package/examples/cargo-publish-guard.sh +14 -1
- package/examples/case-insensitive-path-guard.sh +21 -2
- package/examples/case-sensitive-guard.sh +23 -2
- package/examples/cch-cache-guard.sh +0 -0
- package/examples/cch-sentinel-precommit-guard.sh +47 -0
- package/examples/check-abort-controller.sh +1 -1
- package/examples/check-accessibility.sh +0 -0
- package/examples/check-aria-labels.sh +0 -0
- package/examples/check-before-act-enforcer.sh +12 -0
- package/examples/check-charset-meta.sh +0 -0
- package/examples/check-cookie-flags.sh +0 -0
- package/examples/check-cors-config.sh +0 -0
- package/examples/check-csp-headers.sh +0 -0
- package/examples/check-csrf-protection.sh +0 -0
- package/examples/check-debounce.sh +1 -1
- package/examples/check-dependency-age.sh +0 -0
- package/examples/check-dependency-license.sh +0 -0
- package/examples/check-dockerfile-best-practice.sh +0 -0
- package/examples/check-error-boundaries.sh +0 -0
- package/examples/check-error-class.sh +1 -1
- package/examples/check-error-handling.sh +0 -0
- package/examples/check-error-logging.sh +1 -1
- package/examples/check-error-message.sh +0 -0
- package/examples/check-error-stack.sh +0 -0
- package/examples/check-favicon.sh +0 -0
- package/examples/check-git-hooks-compat.sh +0 -0
- package/examples/check-gitattributes.sh +0 -0
- package/examples/check-https-redirect.sh +0 -0
- package/examples/check-input-validation.sh +0 -0
- package/examples/check-lang-attribute.sh +0 -0
- package/examples/check-npm-scripts-exist.sh +0 -0
- package/examples/check-package-size.sh +0 -0
- package/examples/check-port-availability.sh +0 -0
- package/examples/check-promise-all.sh +1 -1
- package/examples/check-rate-limiting.sh +0 -0
- package/examples/check-return-types.sh +0 -0
- package/examples/check-semantic-versioning.sh +0 -0
- package/examples/check-suspense-fallback.sh +1 -1
- package/examples/check-test-exists.sh +0 -0
- package/examples/check-test-naming.sh +0 -0
- package/examples/check-timeout-cleanup.sh +1 -1
- package/examples/check-tls-version.sh +0 -0
- package/examples/check-unsubscribe.sh +1 -1
- package/examples/check-viewport-meta.sh +0 -0
- package/examples/check-worker-terminate.sh +1 -1
- package/examples/checkpoint-tamper-guard.sh +12 -0
- package/examples/chmod-guard.sh +12 -0
- package/examples/chown-guard.sh +24 -0
- package/examples/ci-workflow-guard.sh +6 -1
- package/examples/claim-verify-detector.sh +109 -0
- package/examples/claim-vs-caveat-checker.sh +77 -0
- package/examples/classifier-fallback-allow.sh +114 -40
- package/examples/claude-cache-gc.sh +1 -0
- package/examples/claude-code-version-pin-advisor.sh +183 -0
- package/examples/claude-md-reinjector.sh +129 -0
- package/examples/claude-md-size-monitor.sh +66 -0
- package/examples/claude-update-budget-guard.sh +123 -0
- package/examples/claude-update-smart.sh +113 -0
- package/examples/claudeignore-enforce-guard.sh +12 -0
- package/examples/claudemd-enforcer.sh +0 -0
- package/examples/claudemd-tool-prohibition.sh +128 -0
- package/examples/clear-command-confirm-guard.sh +32 -15
- package/examples/cli-config-pinning-detector.sh +162 -0
- package/examples/cliff-countdown-advisor.sh +96 -0
- package/examples/clipboard-secret-guard.sh +12 -0
- package/examples/closure-word-verify-gate.sh +150 -0
- package/examples/cloud-cli-guard.sh +39 -2
- package/examples/commit-all-scope-guard.sh +44 -0
- package/examples/commit-message-check.sh +1 -1
- package/examples/commit-message-quality.sh +6 -1
- package/examples/commitment-carry-forward-arrest.sh +361 -0
- package/examples/compact-alert-notification.sh +0 -0
- package/examples/compact-circuit-breaker.sh +72 -0
- package/examples/compact-dispatch-watchdog.sh +149 -0
- package/examples/completion-claim-without-verification-detector.sh +207 -0
- package/examples/composer-guard.sh +12 -0
- package/examples/compound-bash-permission-resolver.sh +156 -0
- package/examples/compound-command-allow.sh +1 -1
- package/examples/compound-command-deny-enforcer.sh +183 -0
- package/examples/compound-inject-guard.sh +12 -0
- package/examples/concurrent-edit-lock.sh +12 -0
- package/examples/conflict-marker-guard.sh +12 -0
- package/examples/consecutive-error-breaker.sh +0 -0
- package/examples/console-log-count.sh +1 -0
- package/examples/context-compact-advisor.sh +29 -2
- package/examples/context-length-budget.sh +69 -0
- package/examples/context-threshold-alert.sh +12 -0
- package/examples/context-usage-drift-alert.sh +41 -13
- package/examples/conversation-history-guard.sh +12 -0
- package/examples/core-file-protect-guard.sh +12 -0
- package/examples/cors-star-warn.sh +0 -0
- package/examples/cost-incident-self-audit.sh +162 -0
- package/examples/cowork-claude-md-load-checker.sh +100 -0
- package/examples/cowork-fuse-staleness-watcher.sh +140 -0
- package/examples/cowork-hook-absence-warner.sh +167 -0
- package/examples/cowork-model-picker-advisor.sh +115 -0
- package/examples/credential-exfil-guard.sh +65 -4
- package/examples/credential-file-cat-guard.sh +17 -1
- package/examples/cron-create-receipt.sh +112 -0
- package/examples/cron-modification-guard.sh +12 -0
- package/examples/cross-product-mcp-spawn-detector.sh +178 -0
- package/examples/cwd-project-boundary-guard.sh +0 -0
- package/examples/daemon-zombie-worker-surfacer.sh +118 -0
- package/examples/daily-cost-guard.sh +164 -0
- package/examples/daily-usage-tracker.sh +0 -0
- package/examples/dangerous-pip-flag-guard.sh +12 -0
- package/examples/db-connect-guard.sh +13 -1
- package/examples/denied-action-retry-guard.sh +12 -0
- package/examples/deny-bypass-detector.sh +12 -0
- package/examples/deny-rule-integrity-verifier.sh +181 -0
- package/examples/dependency-install-guard.sh +12 -0
- package/examples/deploy-guard.sh +14 -2
- package/examples/deploy-path-verify-guard.sh +12 -0
- package/examples/deployment-readback-gate.sh +135 -0
- package/examples/deployment-readback-gh-adapter.sh +247 -0
- package/examples/deployment-verify-guard.sh +25 -2
- package/examples/destructive-db-script-write-guard.sh +166 -0
- package/examples/destructive-migration-write-guard.sh +163 -0
- package/examples/diff-size-guard.sh +12 -0
- package/examples/disabled-feature-toggle-advisor.sh +88 -0
- package/examples/disk-partition-guard.sh +12 -0
- package/examples/dispatch-allowlist-preflight.sh +301 -0
- package/examples/dispatch-liveness-watchdog.sh +216 -0
- package/examples/dispatch-receipt.sh +174 -0
- package/examples/django-migrate-guard.sh +12 -0
- package/examples/dns-config-guard.sh +12 -0
- package/examples/docker-dangerous-guard.sh +12 -0
- package/examples/docker-volume-guard.sh +0 -0
- package/examples/dockerfile-latest-guard.sh +1 -0
- package/examples/dotenv-anthropic-key-billing-guard.sh +99 -0
- package/examples/dotenv-commit-guard.sh +12 -0
- package/examples/dotenv-read-guard.sh +12 -0
- package/examples/dotfile-protection-guard.sh +12 -0
- package/examples/dotnet-build-on-edit.sh +1 -1
- package/examples/drizzle-migrate-guard.sh +27 -2
- package/examples/echo-probe-spam-detector.sh +176 -0
- package/examples/edit-counter-test-gate.sh +12 -0
- package/examples/edit-error-counter.sh +1 -1
- package/examples/edit-guard.sh +12 -0
- package/examples/edit-old-string-validator.sh +12 -0
- package/examples/edit-quote-ambiguity-guard.sh +97 -0
- package/examples/edit-retry-loop-guard.sh +0 -0
- package/examples/edit-verify.sh +0 -0
- package/examples/env-inline-secret-guard.sh +14 -2
- package/examples/env-naming-convention.sh +0 -0
- package/examples/env-prod-guard.sh +0 -0
- package/examples/env-required-check.sh +0 -0
- package/examples/env-source-guard.sh +12 -0
- package/examples/env-var-check.sh +24 -1
- package/examples/ephemeral-container-detector.sh +147 -0
- package/examples/error-memory-guard.sh +12 -0
- package/examples/evidence-claim-gate.sh +187 -0
- package/examples/exploration-budget-guard.sh +89 -0
- package/examples/expo-eject-guard.sh +13 -0
- package/examples/extended-thinking-loop-guard.sh +139 -0
- package/examples/extended-thinking-resume-warning.sh +119 -0
- package/examples/extended-thinking-tool-use-mismatch-detector.sh +212 -0
- package/examples/fabricated-command-detector.sh +145 -0
- package/examples/false-disk-full-guard.sh +63 -0
- package/examples/feature-deprecation-detector.sh +109 -0
- package/examples/file-change-tracker.sh +1 -1
- package/examples/file-change-undo-tracker.sh +0 -0
- package/examples/file-edit-backup.sh +63 -14
- package/examples/file-reference-check.sh +0 -0
- package/examples/file-size-limit.sh +12 -0
- package/examples/financial-operation-guard.sh +12 -0
- package/examples/firewall-guard.sh +12 -0
- package/examples/fish-shell-wrapper.sh +125 -7
- package/examples/five-hundred-milestone.sh +1 -0
- package/examples/flask-static-route-guard.sh +89 -0
- package/examples/forbidden-response-phrase-detector.sh +161 -0
- package/examples/gem-push-guard.sh +13 -0
- package/examples/gh-cli-destructive-guard.sh +13 -1
- package/examples/git-checkout-safety-guard.sh +12 -0
- package/examples/git-checkout-uncommitted-guard.sh +12 -0
- package/examples/git-config-guard.sh +12 -0
- package/examples/git-crypt-worktree-guard.sh +12 -0
- package/examples/git-filter-repo-guard.sh +56 -0
- package/examples/git-history-rewrite-guard.sh +12 -0
- package/examples/git-hook-bypass-guard.sh +0 -0
- package/examples/git-maintenance-guard.sh +83 -0
- package/examples/git-merge-conflict-prevent.sh +0 -0
- package/examples/git-message-length-check.sh +0 -0
- package/examples/git-message-length.sh +0 -0
- package/examples/git-operations-require-approval.sh +68 -14
- package/examples/git-pack-temp-cleanup-guard.sh +102 -0
- package/examples/git-push-blast-radius-guard.sh +218 -0
- package/examples/git-remote-guard.sh +24 -3
- package/examples/git-rm-orphan-wipe-guard.sh +85 -0
- package/examples/git-stash-before-checkout.sh +12 -0
- package/examples/git-stash-before-danger.sh +0 -0
- package/examples/git-submodule-guard.sh +69 -2
- package/examples/git-tag-guard.sh +12 -0
- package/examples/github-actions-secret-guard.sh +0 -0
- package/examples/gitignore-check.sh +0 -0
- package/examples/gitops-drift-guard.sh +12 -0
- package/examples/go-mod-tidy-warn.sh +1 -0
- package/examples/go-vet-after-edit.sh +12 -0
- package/examples/goal-iteration-limit-warner.sh +152 -0
- package/examples/grep-nul-false-negative-guard.sh +30 -0
- package/examples/growthbook-flag-monitor.sh +257 -0
- package/examples/hallucination-url-check.sh +1 -1
- package/examples/halt-signal-detector.sh +135 -0
- package/examples/handoff-on-clear.sh +65 -0
- package/examples/hardcoded-ip-guard.sh +1 -0
- package/examples/headless-empty-result-guard.sh +0 -0
- package/examples/headless-stop-guard.sh +0 -0
- package/examples/helm-install-guard.sh +2 -1
- package/examples/home-critical-bash-guard.sh +17 -1
- package/examples/hook-debug-wrapper.sh +6 -2
- package/examples/hook-permission-fixer.sh +0 -0
- package/examples/hook-self-disable-detector.sh +99 -0
- package/examples/hook-stdout-sanitizer.sh +6 -2
- package/examples/hook-tamper-guard.sh +12 -0
- package/examples/idle-session-cost-alert.sh +0 -0
- package/examples/image-dimension-guard.sh +109 -0
- package/examples/image-file-validator.sh +0 -0
- package/examples/iteration-quality-degradation-detector.sh +170 -0
- package/examples/java-compile-on-edit.sh +1 -1
- package/examples/json-syntax-check.sh +0 -0
- package/examples/k8s-production-guard.sh +13 -1
- package/examples/kill-process-guard.sh +12 -0
- package/examples/kubernetes-guard.sh +13 -1
- package/examples/laravel-artisan-guard.sh +13 -0
- package/examples/large-file-guard.sh +1 -1
- package/examples/log-level-guard.sh +0 -0
- package/examples/log-truncation-guard.sh +12 -0
- package/examples/long-session-malformed-tool-call-detector.sh +183 -0
- package/examples/loop-detector.sh +12 -0
- package/examples/magic-number-warn.sh +1 -0
- package/examples/main-branch-warn.sh +12 -0
- package/examples/markdown-link-check.sh +0 -0
- package/examples/max-concurrent-agents.sh +12 -0
- package/examples/max-edit-size-guard.sh +13 -0
- package/examples/max-file-count-guard.sh +0 -0
- package/examples/max-file-delete-count.sh +0 -0
- package/examples/max-function-length.sh +1 -0
- package/examples/max-import-count.sh +0 -0
- package/examples/max-subagent-count.sh +0 -0
- package/examples/mcp-config-freeze.sh +12 -0
- package/examples/mcp-config-poisoning-audit.sh +150 -0
- package/examples/mcp-data-boundary.sh +1 -1
- package/examples/mcp-misdiagnosis-arrest.sh +209 -0
- package/examples/mcp-orphan-leak-warner.sh +49 -0
- package/examples/mcp-permissions-ask-warner.sh +151 -0
- package/examples/mcp-routine-approval-detector.sh +154 -0
- package/examples/mcp-server-allowlist.sh +12 -0
- package/examples/mcp-server-guard.sh +12 -0
- package/examples/mcp-startup-bloat-detector.sh +87 -0
- package/examples/mcp-stdio-compatibility-test.sh +153 -0
- package/examples/mcp-tool-audit-log.sh +0 -0
- package/examples/mcp-tool-guard.sh +101 -1
- package/examples/mcp-warmup-wait.sh +0 -0
- package/examples/memory-chain-audit.sh +56 -0
- package/examples/memory-orphan-detector.sh +188 -0
- package/examples/memory-write-guard.sh +99 -17
- package/examples/migration-verify-guard.sh +12 -0
- package/examples/model-swap-suggester.sh +177 -0
- package/examples/model-version-alert.sh +5 -1
- package/examples/model-version-lock.sh +63 -0
- package/examples/move-delete-sequence-guard.sh +33 -2
- package/examples/multi-file-plan-routing-gate.sh +192 -0
- package/examples/multi-vendor-concurrent-warner.sh +108 -0
- package/examples/multi-window-auth-drift-detector.sh +183 -0
- package/examples/multiline-command-approver.sh +161 -23
- package/examples/nested-background-agent-guard.sh +109 -0
- package/examples/nested-spawn-inflight-guard.sh +178 -0
- package/examples/network-exfil-guard.sh +12 -0
- package/examples/network-guard.sh +12 -0
- package/examples/network-interface-guard.sh +12 -0
- package/examples/nextjs-env-guard.sh +1 -1
- package/examples/no-absolute-import.sh +0 -0
- package/examples/no-alert-confirm-prompt.sh +0 -0
- package/examples/no-anonymous-default-export.sh +0 -0
- package/examples/no-any-type.sh +0 -0
- package/examples/no-any-typescript.sh +1 -0
- package/examples/no-ask-human.sh +12 -0
- package/examples/no-assignment-in-condition.sh +0 -0
- package/examples/no-base64-exfil.sh +12 -0
- package/examples/no-callback-hell.sh +0 -0
- package/examples/no-circular-dependency.sh +0 -0
- package/examples/no-cleartext-storage.sh +0 -0
- package/examples/no-commented-code.sh +0 -0
- package/examples/no-commit-fixup.sh +0 -0
- package/examples/no-console-assert.sh +0 -0
- package/examples/no-console-error-swallow.sh +0 -0
- package/examples/no-console-in-prod.sh +0 -0
- package/examples/no-console-log-commit.sh +12 -0
- package/examples/no-console-log.sh +0 -0
- package/examples/no-console-time.sh +0 -0
- package/examples/no-cors-wildcard.sh +4 -1
- package/examples/no-curl-upload.sh +0 -0
- package/examples/no-dangerouslySetInnerHTML.sh +0 -0
- package/examples/no-dangling-await.sh +1 -0
- package/examples/no-debug-commit.sh +12 -0
- package/examples/no-debug-in-commit.sh +0 -0
- package/examples/no-deep-nesting.sh +0 -0
- package/examples/no-deep-relative-import.sh +1 -0
- package/examples/no-default-credentials.sh +0 -0
- package/examples/no-deploy-friday.sh +13 -1
- package/examples/no-disabled-test.sh +0 -0
- package/examples/no-document-write.sh +0 -0
- package/examples/no-empty-function.sh +0 -0
- package/examples/no-eval-in-template.sh +0 -0
- package/examples/no-eval-template.sh +3 -1
- package/examples/no-eval.sh +0 -0
- package/examples/no-exec-user-input.sh +0 -0
- package/examples/no-fixme-ship.sh +12 -0
- package/examples/no-floating-promises.sh +0 -0
- package/examples/no-force-flag.sh +22 -5
- package/examples/no-force-install.sh +0 -0
- package/examples/no-git-amend.sh +12 -0
- package/examples/no-git-rebase-public.sh +0 -0
- package/examples/no-global-install.sh +12 -0
- package/examples/no-global-state.sh +0 -0
- package/examples/no-hardcoded-port.sh +1 -0
- package/examples/no-hardcoded-url.sh +0 -0
- package/examples/no-helmet-missing.sh +0 -0
- package/examples/no-http-url.sh +1 -0
- package/examples/no-http-without-https.sh +0 -0
- package/examples/no-inline-style.sh +0 -0
- package/examples/no-inline-styles.sh +1 -0
- package/examples/no-innerhtml.sh +0 -0
- package/examples/no-install-global.sh +12 -0
- package/examples/no-jwt-in-url.sh +0 -0
- package/examples/no-large-commit.sh +0 -0
- package/examples/no-localhost-expose.sh +0 -0
- package/examples/no-magic-number.sh +0 -0
- package/examples/no-md5-sha1.sh +0 -0
- package/examples/no-mixed-line-endings.sh +8 -1
- package/examples/no-mutation-in-reducer.sh +0 -0
- package/examples/no-nested-ternary.sh +0 -0
- package/examples/no-network-exfil.sh +0 -0
- package/examples/no-open-redirect.sh +0 -0
- package/examples/no-output-truncation.sh +12 -0
- package/examples/no-package-downgrade.sh +0 -0
- package/examples/no-package-lock-edit.sh +12 -0
- package/examples/no-path-join-user-input.sh +0 -0
- package/examples/no-port-bind.sh +0 -0
- package/examples/no-process-exit.sh +0 -0
- package/examples/no-prototype-pollution.sh +0 -0
- package/examples/no-push-without-ci.sh +0 -0
- package/examples/no-push-without-tests.sh +12 -0
- package/examples/no-raw-password-in-url.sh +0 -0
- package/examples/no-root-user-docker.sh +1 -0
- package/examples/no-root-write.sh +12 -0
- package/examples/no-secrets-in-args.sh +4 -1
- package/examples/no-secrets-in-logs.sh +0 -0
- package/examples/no-sensitive-log.sh +0 -0
- package/examples/no-sleep-in-hooks.sh +0 -0
- package/examples/no-star-import-python.sh +1 -0
- package/examples/no-string-concat-sql.sh +0 -0
- package/examples/no-sudo-guard.sh +12 -0
- package/examples/no-sync-fs.sh +0 -0
- package/examples/no-todo-in-merge.sh +0 -0
- package/examples/no-todo-in-production.sh +1 -0
- package/examples/no-todo-without-issue.sh +0 -0
- package/examples/no-unused-import.sh +0 -0
- package/examples/no-var-keyword.sh +0 -0
- package/examples/no-verify-blocker.sh +12 -0
- package/examples/no-wget-piped-bash.sh +12 -0
- package/examples/no-wildcard-cors.sh +0 -0
- package/examples/no-wildcard-delete.sh +0 -0
- package/examples/no-wildcard-import.sh +0 -0
- package/examples/no-with-statement.sh +0 -0
- package/examples/no-write-outside-src.sh +0 -0
- package/examples/no-xml-external-entity.sh +0 -0
- package/examples/non-english-quality-warner.sh +156 -0
- package/examples/notify-waiting.sh +1 -1
- package/examples/npm-audit-warn.sh +0 -0
- package/examples/npm-global-install-guard.sh +12 -0
- package/examples/npm-publish-guard.sh +12 -0
- package/examples/npm-script-injection.sh +0 -0
- package/examples/npm-supply-chain-guard.sh +0 -0
- package/examples/nuxt-config-guard.sh +1 -0
- package/examples/oauth-refresh-monitor.sh +210 -0
- package/examples/opus-degradation-tracker.sh +187 -0
- package/examples/opus48-routine-task-warning.sh +196 -0
- package/examples/opus48-thinking-wedge-advisor.sh +143 -0
- package/examples/output-credential-scan.sh +1 -1
- package/examples/output-secret-mask.sh +31 -9
- package/examples/output-token-spike-detector.sh +145 -0
- package/examples/package-json-guard.sh +12 -0
- package/examples/package-lock-frozen.sh +12 -0
- package/examples/parallel-batch-size-limiter.sh +121 -0
- package/examples/parallel-cascade-detector.sh +116 -0
- package/examples/partial-view-claim-arrest.sh +174 -0
- package/examples/path-deny-bash-guard.sh +12 -0
- package/examples/path-traversal-guard.sh +12 -0
- package/examples/per-prompt-token-cap.sh +124 -0
- package/examples/permission-audit-log.sh +1 -1
- package/examples/permission-denial-enforcer.sh +12 -0
- package/examples/permission-entry-validator.sh +0 -0
- package/examples/persisted-1m-model-advisor.sh +88 -0
- package/examples/php-lint-on-edit.sh +1 -1
- package/examples/pii-upload-guard.sh +12 -0
- package/examples/pip-publish-guard.sh +13 -0
- package/examples/pip-requirements-guard.sh +12 -0
- package/examples/pip-venv-required.sh +12 -0
- package/examples/plan-mode-edit-guard.sh +12 -0
- package/examples/plan-mode-enforcer.sh +12 -0
- package/examples/plan-mode-strict-guard.sh +12 -0
- package/examples/plugin-hooks-json-bloat-detector.sh +169 -0
- package/examples/plugin-process-cleanup.sh +0 -0
- package/examples/polyglot-rm-guard.sh +12 -0
- package/examples/post-compact-safety.sh +12 -0
- package/examples/post-edit-disk-verify.sh +152 -0
- package/examples/powershell-remove-item-guard.sh +85 -0
- package/examples/pr-description-check.sh +0 -0
- package/examples/pr-duplicate-guard.sh +13 -0
- package/examples/pre-bash-sed-line-ending-windows.sh +87 -0
- package/examples/pre-compact-knowledge-save.sh +0 -0
- package/examples/pre-compact-transcript-backup.sh +0 -0
- package/examples/pre-execution-claim-detector.sh +179 -0
- package/examples/prefer-builtin-tools.sh +10 -1
- package/examples/prefer-const.sh +0 -0
- package/examples/prefer-dedicated-tools.sh +12 -0
- package/examples/prefer-optional-chaining.sh +0 -0
- package/examples/prisma-migrate-guard.sh +24 -7
- package/examples/production-port-kill-guard.sh +12 -0
- package/examples/prompt-injection-guard.sh +2 -1
- package/examples/prompt-usage-logger.sh +0 -0
- package/examples/protect-claudemd.sh +12 -0
- package/examples/protect-dotfiles.sh +12 -0
- package/examples/proxy-capture-suggester.sh +159 -0
- package/examples/public-artefact-socratic-narrowing.sh +232 -0
- package/examples/public-repo-push-guard.sh +12 -0
- package/examples/push-requires-test-pass-record.sh +0 -0
- package/examples/push-requires-test-pass.sh +12 -0
- package/examples/quota-anomaly-detector.sh +173 -0
- package/examples/quota-reset-audit.sh +58 -0
- package/examples/quoted-flag-approver.sh +99 -10
- package/examples/rails-migration-guard.sh +12 -0
- package/examples/rate-limit-guard.sh +0 -0
- package/examples/read-all-files-enforcer.sh +0 -0
- package/examples/read-budget-guard.sh +12 -0
- package/examples/read-loop-detector.sh +59 -0
- package/examples/read-once-guard.sh +96 -0
- package/examples/read-only-mode.sh +12 -0
- package/examples/readme-exists-check.sh +0 -0
- package/examples/record-read-coverage.sh +73 -0
- package/examples/redirect-fragment-warner.sh +149 -0
- package/examples/redis-flushall-guard.sh +13 -0
- package/examples/redundant-read-blocker.sh +132 -0
- package/examples/refusal-arrest-gate.sh +248 -0
- package/examples/registry-publish-guard.sh +13 -1
- package/examples/remote-control-billing-classifier.sh +135 -0
- package/examples/replace-all-guard.sh +12 -0
- package/examples/repo-visibility-guard.sh +12 -0
- package/examples/reroute-after-block-guard.sh +150 -0
- package/examples/response-budget-guard.sh +0 -0
- package/examples/resume-drift-watcher.sh +57 -0
- package/examples/rg-replace-flag-detector.sh +125 -0
- package/examples/rhetorical-verification-prompt-detector.sh +102 -0
- package/examples/ripgrep-permission-fix.sh +0 -0
- package/examples/rm-safety-net.sh +151 -8
- package/examples/role-tool-guard.sh +12 -0
- package/examples/route-handler-emptiness-gate.sh +324 -0
- package/examples/ruby-lint-on-edit.sh +1 -1
- package/examples/runtime-binary-change-detector.sh +102 -0
- package/examples/same-command-repeat-detector.sh +196 -0
- package/examples/same-correction-arrest.sh +169 -0
- package/examples/sandbox-denywrite-enforcer.sh +78 -0
- package/examples/sandbox-relative-path-audit.sh +83 -25
- package/examples/sandbox-write-verify.sh +12 -0
- package/examples/schema-migration-guard.sh +0 -0
- package/examples/scope-expansion-receipt.sh +178 -0
- package/examples/scope-guard.sh +88 -9
- package/examples/secret-file-read-guard.sh +12 -0
- package/examples/secret-file-write-guard.sh +150 -0
- package/examples/self-modify-bypass-guard.sh +0 -0
- package/examples/sensitive-file-read-guard.sh +23 -3
- package/examples/sensitive-log-guard.sh +1 -0
- package/examples/server-side-prompt-injection-detector.sh +69 -0
- package/examples/session-agent-cost-limiter.sh +12 -0
- package/examples/session-backup-on-start.sh +1 -1
- package/examples/session-cost-alert.sh +12 -0
- package/examples/session-drift-guard.sh +12 -0
- package/examples/session-duration-guard.sh +25 -2
- package/examples/session-end-logger.sh +0 -0
- package/examples/session-health-monitor.sh +0 -0
- package/examples/session-index-repair.sh +1 -1
- package/examples/session-memory-watchdog.sh +1 -0
- package/examples/session-permission-reset-guard.sh +12 -0
- package/examples/session-persistence-verifier.sh +93 -0
- package/examples/session-plugin-pin-watch.sh +68 -0
- package/examples/session-rate-monitor.sh +176 -0
- package/examples/session-resume-env-fix.sh +0 -0
- package/examples/session-start-quota-status.sh +193 -0
- package/examples/session-start-safety-check.sh +55 -3
- package/examples/session-state-saver.sh +0 -0
- package/examples/session-summary-stop.sh +1 -1
- package/examples/session-summary.sh +0 -0
- package/examples/session-token-counter.sh +1 -1
- package/examples/settings-hooks-key-restorer.sh +99 -0
- package/examples/settings-json-health-check.sh +57 -0
- package/examples/settings-json-model-guard.sh +0 -0
- package/examples/settings-regression-tester.sh +188 -0
- package/examples/shell-config-truncation-guard.sh +12 -0
- package/examples/shell-wrapper-guard.sh +12 -0
- package/examples/skill-cumulative-size-detector.sh +104 -0
- package/examples/skill-description-drop-detector.sh +148 -0
- package/examples/skill-gate.sh +0 -0
- package/examples/skill-truncation-detector.sh +154 -0
- package/examples/skills-context-recorder.sh +167 -0
- package/examples/skills-load-verifier.sh +96 -0
- package/examples/skills-settings-validator.sh +139 -0
- package/examples/sonnet-45-deprecation-helper.sh +85 -0
- package/examples/spec-file-scope-guard.sh +12 -0
- package/examples/spring-profile-guard.sh +1 -0
- package/examples/spurious-malformed-notice-detector.sh +247 -0
- package/examples/sql-bulk-delete-warn.sh +88 -0
- package/examples/sql-injection-detect.sh +0 -0
- package/examples/ssh-key-protect.sh +12 -0
- package/examples/staged-secret-scan.sh +22 -1
- package/examples/stale-temp-settings-detector.sh +86 -0
- package/examples/standing-rule-session-surfacer.sh +137 -0
- package/examples/stop-hook-sigterm-wrapper.sh +182 -0
- package/examples/stop-transcript-write-health.sh +64 -0
- package/examples/strict-allowlist.sh +57 -15
- package/examples/strip-coauthored-by.sh +50 -10
- package/examples/subagent-blast-radius-guard.sh +184 -0
- package/examples/subagent-boundary-precheck.sh +126 -0
- package/examples/subagent-budget-guard.sh +12 -0
- package/examples/subagent-claudemd-inject.sh +0 -0
- package/examples/subagent-closure-verify-gate.sh +164 -0
- package/examples/subagent-destructive-git-guard.sh +120 -0
- package/examples/subagent-error-detector.sh +0 -0
- package/examples/subagent-forged-system-reminder-guard.sh +156 -0
- package/examples/subagent-identity-leak-guard.sh +102 -0
- package/examples/subagent-identity-reassertion.sh +51 -0
- package/examples/subagent-inheritance-tester.sh +179 -0
- package/examples/subagent-permission-mode-guard.sh +110 -0
- package/examples/subagent-scope-guard.sh +12 -0
- package/examples/subagent-spawn-verification-enforcer.sh +102 -0
- package/examples/subagent-tool-allowlist-enforcer.sh +101 -0
- package/examples/subagent-tool-call-limiter.sh +87 -15
- package/examples/subcommand-chain-guard.sh +12 -0
- package/examples/subscription-api-billing-warner.sh +160 -0
- package/examples/subscription-bypass-detector.sh +152 -0
- package/examples/svelte-lint-on-edit.sh +1 -0
- package/examples/swift-build-on-edit.sh +1 -1
- package/examples/symlink-guard.sh +27 -2
- package/examples/symlink-protect.sh +1 -0
- package/examples/system-dir-protection-guard.sh +73 -8
- package/examples/system-message-workaround.sh +38 -13
- package/examples/system-package-guard.sh +12 -0
- package/examples/systemd-service-guard.sh +12 -0
- package/examples/task-integrity-guard.sh +12 -0
- package/examples/temp-file-cleanup.sh +1 -1
- package/examples/temporal-suggestion-detector.sh +135 -0
- package/examples/terraform-guard.sh +27 -6
- package/examples/test-after-edit.sh +1 -1
- package/examples/test-before-push.sh +12 -0
- package/examples/test-coverage-reminder.sh +24 -1
- package/examples/thinking-budget-effort-mismatch-detector.sh +149 -0
- package/examples/thinking-stall-detector.sh +61 -0
- package/examples/timezone-guard.sh +0 -0
- package/examples/tmp-output-size-guard.sh +0 -0
- package/examples/todo-check.sh +1 -1
- package/examples/token-budget-guard.sh +12 -0
- package/examples/token-budget-per-task.sh +12 -0
- package/examples/token-usage-tracker.sh +7 -1
- package/examples/tokenizer-ratio-alert-corpus.sh +90 -0
- package/examples/tokenizer-ratio-alert.sh +104 -0
- package/examples/tool-call-rate-limiter.sh +0 -0
- package/examples/tool-result-correlation-checker.sh +167 -0
- package/examples/tool-result-size-guard.sh +132 -0
- package/examples/tool-retry-budget-guard.sh +12 -0
- package/examples/transcript-contamination-detector.sh +138 -0
- package/examples/trustfall-mcp-injection-guard.sh +103 -0
- package/examples/ultrareview-large-diff-advisor.sh +184 -0
- package/examples/unbounded-output-guard.sh +94 -0
- package/examples/uncommitted-changes-stop.sh +1 -0
- package/examples/uncommitted-discard-guard.sh +23 -5
- package/examples/uncommitted-work-guard.sh +12 -0
- package/examples/usage-warn.sh +0 -0
- package/examples/user-account-guard.sh +12 -0
- package/examples/userprompt-submit-receipt.sh +123 -0
- package/examples/variable-expansion-guard.sh +12 -0
- package/examples/verify-after-publish-reminder.sh +93 -0
- package/examples/verify-before-commit.sh +12 -0
- package/examples/version-bump-detector.sh +156 -0
- package/examples/version-regression-warner.sh +144 -0
- package/examples/vue-lint-on-edit.sh +1 -0
- package/examples/warn-cron-cost-trap.sh +173 -0
- package/examples/webfetch-runaway-guard.sh +89 -0
- package/examples/wellness-break-reminder.sh +118 -0
- package/examples/windows-destructive-command-guard.sh +177 -0
- package/examples/windows-path-guard.sh +12 -0
- package/examples/windows-python-stub-detector.sh +63 -0
- package/examples/work-hours-guard.sh +12 -0
- package/examples/working-directory-fence.sh +12 -0
- package/examples/workspace-lease-guard.sh +205 -0
- package/examples/worktree-create-log.sh +1 -0
- package/examples/worktree-delete-guard.sh +12 -0
- package/examples/worktree-edit-boundary-guard.sh +127 -0
- package/examples/worktree-escape-write-guard.sh +114 -0
- package/examples/worktree-hook-linker.sh +0 -0
- package/examples/worktree-hooks-path-fix.sh +111 -0
- package/examples/worktree-parent-write-guard.sh +43 -0
- package/examples/worktree-project-unify.sh +2 -1
- package/examples/worktree-remove-uncommitted-guard.sh +13 -0
- package/examples/worktree-unmerged-guard.sh +7 -1
- package/examples/write-byte-integrity-verifier.sh +190 -0
- package/examples/write-empty-content-guard.sh +67 -0
- package/examples/write-nul-corruption-detector.sh +61 -0
- package/examples/write-overwrite-confirm.sh +160 -14
- package/examples/write-secret-guard.sh +27 -5
- package/examples/write-shrink-guard.sh +12 -0
- package/examples/write-test-ratio.sh +0 -0
- package/examples/wsl-host-disk-space-guard.sh +97 -0
- package/examples/xml-format-leak-detector.sh +266 -0
- package/examples/yaml-syntax-check.sh +12 -0
- package/hooks/hooks.json +6 -6
- package/index.mjs +1699 -183
- package/mission.md +2 -0
- package/package.json +4 -4
- package/plugins/credential-guard/.claude-plugin/plugin.json +9 -7
- package/plugins/git-protection/.claude-plugin/plugin.json +10 -8
- package/plugins/safety-essentials/.claude-plugin/plugin.json +9 -7
- package/plugins/token-guard/.claude-plugin/plugin.json +10 -8
- package/scripts/agents-md-sync-setup.sh +156 -0
- package/scripts/autonomy-blocked-summary.sh +148 -0
- package/scripts/build-search-index.py +43 -0
- package/scripts/build-sitemap.py +119 -0
- package/scripts/cc-doctor.sh +192 -0
- package/scripts/check-hook-event-names.py +127 -0
- package/scripts/check-misfire-instrument.sh +64 -0
- package/scripts/claim-verify-audit.sh +467 -0
- package/scripts/cowork-claudemd-helper.sh +136 -0
- package/scripts/june-15-deprecated-model-scan.sh +111 -0
- package/scripts/measure-heredoc-shapes.sh +67 -0
- package/scripts/measure-literal-match-misfire.sh +110 -0
- package/scripts/probe-scope-guard-branches.sh +48 -0
- package/scripts/probe-skip-line-both-ways.sh +65 -0
- package/scripts/receipts-aggregate.py +199 -0
- package/scripts/session-forensic-audit.py +360 -0
- package/scripts.json +9 -9
- package/scripts.json.bak-2026-07-27-find +13 -0
- package/skills/safety-setup/SKILL.md +5 -5
- package/tests/approve-side-remaining-two.test.sh +129 -0
- package/tests/approver-list-granularity.test.sh +145 -0
- package/tests/askuserquestion-autonomy-gate.test.sh +181 -0
- package/tests/audit-ci-gate.test.sh +87 -0
- package/tests/audit-cross-layer-claudemd.test.sh +170 -0
- package/tests/audit-fail-open-parse.test.sh +100 -0
- package/tests/auto-approve-compound-tail.test.sh +194 -0
- package/tests/auto-mode-path-invoked-commands.test.sh +89 -0
- package/tests/auto-mode-quoted-targets.test.sh +127 -0
- package/tests/auto-mode-unquoted-targets.test.sh +112 -0
- package/tests/bash-secret-output-detector-detects.test.sh +102 -0
- package/tests/blindspots-cross-layer.test.sh +177 -0
- package/tests/block-database-wipe.test.sh +64 -0
- package/tests/bundled-short-flags-bypass.test.sh +85 -0
- package/tests/cd-git-allow-compound-tail.test.sh +67 -0
- package/tests/chown-guard-filesystem-root.test.sh +64 -0
- package/tests/ci-workflow-guard-skip-rule.test.sh +115 -0
- package/tests/compound-deny-safe-rm-targets.test.sh +105 -0
- package/tests/core-scripts-parser-fallback.test.sh +140 -0
- package/tests/destructive-guard-bare-glob.test.sh +74 -0
- package/tests/destructive-guard-every-operand.test.sh +100 -0
- package/tests/destructive-guard-find-deletion.test.sh +114 -0
- package/tests/destructive-guard-flag-order.test.sh +106 -0
- package/tests/destructive-guard-mentions-vs-invocations.test.sh +107 -0
- package/tests/destructive-guard-quoted-target.test.sh +89 -0
- package/tests/destructive-guard-separators.test.sh +105 -0
- package/tests/destructive-guard-variable-target.test.sh +98 -0
- package/tests/destructive-guard-wrapped-command.test.sh +91 -0
- package/tests/doctor-hook-disabling-env.test.sh +92 -0
- package/tests/doctor-misspelled-settings-keys.test.sh +106 -0
- package/tests/example-guards-wrapped-command.test.sh +169 -0
- package/tests/file-edit-backup.test.sh +93 -0
- package/tests/git-filter-repo-guard.test.sh +60 -0
- package/tests/git-global-options-bypass.test.sh +141 -0
- package/tests/git-invocation-prefixes-all-guards.test.sh +114 -0
- package/tests/git-maintenance-guard.test.sh +83 -0
- package/tests/git-pack-temp-cleanup-guard.test.sh +133 -0
- package/tests/git-rm-orphan-wipe-guard.test.sh +66 -0
- package/tests/git-submodule-guard.test.sh +50 -0
- package/tests/grep-pcre-portability.test.sh +155 -0
- package/tests/hook-registration-matches-input.test.sh +73 -0
- package/tests/hook-trigger-matches-envelope.test.sh +178 -0
- package/tests/install-does-not-claim-protection.test.sh +110 -0
- package/tests/install-example-standalone-tools.test.sh +139 -0
- package/tests/line-continuation-bypass.test.sh +114 -0
- package/tests/mcp-config-poisoning-audit.test.sh +106 -0
- package/tests/nested-background-agent-guard.test.sh +58 -0
- package/tests/no-unqualified-safety-claims.test.sh +114 -0
- package/tests/option-eating-grep-patterns.test.sh +130 -0
- package/tests/outdated-core-guards.test.sh +134 -0
- package/tests/outdated-report.test.sh +155 -0
- package/tests/output-secret-mask-warns.test.sh +73 -0
- package/tests/plugin-hooks-fail-open-under-sh.test.sh +165 -0
- package/tests/plugins-word-order.test.sh +126 -0
- package/tests/powershell-remove-item-guard.test.sh +85 -0
- package/tests/project-dir-naming.test.sh +103 -0
- package/tests/quoted-safe-targets-and-mentions.test.sh +190 -0
- package/tests/reroute-after-block-guard.test.sh +104 -0
- package/tests/secret-guard-documented-promises.test.sh +97 -0
- package/tests/session-plugin-pin-watch.test.sh +45 -0
- package/tests/settings-unreadable-refuses-to-write.test.sh +95 -0
- package/tests/shield-no-silent-adoption.test.py +110 -0
- package/tests/shield-no-silent-adoption.test.sh +46 -0
- package/tests/stop-transcript-write-health.test.sh +46 -0
- package/tests/system-dir-guard-root-erased.test.sh +66 -0
- package/tests/test-account-billing-log.sh +100 -0
- package/tests/test-account-routing-preflight.sh +77 -0
- package/tests/test-agent-sdk-credit-pool-monitor.sh +157 -0
- package/tests/test-agent-view-quota-warn.sh +184 -0
- package/tests/test-agents-md-edit-drift-warner.sh +176 -0
- package/tests/test-agents-md-loader.sh +204 -0
- package/tests/test-agents-md-sync-checker.sh +125 -0
- package/tests/test-ai-slop-punctuation-arrest.sh +280 -0
- package/tests/test-allowlist.sh +60 -0
- package/tests/test-always-allow-pattern-suggester.sh +147 -0
- package/tests/test-anthropic-base-url-guard.sh +88 -0
- package/tests/test-articulated-scope-capture.sh +139 -0
- package/tests/test-attachment-traversal-guard.sh +168 -0
- package/tests/test-aup-block-pattern-logger.sh +315 -0
- package/tests/test-aup-false-positive-helper.sh +188 -0
- package/tests/test-aup-large-tool-output-warner.sh +370 -0
- package/tests/test-aup-retry-loop-guard.sh +341 -0
- package/tests/test-auth-expiry-reminder.sh +303 -0
- package/tests/test-auth-macos-sleep-detector.sh +254 -0
- package/tests/test-auth-path-detector.sh +232 -0
- package/tests/test-auth-status-checker.sh +233 -0
- package/tests/test-authorized-reconfirmation-detector.sh +280 -0
- package/tests/test-background-cost-launch-guard.sh +62 -0
- package/tests/test-bash-allowlist-secondary-check.sh +143 -0
- package/tests/test-bash-fanout-bounded-rewriter.sh +186 -0
- package/tests/test-bash-project-fence.sh +127 -0
- package/tests/test-bg-permission-prompt-warner.sh +221 -0
- package/tests/test-binary-read-detector.sh +173 -0
- package/tests/test-broad-prefix-session-trap-warner.sh +167 -0
- package/tests/test-bulk-delete-target-resolution.sh +49 -0
- package/tests/test-bypass-mode-effective-verifier.sh +179 -0
- package/tests/test-cache-creation-drift-detector.sh +244 -0
- package/tests/test-cache-residue-detector.sh +429 -0
- package/tests/test-cache-tier-logger.sh +59 -0
- package/tests/test-cache-ttl-eviction-detector.sh +282 -0
- package/tests/test-cch-sentinel-precommit-guard.sh +64 -0
- package/tests/test-claim-verify-detector.sh +222 -0
- package/tests/test-claim-vs-caveat-checker.sh +134 -0
- package/tests/test-claude-code-version-pin-advisor.sh +353 -0
- package/tests/test-claude-md-reinjector.sh +173 -0
- package/tests/test-claude-md-size-monitor.sh +107 -0
- package/tests/test-claude-update-budget-guard.sh +132 -0
- package/tests/test-claude-update-smart.sh +91 -0
- package/tests/test-claudemd-tool-prohibition.sh +239 -0
- package/tests/test-cli-config-pinning-detector.sh +233 -0
- package/tests/test-cliff-countdown-advisor.sh +138 -0
- package/tests/test-closure-word-verify-gate.sh +158 -0
- package/tests/test-commit-all-scope-guard.sh +73 -0
- package/tests/test-commitment-carry-forward-arrest.sh +250 -0
- package/tests/test-compact-circuit-breaker.sh +134 -0
- package/tests/test-compact-dispatch-watchdog.sh +147 -0
- package/tests/test-completion-claim-without-verification-detector.sh +326 -0
- package/tests/test-compound-bash-permission-resolver.sh +264 -0
- package/tests/test-compound-command-deny-enforcer.sh +67 -0
- package/tests/test-context-length-budget.sh +68 -0
- package/tests/test-context-usage-drift-alert.sh +52 -22
- package/tests/test-cost-incident-self-audit.sh +94 -0
- package/tests/test-cowork-claude-md-load-checker.sh +257 -0
- package/tests/test-cowork-claudemd-helper.sh +141 -0
- package/tests/test-cowork-fuse-staleness-watcher.sh +287 -0
- package/tests/test-cowork-hook-absence-warner.sh +230 -0
- package/tests/test-cowork-model-picker-advisor.sh +317 -0
- package/tests/test-credential-exfil-guard.sh +62 -0
- package/tests/test-cron-create-receipt.sh +165 -0
- package/tests/test-cross-product-mcp-spawn-detector.sh +112 -0
- package/tests/test-daemon-zombie-worker-surfacer.sh +71 -0
- package/tests/test-daily-cost-guard.sh +131 -0
- package/tests/test-deny-rule-integrity-verifier.sh +288 -0
- package/tests/test-deployment-readback-gate.sh +92 -0
- package/tests/test-deployment-readback-gh-adapter.sh +163 -0
- package/tests/test-disabled-feature-toggle-advisor.sh +78 -0
- package/tests/test-dispatch-allowlist-preflight.sh +385 -0
- package/tests/test-dispatch-liveness-watchdog.sh +319 -0
- package/tests/test-dispatch-receipt.sh +314 -0
- package/tests/test-dotenv-anthropic-key-billing-guard.sh +160 -0
- package/tests/test-echo-probe-spam-detector.sh +143 -0
- package/tests/test-edit-quote-ambiguity-guard.sh +75 -0
- package/tests/test-ephemeral-container-detector.sh +251 -0
- package/tests/test-evidence-claim-gate.sh +291 -0
- package/tests/test-exploration-budget-guard.sh +164 -0
- package/tests/test-extended-thinking-loop-guard.sh +289 -0
- package/tests/test-extended-thinking-resume-warning.sh +240 -0
- package/tests/test-extended-thinking-tool-use-mismatch-detector.sh +236 -0
- package/tests/test-fabricated-command-detector.sh +296 -0
- package/tests/test-false-disk-full-guard.sh +36 -0
- package/tests/test-feature-deprecation-detector.sh +117 -0
- package/tests/test-forbidden-response-phrase-detector.sh +246 -0
- package/tests/test-git-operations-require-approval.sh +53 -0
- package/tests/test-git-push-blast-radius-guard.sh +115 -0
- package/tests/test-goal-iteration-limit-warner.sh +128 -0
- package/tests/test-growthbook-flag-monitor.sh +428 -0
- package/tests/test-halt-signal-detector.sh +120 -0
- package/tests/test-hook-self-disable-detector.sh +232 -0
- package/tests/test-iteration-quality-degradation-detector.sh +244 -0
- package/tests/test-long-session-malformed-tool-call-detector.sh +185 -0
- package/tests/test-marketplace-plugins.sh +193 -0
- package/tests/test-mcp-misdiagnosis-arrest.sh +235 -0
- package/tests/test-mcp-orphan-leak-warner.sh +71 -0
- package/tests/test-mcp-permissions-ask-warner.sh +207 -0
- package/tests/test-mcp-routine-approval-detector.sh +229 -0
- package/tests/test-mcp-startup-bloat-detector.sh +141 -0
- package/tests/test-mcp-stdio-compatibility-test.sh +112 -0
- package/tests/test-memory-chain-audit.sh +59 -0
- package/tests/test-memory-orphan-detector.sh +227 -0
- package/tests/test-model-swap-suggester.sh +368 -0
- package/tests/test-model-version-lock.sh +83 -0
- package/tests/test-multi-file-plan-routing-gate.sh +277 -0
- package/tests/test-multi-window-auth-drift-detector.sh +262 -0
- package/tests/test-nested-spawn-inflight-guard.sh +281 -0
- package/tests/test-no-force-flag.sh +49 -0
- package/tests/test-non-english-quality-warner.sh +229 -0
- package/tests/test-oauth-refresh-monitor.sh +311 -0
- package/tests/test-opus-degradation-tracker.sh +274 -0
- package/tests/test-opus48-routine-task-warning.sh +201 -0
- package/tests/test-opus48-thinking-wedge-advisor.sh +249 -0
- package/tests/test-output-token-spike-detector.sh +315 -0
- package/tests/test-parallel-batch-size-limiter.sh +228 -0
- package/tests/test-parallel-cascade-detector.sh +191 -0
- package/tests/test-partial-view-claim-arrest.sh +272 -0
- package/tests/test-per-prompt-token-cap.sh +136 -0
- package/tests/test-persisted-1m-model-advisor.sh +71 -0
- package/tests/test-plugin-hooks-json-bloat-detector.sh +267 -0
- package/tests/test-post-edit-disk-verify.sh +294 -0
- package/tests/test-pre-bash-sed-line-ending-windows.sh +108 -0
- package/tests/test-pre-execution-claim-detector.sh +366 -0
- package/tests/test-private-key-backup-and-git-credentials.sh +100 -0
- package/tests/test-proxy-capture-suggester.sh +214 -0
- package/tests/test-public-artefact-socratic-narrowing.sh +346 -0
- package/tests/test-quota-anomaly-detector.sh +321 -0
- package/tests/test-quota-reset-audit.sh +81 -0
- package/tests/test-read-loop-detector.sh +137 -0
- package/tests/test-read-once-guard.sh +85 -0
- package/tests/test-receipts-aggregate.sh +143 -0
- package/tests/test-redirect-fragment-warner.sh +130 -0
- package/tests/test-redundant-read-blocker.sh +155 -0
- package/tests/test-refusal-arrest-gate.sh +303 -0
- package/tests/test-remote-control-billing-classifier.sh +125 -0
- package/tests/test-resume-drift-watcher.sh +65 -0
- package/tests/test-rg-replace-flag-detector.sh +175 -0
- package/tests/test-rhetorical-verification-prompt-detector.sh +237 -0
- package/tests/test-rm-safety-net.sh +115 -0
- package/tests/test-route-handler-emptiness-gate.sh +300 -0
- package/tests/test-runtime-binary-change-detector.sh +196 -0
- package/tests/test-same-command-repeat-detector.sh +165 -0
- package/tests/test-same-correction-arrest.sh +165 -0
- package/tests/test-scope-expansion-receipt.sh +207 -0
- package/tests/test-server-side-prompt-injection-detector.sh +181 -0
- package/tests/test-session-persistence-verifier.sh +128 -0
- package/tests/test-session-rate-monitor.sh +284 -0
- package/tests/test-session-start-quota-status.sh +210 -0
- package/tests/test-session-start-safety-check.sh +138 -0
- package/tests/test-settings-hooks-key-restorer.sh +257 -0
- package/tests/test-settings-json-health-check.sh +70 -0
- package/tests/test-settings-regression-tester.sh +197 -0
- package/tests/test-skill-cumulative-size-detector.sh +198 -0
- package/tests/test-skill-description-drop-detector.sh +236 -0
- package/tests/test-skill-truncation-detector.sh +263 -0
- package/tests/test-skills-context-recorder.sh +329 -0
- package/tests/test-skills-load-verifier.sh +184 -0
- package/tests/test-skills-settings-validator.sh +242 -0
- package/tests/test-sonnet-45-deprecation-helper.sh +150 -0
- package/tests/test-spurious-malformed-notice-detector.sh +290 -0
- package/tests/test-sql-bulk-delete-warn.sh +123 -0
- package/tests/test-ssh-key-protect.sh +81 -0
- package/tests/test-stale-temp-settings-detector.sh +161 -0
- package/tests/test-standing-rule-session-surfacer.sh +215 -0
- package/tests/test-starter-hooks-manifest.sh +96 -0
- package/tests/test-stop-hook-sigterm-wrapper.sh +258 -0
- package/tests/test-strict-allowlist.sh +72 -0
- package/tests/test-subagent-boundary-precheck.sh +137 -0
- package/tests/test-subagent-closure-verify-gate.sh +193 -0
- package/tests/test-subagent-destructive-git-guard.sh +86 -0
- package/tests/test-subagent-identity-leak-guard.sh +62 -0
- package/tests/test-subagent-identity-reassertion.sh +61 -0
- package/tests/test-subagent-inheritance-tester.sh +319 -0
- package/tests/test-subagent-permission-mode-guard.sh +61 -0
- package/tests/test-subagent-spawn-verification-enforcer.sh +60 -0
- package/tests/test-subagent-tool-allowlist-enforcer.sh +62 -0
- package/tests/test-subagent-tool-call-limiter.sh +97 -0
- package/tests/test-subscription-api-billing-warner.sh +138 -0
- package/tests/test-subscription-bypass-detector.sh +255 -0
- package/tests/test-temporal-suggestion-detector.sh +183 -0
- package/tests/test-thinking-budget-effort-mismatch-detector.sh +327 -0
- package/tests/test-thinking-stall-detector.sh +151 -0
- package/tests/test-tokenizer-ratio-alert-corpus.sh +77 -0
- package/tests/test-tokenizer-ratio-alert.sh +93 -0
- package/tests/test-tool-result-correlation-checker.sh +264 -0
- package/tests/test-tool-result-size-guard.sh +121 -0
- package/tests/test-transcript-contamination-detector.sh +264 -0
- package/tests/test-trigger-header-parsing.sh +151 -0
- package/tests/test-ultrareview-large-diff-advisor.sh +192 -0
- package/tests/test-userprompt-submit-receipt.sh +181 -0
- package/tests/test-verify-after-publish-reminder.sh +184 -0
- package/tests/test-version-bump-detector.sh +212 -0
- package/tests/test-version-regression-warner.sh +165 -0
- package/tests/test-warn-cron-cost-trap.sh +82 -0
- package/tests/test-webfetch-runaway-guard.sh +77 -0
- package/tests/test-wellness-break-reminder.sh +138 -0
- package/tests/test-windows-destructive-command-guard.sh +169 -0
- package/tests/test-windows-python-stub-detector.sh +146 -0
- package/tests/test-worktree-edit-boundary-guard.sh +219 -0
- package/tests/test-write-byte-integrity-verifier.sh +192 -0
- package/tests/test-xml-format-leak-detector.sh +299 -0
- package/tests/trustfall-mcp-injection-guard.test.sh +183 -0
- package/tests/workspace-lease-guard.test.sh +144 -0
- package/tests/worktree-escape-write-guard.test.sh +68 -0
- package/tests/worktree-hooks-path-fix.test.sh +181 -0
- package/tests/worktree-parent-write-guard.test.sh +51 -0
- package/tests/write-empty-content-guard.test.sh +39 -0
- package/tests/write-nul-corruption-detector.test.sh +29 -0
- package/tests/write-overwrite-unread-content.test.sh +178 -0
- package/tests/wsl-host-disk-space-guard.test.sh +55 -0
package/scripts.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"destructive-guard": "#!/bin/bash\n# ================================================================\n# destructive-guard.sh — Destructive Command Blocker\n# ================================================================\n# PURPOSE:\n# Blocks dangerous shell commands that can cause irreversible damage.\n# Catches rm -rf on sensitive paths, git reset --hard, git clean -fd,\n# and other destructive operations before they execute.\n#\n# Built after a real incident where rm -rf on a pnpm project\n# followed NTFS junctions and deleted an entire C:\\Users directory.\n# (GitHub Issue #36339)\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# WHAT IT BLOCKS (exit 2):\n# - rm -rf / rm -r on root, home, or parent paths (/, ~, .., /home, /etc)\n# - git reset --hard\n# - git clean -fd / git clean -fdx\n# - chmod -R 777 on sensitive paths\n# - find ... -delete on broad patterns\n#\n# WHAT IT ALLOWS (exit 0):\n# - rm -rf on specific project subdirectories (node_modules, dist, build)\n# - git reset --soft, git reset HEAD\n# - All non-destructive commands\n#\n# CONFIGURATION:\n# CC_ALLOW_DESTRUCTIVE=1 — disable this guard (not recommended)\n# CC_SAFE_DELETE_DIRS — colon-separated list of safe-to-delete dirs\n# default: \"node_modules:dist:build:.cache:__pycache__:coverage\"\n#\n# NOTE: On Windows/WSL2, rm -rf can follow NTFS junctions (symlinks)\n# and delete far more than intended. This guard is especially critical\n# on WSL2 environments.\n# ================================================================\n\nINPUT=$(cat)\nCOMMAND=$(printf '%s' \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null)\n\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# Allow override (not recommended)\nif [[ \"${CC_ALLOW_DESTRUCTIVE:-0}\" == \"1\" ]]; then\n exit 0\nfi\n\n# Log function — records blocked commands for audit\nlog_block() {\n local reason=\"$1\"\n local logfile=\"${CC_BLOCK_LOG:-$HOME/.claude/blocked-commands.log}\"\n mkdir -p \"$(dirname \"$logfile\")\" 2>/dev/null\n echo \"[$(date -Iseconds)] BLOCKED: $reason | cmd: $COMMAND\" >> \"$logfile\" 2>/dev/null\n}\n\n# Safe directories that can be deleted\nSAFE_DIRS=\"${CC_SAFE_DELETE_DIRS:-node_modules:dist:build:.cache:__pycache__:coverage:.next:.nuxt:tmp}\"\n\n# --- Check 0: --no-preserve-root ---\nif printf '%s' \"$COMMAND\" | grep -qE \"rm\\\\s.*\\\\-\\\\-no-preserve-root\"; then\n echo \"BLOCKED: --no-preserve-root detected.\" >&2\n exit 2\nfi\n\n# --- Check 1: rm -rf on dangerous paths ---\nif printf '%s' \"$COMMAND\" | grep -qE 'rm\\s+(-[rf]+\\s+)*(\\/$|\\/\\s|\\/[^a-z]|\\/home|\\/etc|\\/usr|\\/var|\\/mnt|~\\/|~\\s*$|\\.\\.\\/|\\.\\.\\s*$|\\.\\s*$|\\.\\/\\s*$)'; then\n # Exception: safe directories\n SAFE=0\n IFS=':' read -ra DIRS <<< \"$SAFE_DIRS\"\n for dir in \"${DIRS[@]}\"; do\n if printf '%s' \"$COMMAND\" | grep -qE \"rm\\s+.*${dir}\\s*$|rm\\s+.*${dir}/\"; then\n SAFE=1\n break\n fi\n done\n\n # Check for mounted filesystems inside the target (NFS, Docker, bind mounts)\n # Why: GitHub #36640 — rm -rf on a dir with NFS mount deleted production data\n if (( SAFE == 0 )); then\n # Extract the target path from the rm command\n TARGET_PATH=$(printf '%s' \"$COMMAND\" | grep -oP 'rm\\s+(-[rf]+\\s+)*\\K\\S+')\n if [ -n \"$TARGET_PATH\" ] && command -v findmnt &>/dev/null; then\n if findmnt -n -o TARGET --submounts \"$TARGET_PATH\" 2>/dev/null | grep -q .; then\n log_block \"rm on path with mounted filesystem\"\n echo \"BLOCKED: Target contains a mounted filesystem (NFS, Docker, bind).\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Unmount the filesystem first, then retry.\" >&2\n exit 2\n fi\n fi\n fi\n\n if (( SAFE == 0 )); then\n log_block \"rm on sensitive path\"\n echo \"BLOCKED: rm on sensitive path detected.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"This command targets a sensitive directory that could cause\" >&2\n echo \"irreversible data loss. On WSL2, rm -rf can follow NTFS\" >&2\n echo \"junctions and delete far beyond the target directory.\" >&2\n echo \"\" >&2\n echo \"If you need to delete a specific subdirectory, target it directly:\" >&2\n echo \" rm -rf ./specific-folder\" >&2\n exit 2\n fi\nfi\n\n# --- Check 2: git reset --hard ---\n# Only match when git is the actual command, not inside strings/arguments\nif printf '%s' \"$COMMAND\" | grep -qE '^\\s*git\\s+reset\\s+--hard|;\\s*git\\s+reset\\s+--hard|&&\\s*git\\s+reset\\s+--hard|\\|\\|\\s*git\\s+reset\\s+--hard'; then\n log_block \"git reset --hard\"\n echo \"BLOCKED: git reset --hard discards all uncommitted changes.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: git stash, or git reset --soft to keep changes staged.\" >&2\n exit 2\nfi\n\n# --- Check 3: git clean -fd ---\nif printf '%s' \"$COMMAND\" | grep -qE '^\\s*git\\s+clean\\s+-[a-z]*[fd]|;\\s*git\\s+clean|&&\\s*git\\s+clean|\\|\\|\\s*git\\s+clean'; then\n log_block \"git clean\"\n echo \"BLOCKED: git clean removes untracked files permanently.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: git clean -n (dry run) first to see what would be deleted.\" >&2\n exit 2\nfi\n\n# --- Check 4: chmod 777 on broad paths ---\nif printf '%s' \"$COMMAND\" | grep -qE 'chmod\\s+(-R\\s+)?777\\s+(\\/|~|\\.)'; then\n echo \"BLOCKED: chmod 777 on broad path is a security risk.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n exit 2\nfi\n\n# --- Check 5: find -delete on broad patterns ---\nif printf '%s' \"$COMMAND\" | grep -qE 'find\\s+(\\/|~|\\.\\.)\\s.*-delete'; then\n echo \"BLOCKED: find -delete on broad path risks mass deletion.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: find ... -print first to verify what matches.\" >&2\n exit 2\nfi\n\n# --- Check 6: sudo with dangerous commands ---\nif printf '%s' \"$COMMAND\" | grep -qE '^\\s*sudo\\s+(rm\\s+-[rf]|chmod\\s+(-R\\s+)?777|dd\\s+if=|mkfs)'; then\n log_block \"sudo with dangerous command\"\n echo \"BLOCKED: sudo with dangerous command detected.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Running destructive commands with sudo amplifies the damage.\" >&2\n echo \"Review the command carefully before proceeding.\" >&2\n exit 2\nfi\n\n\n# --- Check 7: PowerShell Remove-Item (Windows/WSL2) ---\n# Real incident: GitHub #37331 — destroyed entire repo\n# Skip if command is git commit (message text triggers false positive)\nif printf '%s' \"$COMMAND\" | grep -qE '^\\s*(git\\s+commit|echo\\s|printf\\s|cat\\s)'; then\n : # string output commands mentioning PS commands are not destructive\nelif printf '%s' \"$COMMAND\" | grep -qiE 'Remove-Item.*-Recurse.*-Force|Remove-Item.*-Force.*-Recurse|del\\s+/s\\s+/q|rd\\s+/s\\s+/q|rmdir\\s+/s\\s+/q'; then\n log_block \"PowerShell destructive command\"\n echo \"BLOCKED: Destructive PowerShell command detected.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Remove-Item with recursive force-delete can destroy entire directories\" >&2\n echo \"irreversibly. Target specific files instead.\" >&2\n exit 2\nfi\nif printf '%s' \"$COMMAND\" | grep -qE '(^|;|&&|\\|\\|)\\s*git\\s+(checkout|switch)\\s+.*(--force\\b|-f\\b|--discard-changes\\b)'; then\n log_block \"git checkout/switch --force\"\n echo \"BLOCKED: git checkout/switch with --force discards uncommitted changes.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: git stash before switching, or use git switch without --force.\" >&2\n exit 2\nfi\n\nif printf '%s' \"$COMMAND\" | grep -qE '(sh|bash|zsh)\\s+-c\\s+'; then\n INNER=$(printf '%s' \"$COMMAND\" | sed -E \"s/.*(sh|bash|zsh)\\s+-c\\s+['\\\"]//\" | sed \"s/['\\\"]*$//\" )\n if echo \"$INNER\" | grep -qE 'rm\\s+-[rf]*\\s+[/~]|git\\s+reset\\s+--hard|git\\s+clean\\s+-[fd]+|mkfs\\.|dd\\s+if='; then\n echo \"BLOCKED: Destructive command hidden in shell wrapper\" >&2\n echo \"\" >&2\n echo \"Detected: $INNER\" >&2\n exit 2\n fi\nfi\nif printf '%s' \"$COMMAND\" | grep -qE '\\|\\s*(sh|bash)\\s*$'; then\n if printf '%s' \"$COMMAND\" | grep -qE 'rm\\s+-[rf]*\\s+[/~]|git\\s+reset\\s+--hard|git\\s+clean\\s+-[fd]+'; then\n echo \"BLOCKED: Destructive command piped to shell\" >&2\n exit 2\n fi\nfi\nexit 0",
|
|
3
|
-
"branch-guard": "#!/bin/bash\n# ================================================================\n# branch-guard.sh — Branch Push Protector\n# ================================================================\n# PURPOSE:\n# Prevents accidental git push to main/master branches AND\n# blocks force-push on ALL branches without explicit approval.\n#\n# Force-pushes rewrite history and can destroy teammates' work.\n# Protected branch pushes bypass code review.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# WHAT IT BLOCKS (exit 2):\n# - git push origin main/master (any protected branch)\n# - git push --force (any branch — history rewriting)\n# - git push -f (short flag variant)\n# - git push --force-with-lease (still destructive)\n#\n# WHAT IT ALLOWS (exit 0):\n# - git push origin feature-branch (non-force)\n# - git push -u origin feature-branch\n# - All other git commands\n# - All non-git commands\n#\n# CONFIGURATION:\n# CC_PROTECT_BRANCHES — colon-separated list of protected branches\n# default: \"main:master\"\n# CC_ALLOW_FORCE_PUSH=1 — disable force-push protection\n# ================================================================\n\nINPUT=$(cat)\
|
|
4
|
-
"syntax-check": "#!/bin/bash\n# ================================================================\n# syntax-check.sh — Automatic Syntax Validation After Edits\n# ================================================================\n# PURPOSE:\n# Runs syntax checks immediately after Claude Code edits or\n# writes a file. Catches syntax errors before they propagate\n# into downstream failures.\n#\n# SUPPORTED LANGUAGES:\n# .py — python -m py_compile\n# .sh — bash -n\n# .bash — bash -n\n# .json — jq empty\n# .yaml — python3 yaml.safe_load (if PyYAML installed)\n# .yml — python3 yaml.safe_load (if PyYAML installed)\n# .js — node --check (if node installed)\n# .ts — npx tsc --noEmit (if tsc available) [EXPERIMENTAL]\n#\n# TRIGGER: PostToolUse\n# MATCHER: \"Edit|Write\"\n#\n# DESIGN PHILOSOPHY:\n# -
|
|
2
|
+
"destructive-guard": "#!/bin/bash\n# ================================================================\n# destructive-guard.sh — Destructive Command Blocker\n# ================================================================\n# PURPOSE:\n# Blocks dangerous shell commands that can cause irreversible damage.\n# Catches rm -rf on sensitive paths, git reset --hard, git clean -fd,\n# and other destructive operations before they execute.\n#\n# Built after a real incident where rm -rf on a pnpm project\n# followed NTFS junctions and deleted an entire C:\\Users directory.\n# (GitHub Issue #36339)\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# WHAT IT BLOCKS (exit 2):\n# - rm -rf / rm -r on root, home, or parent paths (/, ~, .., /home, /etc)\n# - git reset --hard\n# - git clean -fd / git clean -fdx\n# - chmod -R 777 on sensitive paths\n# - find ... -delete on broad patterns\n#\n# WHAT IT ALLOWS (exit 0):\n# - rm -rf on specific project subdirectories (node_modules, dist, build)\n# - git reset --soft, git reset HEAD\n# - All non-destructive commands\n#\n# CONFIGURATION:\n# CC_ALLOW_DESTRUCTIVE=1 — disable this guard (not recommended)\n# CC_SAFE_DELETE_DIRS — colon-separated list of safe-to-delete dirs\n# default: \"node_modules:dist:build:.cache:__pycache__:coverage\"\n#\n# NOTE: On Windows/WSL2, rm -rf can follow NTFS junctions (symlinks)\n# and delete far more than intended. This guard is especially critical\n# on WSL2 environments.\n# ================================================================\n\nINPUT=$(cat)\n\n# --- Reading the hook payload -------------------------------------------------\n# Why a chain instead of jq alone: on a machine without jq the read returned an\n# empty string, every check below was skipped, and this hook exited 0 in silence.\n# It stayed listed in settings.json and protected nothing. Measured 2026-07-27:\n# a destructive command passed with exit 0 and no output at all.\n# If none of the three readers exist we warn once and allow, because blocking\n# every Bash call would make Claude Code unusable — stopping all work is a\n# failure, not safety.\ncc_read_field() {\n if command -v jq >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | jq -r \".$1 // empty\" 2>/dev/null\n elif command -v python3 >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" python3 -c 'import sys,json,os\ntry:\n d=json.load(sys.stdin)\nexcept Exception:\n sys.exit(0)\nfor k in os.environ.get(\"CC_FIELD\",\"\").split(\".\"):\n d = d.get(k) if isinstance(d,dict) else None\nsys.stdout.write(\"\" if d is None else str(d))\n' 2>/dev/null\n elif command -v node >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" node -e 'let s=\"\";process.stdin.on(\"data\",d=>s+=d).on(\"end\",()=>{try{let o=JSON.parse(s);for(const k of (process.env.CC_FIELD||\"\").split(\".\"))o=(o&&typeof o===\"object\")?o[k]:undefined;process.stdout.write(o==null?\"\":String(o))}catch(e){}})' 2>/dev/null\n else\n printf '%s' '__CC_NO_PARSER__'\n fi\n}\n\ncc_warn_no_parser() {\n marker=\"${TMPDIR:-/tmp}/cc-noparser-warned-destructive-guard-$PPID\"\n [ -f \"$marker\" ] && return 0\n : > \"$marker\" 2>/dev/null\n echo \"WARNING [destructive-guard]: jq, python3 and node are all unavailable, so this hook cannot read the tool call and is NOT protecting you. Install any one of them.\" >&2\n}\n\nCOMMAND=$(cc_read_field tool_input.command)\n\n# 包みを剥いだ再検査の子として呼ばれた時は、渡された命令をそのまま判定する\n# (下の Check 0z を参照)。子は標準入力を読まないので、ここで受け取る。\nif [ -n \"${CC_DG_UNWRAPPED_CMD:-}\" ]; then\n COMMAND=\"$CC_DG_UNWRAPPED_CMD\"\nfi\n\n# 代入を解決した再検査の子として呼ばれた時も、渡された命令をそのまま判定する\n# (下の Check 0x を参照)。包みを剥ぐ側とは別の変数にしてあるのは、子でも\n# Check 0y (危険な動詞が実際に呼ばれているか) を走らせたいため。ここを共用にすると\n# echo \"rm -rf $D\" のような「言及」まで止まる誤検知が出る。\nif [ -n \"${CC_DG_SUBSTITUTED_CMD:-}\" ]; then\n COMMAND=\"$CC_DG_SUBSTITUTED_CMD\"\nfi\n\nif [ \"$COMMAND\" = \"__CC_NO_PARSER__\" ]; then\n\n cc_warn_no_parser\n\n exit 0\n\nfi\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# --- a trailing backslash makes the next line part of the same command -------\n# Added 2026-09-03. The shell joins `\\` + newline before it runs anything, so\n# `rm -rf \\` / ` ~/Documents` is one deletion. grep is line-oriented and saw\n# two unrelated lines, matched neither, and this hook exited 0. Measured against\n# the shipped copy: the rm, the chmod 777 and every git check could all be\n# stepped over this way. 0.9% of 43,858 real Bash calls in this operator's own\n# history use a line continuation, so this is ordinary typing, not an exploit.\n# Join them once, up front, and every check below sees what the shell will run.\ncc_join_continuations() {\n local s=$1\n s=${s//\\\\$'\\n'/ }\n printf '%s' \"$s\"\n}\nCOMMAND=$(cc_join_continuations \"$COMMAND\")\n\n# --- git's global options sit between `git` and the verb ----------------------\n# Added 2026-09-03. Every git check in this hook expected the verb to be next to\n# the word `git`, so `git -C /repo push --force` matched none of them and the\n# hook exited 0 -- an affirmative approval. Measured against the shipped copy on\n# 2026-09-03: 4 bypasses here, 2 in secret-guard, 4 in destructive-guard. git\n# accepts these options anywhere before the verb and any number of times, so\n# strip them once and let the patterns below stay readable.\n# The messages still print $COMMAND, because the operator needs to see what they\n# actually typed, not our normalised copy.\ncc_strip_git_globals() {\n if ! command -v sed >/dev/null 2>&1; then printf '%s' \"$1\"; return; fi\n printf '%s' \"$1\" | sed -E ':a; s/(^|[;&|[:space:]])git[[:space:]]+((-C|-c)[[:space:]]+[^[:space:]]+|--(git-dir|work-tree|namespace|exec-path|super-prefix|config-env|attr-source)(=[^[:space:]]+|[[:space:]]+[^[:space:]]+)|--(no-pager|paginate|bare|literal-pathspecs|glob-pathspecs|noglob-pathspecs|icase-pathspecs|no-replace-objects|no-optional-locks|no-lazy-fetch|no-advice)|-[pP])[[:space:]]+/\\1git /g; ta'\n}\n\n# 2026-09-14: the git checks below anchored on `(^|;|&+|\\|\\|)\\s*git`, so `git` had\n# to be the first word of a segment. Measured against the shipped copy, eight real\n# invocation forms walked past every one of them:\n# /usr/bin/git reset --hard ./git reset --hard sudo git reset --hard\n# env FOO=1 git ... FOO=1 git ... time git ... nohup git ...\n# /usr/bin/git clean -fd\n# Check 0y above already counts every one of those as an invocation, so the\n# pre-filter was right and the checks it feeds were narrower than it. CC_CMDPOS is\n# that same prefix, kept in one place so the two cannot drift apart again.\nCC_CMDPOS='(^|[;&|`(){}]|\\$\\(|[[:space:]](then|do|else|elif)[[:space:]])[[:space:]]*([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+|(env|nice|ionice|timeout|exec|command|builtin|stdbuf|nohup|sudo|time|xargs)([[:space:]]+-[^[:space:]]+)*([[:space:]]+[0-9]+)?([[:space:]]+-[^[:space:]]+)*[[:space:]]+)*([./]*[A-Za-z0-9_./-]*/)?'\nCHECK_CMD=$(cc_strip_git_globals \"$COMMAND\")\n# Never let a failed normalisation turn every check below into a silent pass.\n[ -z \"$CHECK_CMD\" ] && CHECK_CMD=\"$COMMAND\"\n\n# Allow override (not recommended)\nif [[ \"${CC_ALLOW_DESTRUCTIVE:-0}\" == \"1\" ]]; then\n exit 0\nfi\n\n# Log function — records blocked commands for audit\nlog_block() {\n local reason=\"$1\"\n local logfile=\"${CC_BLOCK_LOG:-$HOME/.claude/blocked-commands.log}\"\n mkdir -p \"$(dirname \"$logfile\")\" 2>/dev/null\n echo \"[$(date -Iseconds)] BLOCKED: $reason | cmd: $COMMAND\" >> \"$logfile\" 2>/dev/null\n}\n\n# Safe directories that can be deleted\nSAFE_DIRS=\"${CC_SAFE_DELETE_DIRS:-node_modules:dist:build:.cache:__pycache__:coverage:.next:.nuxt:tmp}\"\n\n# --- Check 0x: resolve assignments made in the same command string ------------\n# 行き先を変数に置くのはシェルでは普通の書き方であって、回避の手口ではない。\n# 引用符の件 (PR #961) と同じ構図で、丁寧に書く人ほど守られていなかった。\n# 2026-08-06 に配信中の版を対照つきで測った結果、次の3つが素通りしていた:\n#\n# D=/etc; rm -rf $D\n# D=/; rm -rf \"$D\"\n# CMD=\"rm -rf /\"; $CMD\n#\n# ここでも新しい判定は足さない。判定の前に代入を解決して、この同じスクリプトを\n# もう一度走らせる (Check 0z と同じ作り)。D=./build のような安全な値は展開しても\n# 安全なままなので、既存の判定がそのまま通す = 誤検知は増えない。\n# 追えるのは同じコマンド文字列の中で完結している代入だけで、実行時にしか値が\n# 決まらないもの (親から渡る環境変数) は静的に追えない。そこは穴として残る。\nif [ -z \"${CC_DG_UNWRAPPED_CMD:-}\" ] && [ -z \"${CC_DG_SUBSTITUTED_CMD:-}\" ]; then\n CC_DG_SUBST=\"$COMMAND\"\n while IFS= read -r cc_dg_assign; do\n [ -z \"$cc_dg_assign\" ] && continue\n cc_dg_name=\"${cc_dg_assign%%=*}\"\n cc_dg_value=\"${cc_dg_assign#*=}\"\n # 先頭と末尾の引用符だけ落とす。値の中の引用符は触らない\n cc_dg_value=\"${cc_dg_value#\\\"}\"; cc_dg_value=\"${cc_dg_value%\\\"}\"\n cc_dg_value=\"${cc_dg_value#\\'}\"; cc_dg_value=\"${cc_dg_value%\\'}\"\n [ -z \"$cc_dg_name\" ] && continue\n [ -z \"$cc_dg_value\" ] && continue\n # 長い書き方から先に置き換える。\"$D\" -> ${D} -> $D の順でないと\n # ${D} の中の $D が先に当たって括弧が残る。\n CC_DG_SUBST=\"${CC_DG_SUBST//\\\"\\$$cc_dg_name\\\"/$cc_dg_value}\"\n CC_DG_SUBST=\"${CC_DG_SUBST//\\$\\{$cc_dg_name\\}/$cc_dg_value}\"\n CC_DG_SUBST=\"${CC_DG_SUBST//\\$$cc_dg_name/$cc_dg_value}\"\n done <<CC_DG_ASSIGNMENTS\n$(printf '%s' \"$COMMAND\" | grep -oE '(^|[;&|[:space:]])[A-Za-z_][A-Za-z0-9_]*=(\"[^\"]*\"|'\"'\"'[^'\"'\"']*'\"'\"'|[^[:space:];&|]+)' | sed -E 's/^[;&|[:space:]]+//')\nCC_DG_ASSIGNMENTS\n if [ \"$CC_DG_SUBST\" != \"$COMMAND\" ]; then\n CC_DG_SUBSTITUTED_CMD=\"$CC_DG_SUBST\" bash \"$0\" </dev/null >/dev/null 2>&1\n if [ \"$?\" = \"2\" ]; then\n log_block \"destructive target hidden behind a variable\"\n echo \"BLOCKED: the deletion target is set in a variable in this same command.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Putting the path in a variable does not change what gets deleted.\" >&2\n echo \"Name the directory you mean, or run the harmless part on its own.\" >&2\n exit 2\n fi\n fi\nfi\n\n# --- Check 0y: is a destructive verb actually being invoked? -------------------\n# The checks below scan the whole command string, so a dangerous string written\n# inside quotes or inside prose reaches them just as an executed command would.\n# Measured 2026-08-04 against a set of fifteen commands that must not be\n# blocked: six were blocked, all of them merely mentioning a command --\n# searching for it, echoing it into a note, putting it in a commit message,\n# naming it in a jq filter or an awk program, describing it in a sentence.\n# Searching for a string is not running it, and a guard that blocks grep, awk\n# and git commit is switched off the same day it is installed.\n#\n# This does NOT strip the quotes before matching. Stripping them would lose a\n# real invocation whose target is quoted. It only asks whether a destructive\n# verb appears OUTSIDE quotes at a command start. If none does, nothing\n# destructive is being invoked and the rest of the file has nothing to judge.\n# Everything after this point still matches against the original text.\nif [ -z \"${CC_DG_UNWRAPPED_CMD:-}\" ]; then\n CC_DG_OUTSIDE_TEXT=$(printf '%s' \"$COMMAND\" | awk -v SQ=\"'\" -v DQ='\"' '\n { line = $0; out = \"\"; q = 0\n for (i = 1; i <= length(line); i++) {\n c = substr(line, i, 1)\n if (q == 0 && c == SQ) { q = 1; out = out \" \"; continue }\n if (q == 1) { if (c == SQ) q = 0; out = out \" \"; continue }\n if (q == 0 && c == DQ) { q = 2; out = out \" \"; continue }\n if (q == 2) { if (c == DQ) q = 0; out = out \" \"; continue }\n out = out c\n }\n print out }')\n CC_DG_VERB_OUTSIDE=$(printf '%s' \"$CC_DG_OUTSIDE_TEXT\" |\n grep -cE '(^|[;&|`(){}]|\\$\\(|[[:space:]](then|do|else|elif)[[:space:]])[[:space:]]*([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+|(env|nice|ionice|timeout|exec|command|builtin|stdbuf|nohup|sudo|time|xargs)([[:space:]]+-[^[:space:]]+)*([[:space:]]+[0-9]+)?([[:space:]]+-[^[:space:]]+)*[[:space:]]+)*([./]*[A-Za-z0-9_./-]*/)?(rm|rmdir|unlink|shred|mkfs[.a-z0-9]*|dd|chmod|chown|mv|cp|git|find|xargs|truncate|kill|killall|docker|kubectl|terraform|aws|gcloud|az|npm|npx|yarn|pnpm|pip|pip3|Remove-Item|del|rd|base64|curl|wget|eval|source|\\.)([[:space:]]|$)' \\\n 2>/dev/null | head -n 1)\n case \"$CC_DG_VERB_OUTSIDE\" in ''|*[!0-9]*) CC_DG_VERB_OUTSIDE=1 ;; esac\n\n\n # `git` has to be in the verb list because this guard blocks `git reset --hard`\n # and friends, but that makes every `git commit -m \"...text about rm...\"` reach\n # the checks below. Count git only when a subcommand this guard actually judges\n # is also outside the quotes.\n if [ \"$CC_DG_VERB_OUTSIDE\" != \"0\" ]; then\n CC_DG_NONGIT=$(printf '%s' \"$CC_DG_OUTSIDE_TEXT\" | grep -cE '(^|[;&|`(){}]|\\$\\(|[[:space:]](then|do|else|elif)[[:space:]])[[:space:]]*([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+|(env|nice|ionice|timeout|exec|command|builtin|stdbuf|nohup|sudo|time|xargs)([[:space:]]+-[^[:space:]]+)*([[:space:]]+[0-9]+)?([[:space:]]+-[^[:space:]]+)*[[:space:]]+)*([./]*[A-Za-z0-9_./-]*/)?(rm|rmdir|unlink|shred|mkfs[.a-z0-9]*|dd|chmod|chown|mv|find|xargs|truncate|docker|kubectl|terraform|aws|gcloud|az|Remove-Item|del|rd|base64|eval)([[:space:]]|$)' 2>/dev/null | head -n 1)\n # Normalised, because this pre-filter is the gate that decides whether any\n # destructive verb is present at all. Before 2026-09-03 it read the raw text,\n # counted 0 for `git -C /repo reset --hard`, and exited 0 before any of the\n # checks below ever ran -- fixing those checks alone changed nothing.\n CC_DG_GITSUB=$(printf '%s' \"$(cc_strip_git_globals \"$CC_DG_OUTSIDE_TEXT\")\" | grep -cE 'git[[:space:]]+(reset|clean|checkout|switch|restore|branch|push|rm|submodule|stash|filter-branch|update-ref|gc|prune)\\b' 2>/dev/null | head -n 1)\n case \"$CC_DG_NONGIT\" in ''|*[!0-9]*) CC_DG_NONGIT=1 ;; esac\n case \"$CC_DG_GITSUB\" in ''|*[!0-9]*) CC_DG_GITSUB=1 ;; esac\n if [ \"$CC_DG_NONGIT\" = \"0\" ] && [ \"$CC_DG_GITSUB\" = \"0\" ]; then\n CC_DG_VERB_OUTSIDE=0\n fi\n fi\n\n\n # A quoted string is only inert while nothing executes it. `sh -c '...'`,\n # `bash -c \"...\"` and `eval` all run their argument, so the quoted text there\n # is a command, not a mention. Never let the gate pass those through.\n # `echo \"...\" | sh` and `... | bash` run whatever was piped in, so the quoted\n # text is a command there too. The pipe itself is outside the quotes, so the\n # blanked text still shows it.\n if printf '%s' \"$CC_DG_OUTSIDE_TEXT\" | grep -qE '\\|[[:space:]]*(sudo[[:space:]]+)?(sh|bash|zsh|dash|ksh|python3?|perl|ruby|node)([[:space:]]|$)' 2>/dev/null; then\n CC_DG_VERB_OUTSIDE=1\n fi\n if printf '%s' \"$CC_DG_OUTSIDE_TEXT\" | grep -qE '(^|[;&|`(){}]|[[:space:]])((sh|bash|zsh|dash|ksh)[[:space:]]+(-[a-z]*[[:space:]]+)*-c\\b|(powershell|powershell\\.exe|pwsh)([[:space:]]+-[A-Za-z]+)*[[:space:]]+-([Cc]ommand|c)\\b|eval\\b)' 2>/dev/null; then\n CC_DG_VERB_OUTSIDE=1\n fi\n if [ \"$CC_DG_VERB_OUTSIDE\" = \"0\" ]; then\n exit 0\n fi\nfi\n\n# --- Check 0z: unwrap the command and run this guard against the inside -------\n# Every boundary in this file is (^|[;&|]) -- the start of the line, or just\n# after a separator. A command placed inside a substitution, a subshell, or\n# after a shell keyword sits at neither, so none of the checks below ever see\n# it. All three of these passed before this check existed:\n#\n# [[ -n $(rm -rf ~) ]]\n# if true; then rm -rf ~; fi\n# `rm -rf ~`\n#\n# Claude Code 2.1.221 fixed the same shape on its own side (\"a Bash tool\n# permission-check bypass where zsh could execute hidden commands in [[ ]]\n# regex conditionals\"). This guard had it too. Measured 2026-08-04: one blocked\n# command, ten wrappings, ten passes.\n#\n# Rather than rewrite every pattern in this file, replace the wrapping tokens\n# with separators and run this same script once more against that text. The\n# inner command then sits at a real boundary and meets every existing check\n# unchanged. Detection only: the rewritten text is never executed, and the\n# message shows the command the user actually sent.\nif [ -z \"${CC_DG_UNWRAPPED_CMD:-}\" ]; then\n CC_DG_UNWRAPPED=$(printf '%s' \"$COMMAND\" | sed -E \\\n -e 's/\\$\\(/ ; /g' \\\n -e 's/`/ ; /g' \\\n -e 's/\\(/ ; /g' \\\n -e 's/\\)/ ; /g' \\\n -e 's/\\{/ ; /g' \\\n -e 's/\\}/ ; /g' \\\n -e 's/(^|[[:space:]])(then|do|else|elif)([[:space:]])/\\1 ; \\3/g')\n if [ \"$CC_DG_UNWRAPPED\" != \"$COMMAND\" ]; then\n CC_DG_UNWRAPPED_CMD=\"$CC_DG_UNWRAPPED\" bash \"$0\" </dev/null >/dev/null 2>&1\n if [ \"$?\" = \"2\" ]; then\n log_block \"destructive command hidden inside a wrapper\"\n echo \"BLOCKED: a destructive command is hidden inside a wrapper.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"A substitution, a subshell or a shell keyword does not make a\" >&2\n echo \"command safe. Run the harmless part on its own, or name the\" >&2\n echo \"directory you mean.\" >&2\n exit 2\n fi\n fi\nfi\n\n# --- Check 0: --no-preserve-root ---\nif printf '%s' \"$COMMAND\" | grep -qE \"rm\\\\s.*\\\\-\\\\-no-preserve-root\"; then\n echo \"BLOCKED: --no-preserve-root detected.\" >&2\n exit 2\nfi\n\n# --- Check 0b: rm -rf on a bare glob ---\n# 2026-08-03. Check 1 asks whether the target path starts with `/`, `~`, `$HOME`\n# and so on, so `rm -rf *` never matched it -- measured against all three shipping\n# channels (the itch kit, npm 29.8.0 and this tree) and all three let it through.\n# `*` expands to everything in the current directory, which is `rm -rf .` by\n# another name, and `rm -rf .` is already blocked. Blocking one and not the other\n# does not hold together.\n# Narrow on purpose: only a bare `*` or `./*` as the whole target. `rm -rf *.log`,\n# `rm -rf build/*` and `rm -rf node_modules/*` stay allowed, because stopping the\n# ordinary cleanup is its own kind of broken.\nif printf '%s' \"$COMMAND\" | grep -qE '(^|[;&|])[[:space:]]*(sudo[[:space:]]+)?rm[[:space:]]+((-[rRfF]+|--recursive|--force)[[:space:]]+)+[\"'\"'\"']?(\\./)?\\*[\"'\"'\"']?[[:space:]]*($|[;&|])'; then\n log_block \"rm -rf on a bare glob\"\n echo \"BLOCKED: 'rm -rf *' removes everything in the current directory.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Name what you mean: rm -rf ./build, rm -rf *.log, or list it first with ls.\" >&2\n exit 2\nfi\n\n# --- Check 1: rm -rf on dangerous paths ---\n# The path used to have to follow the flags directly, so `rm -rf \"/etc\"` matched\n# nothing: the character after the flags was a quote. Quoting an argument is an\n# ordinary way to write a command, not an evasion. One optional quote is allowed\n# in front of the path, and the terminators accept a closing quote as well.\n# Measured 2026-08-04: three of four quoted targets passed before this.\nif printf '%s' \"$COMMAND\" | grep -qE 'rm\\s+((-[rRfF]+|--recursive|--force)\\s+)*[\"'\"'\"']?(\\/$|\\/\\s|\\/[^a-z]|\\/home|\\/etc|\\/usr|\\/var|\\/mnt|~\\/|~(\\s|$|[\"'\"'\"'])|[\"]?\\$\\{?HOME\\}?[\"]?\\/|[\"]?\\$\\{?HOME\\}?[\"]?(\\s|$)|\\.\\.\\/|\\.\\.(\\s|$)|\\.(\\s|$)|\\.\\/(\\s|$))'; then\n # Exception: safe directories\n SAFE=0\n IFS=':' read -ra DIRS <<< \"$SAFE_DIRS\"\n for dir in \"${DIRS[@]}\"; do\n if printf '%s' \"$COMMAND\" | grep -qE \"rm\\s+.*${dir}\\s*$|rm\\s+.*${dir}/\"; then\n SAFE=1\n break\n fi\n done\n\n # Check for mounted filesystems inside the target (NFS, Docker, bind mounts)\n # Why: GitHub #36640 — rm -rf on a dir with NFS mount deleted production data\n if (( SAFE == 0 )); then\n # Extract the target path from the rm command\n # \\S+ picks up the surrounding quotes too, and findmnt does not\n # understand a path spelled \"/etc\" with the quotes attached.\n TARGET_PATH=$(printf '%s' \"$COMMAND\" | grep -oP 'rm\\s+((-[rRfF]+|--recursive|--force)\\s+)*\\K\\S+' | sed -E 's/^[\"'\"'\"']//; s/[\"'\"'\"']$//')\n if [ -n \"$TARGET_PATH\" ] && command -v findmnt &>/dev/null; then\n if findmnt -n -o TARGET --submounts \"$TARGET_PATH\" 2>/dev/null | grep -q .; then\n log_block \"rm on path with mounted filesystem\"\n echo \"BLOCKED: Target contains a mounted filesystem (NFS, Docker, bind).\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Unmount the filesystem first, then retry.\" >&2\n exit 2\n fi\n fi\n fi\n\n if (( SAFE == 0 )); then\n log_block \"rm on sensitive path\"\n echo \"BLOCKED: rm on sensitive path detected.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"This command targets a sensitive directory that could cause\" >&2\n echo \"irreversible data loss. On WSL2, rm -rf can follow NTFS\" >&2\n echo \"junctions and delete far beyond the target directory.\" >&2\n echo \"\" >&2\n echo \"If you need to delete a specific subdirectory, target it directly:\" >&2\n echo \" rm -rf ./specific-folder\" >&2\n exit 2\n fi\nfi\n\n# --- Check 1b: every operand of rm, not just the first ---\n# Check 1 keys on the position right after the flags, so a safe target in front\n# hides a dangerous one behind it: `rm -rf ..` is blocked and\n# `rm -rf node_modules ..` is not. Measured 2026-08-03. This is the same shape as\n# the approving hooks fixed the same day -- a rule that reads one command position\n# and then speaks for the whole line.\n#\n# Deliberately narrow. Only operands that cannot be meant in an ordinary cleanup\n# are checked here: the parent directory, the filesystem root, the home\n# directory, and the absolute system prefixes Check 1 already calls critical.\n# Anything else stays with Check 1, so this cannot start blocking ordinary work\n# like `rm -rf build ./dist` -- a relative name never matches an absolute prefix.\n#\n# The absolute prefixes were missing until 2026-09-04. The header of this file\n# promises that deletions of /home and /etc are blocked, and Check 1 does block\n# them -- but only when the dangerous path is the operand Check 1 happens to\n# read. Measured that day: `rm -rf /home/<user>/<dir>` was blocked and\n# `rm -rf /home/<user>/<dir> /tmp/x` was not. One extra operand that looks safe\n# turned the documented promise off. The gap was not in Check 1 and not in the\n# test for Check 1b -- both were right about what they covered. It was between\n# what the header promised and what the every-operand list contained.\nif printf '%s' \"$COMMAND\" | grep -qE '(^|[;&|])[[:space:]]*(sudo[[:space:]]+)?rm[[:space:]]'; then\n RM_TAIL=$(printf '%s' \"$COMMAND\" | sed -E 's/.*(^|[;&|])[[:space:]]*(sudo[[:space:]]+)?rm[[:space:]]+//' | sed -E 's/[;&|].*//')\n for operand in $RM_TAIL; do\n case \"$operand\" in\n -*) continue ;;\n ..|../|\"~\"|\"~/\"|/|//|\"~\"/*|/home/*|/Users/*|/etc/*|/usr/*|/var/*|/opt/*|/root/*)\n log_block \"rm operand $operand (checked at every position)\"\n echo \"BLOCKED: rm targets $operand.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"This deletes the parent directory, the home directory or the\" >&2\n echo \"filesystem root. It is checked wherever it appears in the\" >&2\n echo \"argument list, not only in the first position.\" >&2\n echo \"\" >&2\n echo \"Name the directory you mean: rm -rf ./build\" >&2\n exit 2 ;;\n esac\n done\nfi\n\n# --- Check 2: git reset --hard ---\n# Only match when git is the actual command, not inside strings/arguments.\n# 2026-08-03: the flag does not have to follow `reset`. `git reset HEAD~1 --hard`\n# discards exactly the same work and walked past the old pattern. Scan to the end\n# of this command only -- [^;&|] never crosses into the next one.\nif printf '%s' \"$CHECK_CMD\" | grep -qE \"${CC_CMDPOS}\"'git\\s+reset\\b[^;&|]*--hard'; then\n log_block \"git reset --hard\"\n echo \"BLOCKED: git reset --hard discards all uncommitted changes.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: git stash, or git reset --soft to keep changes staged.\" >&2\n exit 2\nfi\n\n# --- Check 3: git clean -fd ---\n# 2026-08-03: the flags do not have to be one bundle, and they do not have to come\n# first. `git clean -x -f -d` deletes more than `git clean -fd` and walked past the\n# old pattern. The trailing \\b keeps the dry runs out: `-nd` and `-ndx` still pass.\n# The dry runs are the thing this hook tells people to do first, so they must not\n# be blocked. `git clean -nd` and `-ndx` list what would go and remove nothing.\n# The exclusion below is the only thing that may let them through. Do NOT add a\n# word boundary after the flag bundle to do it: that also releases `-fdx`, which\n# removes more than `-fd` does. CI caught exactly that on 2026-08-03.\nif printf '%s' \"$CHECK_CMD\" | grep -qE \"${CC_CMDPOS}\"'git\\s+clean\\b[^;&|]*(\\s-[a-z]*[fd]|\\s--force\\b)' \\\n && ! printf '%s' \"$CHECK_CMD\" | grep -qE \"${CC_CMDPOS}\"'git\\s+clean\\b[^;&|]*(\\s-[a-z]*n[a-z]*\\b|\\s--dry-run\\b)'; then\n log_block \"git clean\"\n echo \"BLOCKED: git clean removes untracked files permanently.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: git clean -n (dry run) first to see what would be deleted.\" >&2\n exit 2\nfi\n\n# --- Check 4: chmod 777 on broad paths ---\n# 2026-08-03: the old pattern required -R immediately before 777 and the path\n# immediately after it. `chmod 777 -R /etc` and `chmod --recursive 777 /etc` are the\n# same command with the words in a different order, and both walked past it.\nif printf '%s' \"$COMMAND\" | grep -qE 'chmod\\s[^;&|]*\\b777\\b[^;&|]*\\s(\\/|~|\\.)'; then\n echo \"BLOCKED: chmod 777 on broad path is a security risk.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n exit 2\nfi\n\n# --- Check 5: mass deletion driven by find ---\n# Rewritten 2026-07-27. The old version matched only `find /`, `find ~` and\n# `find ..` followed by -delete, so the two forms people actually run --\n# `find . -delete` and `find . -exec rm -rf {} \\;` -- both walked past it.\n# Quoting the flag (find . '-delete') or putting a wrapper in front\n# (env find . -delete) defeated the word-boundary checks as well.\n#\n# The detection is inverted: normalise first, then ask three plain questions.\n# 1. is the command that runs this actually `find`?\n# 2. does it carry a delete action?\n# 3. how wide is the search?\n# A narrowed cleanup (find . -name '*.pyc' -delete) is everyday work and stays\n# allowed. An unnarrowed one walks the whole tree, so it does not.\n\n# 1. drop quote characters, so '-delete' and \"rm\" read as bare words\nFIND_NORM=$(printf '%s' \"$COMMAND\" | tr -d \"\\\"'\")\n# 2. drop leading VAR=value assignments at every command position\nFIND_NORM=$(printf '%s' \"$FIND_NORM\" | sed -E ':a; s@(^|[;&|][[:space:]]*)[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+@\\1@g; ta')\n# 3. drop wrapper commands that sit in front of the real one, repeatedly.\n# An allow-list of wrappers is what leaked before, so this runs until it\n# stops matching instead of assuming a single wrapper.\nFIND_NORM=$(printf '%s' \"$FIND_NORM\" | sed -E ':b; s@(^|[;&|][[:space:]]*)([A-Za-z0-9_./-]*/)?([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*|env|nice|ionice|timeout|stdbuf|exec|command|sudo|doas|nohup|setsid|xargs)([[:space:]]+-[^[:space:]]+)*([[:space:]]+[0-9]+[smhd]?)?[[:space:]]+@\\1@g; tb')\n# 4. a shell wrapper puts its payload at a command position too\nFIND_NORM=$(printf '%s' \"$FIND_NORM\" | sed -E 's@(^|[;&|][[:space:]]*)(sh|bash|zsh|dash)[[:space:]]+-c[[:space:]]+@\\1@g')\n# 5. /usr/bin/find and ./find are still find\nFIND_NORM=$(printf '%s' \"$FIND_NORM\" | sed -E 's@(^|[[:space:]|;&])[A-Za-z0-9_./-]*/find([[:space:]]|$)@\\1find\\2@g')\n# 6. a trailing backslash continues the line; join so predicates stay together\nFIND_NORM=$(printf '%s' \"$FIND_NORM\" | sed -E ':c; s@\\\\[[:space:]]*$@ @; tc' | tr '\\n' ' ')\n\nFIND_HITS=0\nif printf '%s' \"$FIND_NORM\" | grep -qE '(^|[;&|][[:space:]]*)find([[:space:]]|$)'; then\n # -delete, or -exec/-execdir/-ok handing the match to a deleting command\n if printf '%s' \"$FIND_NORM\" | grep -qE '(^|[[:space:]])-delete([[:space:]]|$)' \\\n || printf '%s' \"$FIND_NORM\" | grep -qE '(^|[[:space:]])-(exec|execdir|ok|okdir)[[:space:]]+(([A-Za-z0-9_./-]*/)?(env|nice|sudo|doas|xargs)[[:space:]]+([-][^[:space:]]+[[:space:]]+)*)*([A-Za-z0-9_./-]*/)?(rm|unlink|shred)([[:space:]]|$)'; then\n FIND_HITS=1\n fi\nfi\n\nif (( FIND_HITS == 1 )); then\n # the search roots are the words between `find` and its first predicate\n FIND_ROOTS=$(printf '%s' \"$FIND_NORM\" | sed -E 's@^.*(^|[;&|][[:space:]]*)find[[:space:]]+@@; s@[[:space:]]-[A-Za-z].*@@; s@[[:space:]]*$@@')\n\n FIND_ROOT_RISKY=0\n if printf '%s' \"$FIND_ROOTS\" | grep -qE '(^|[[:space:]])(/|~|\\$\\{?HOME\\}?|\\.\\.)(/[^[:space:]]*)?([[:space:]]|$)'; then\n FIND_ROOT_RISKY=1\n fi\n\n # A predicate that actually narrows the match. Three things do not count:\n # -type f : still every file in the tree\n # -name '*' : a bare wildcard narrows nothing\n # -o / -or : the delete can bind to the other side of the or\n FIND_NARROWED=0\n if printf '%s' \"$FIND_NORM\" | grep -qE '(^|[[:space:]])-(iname|name|ipath|path|iregex|regex|iwholename|wholename|newer[a-zA-Z]*|mtime|mmin|ctime|cmin|atime|amin|size|user|group|uid|gid|perm|links|inum|samefile)([[:space:]]|$)'; then\n FIND_NARROWED=1\n fi\n if printf '%s' \"$FIND_NORM\" | grep -qE '(^|[[:space:]])-(i?name|i?path|i?regex|i?wholename)[[:space:]]+[*.]+([[:space:]]|$)'; then\n FIND_NARROWED=0\n fi\n if printf '%s' \"$FIND_NORM\" | grep -qE '(^|[[:space:]])-(o|or)([[:space:]]|$)'; then\n FIND_NARROWED=0\n fi\n\n # a root that is one of the throwaway build directories is fine unnarrowed\n FIND_ROOT_SAFE=0\n IFS=':' read -ra FIND_SAFE_DIRS <<< \"$SAFE_DIRS\"\n for dir in \"${FIND_SAFE_DIRS[@]}\"; do\n [ -z \"$dir\" ] && continue\n dir_re=${dir//./\\\\.}\n if printf '%s' \"$FIND_ROOTS\" | grep -qE \"(^|[[:space:]/])${dir_re}(/|[[:space:]]|$)\"; then\n FIND_ROOT_SAFE=1\n break\n fi\n done\n\n if (( FIND_ROOT_RISKY == 1 )) || { (( FIND_NARROWED == 0 )) && (( FIND_ROOT_SAFE == 0 )); }; then\n log_block \"find mass deletion\"\n echo \"BLOCKED: find is about to delete across an unbounded set of paths.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n if (( FIND_ROOT_RISKY == 1 )); then\n echo \"The search starts at a system, home or parent directory.\" >&2\n else\n echo \"Nothing narrows the match, so every file under the search root goes.\" >&2\n fi\n echo \"\" >&2\n echo \"Run the same find with -print first and read the list. If the list is\" >&2\n echo \"right, narrow the delete with -name/-path/-mtime, or point it at a\" >&2\n echo \"specific directory.\" >&2\n exit 2\n fi\nfi\n\n# --- Check 6: sudo with dangerous commands ---\n# The pattern was anchored with `^\\s*sudo`, and the anchor applies to the whole\n# command string, so only the first command position was ever examined.\n# `cd /tmp && sudo rm -rf app` walked straight past. It stayed hidden because\n# Check 1 independently blocks rm on a sensitive path, so the obvious probe\n# (`cd /tmp && sudo rm -rf /var`) still came back blocked; the gap only shows on\n# the targets this check alone covers -- an ordinary relative path, and dd/mkfs,\n# which nothing else looks at. Measured 2026-08-03: 7 of 8 such forms exited 0\n# with a separator in front. Checks 2, 3 and 7 already list their separators,\n# so this one now does too, including a bare `&` and a pipe.\n# Not addressed here: sudo's own options (`sudo -u www rm -rf app`) still slip\n# past, which is a coverage question rather than an anchor one.\nif printf '%s' \"$COMMAND\" | grep -qE '(^|[;&|])\\s*sudo\\s+(rm\\s+-[rf]|chmod\\s+(-R\\s+)?777|dd\\s+if=|mkfs)'; then\n log_block \"sudo with dangerous command\"\n echo \"BLOCKED: sudo with dangerous command detected.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Running destructive commands with sudo amplifies the damage.\" >&2\n echo \"Review the command carefully before proceeding.\" >&2\n exit 2\nfi\n\n\n# --- Check 7: PowerShell Remove-Item (Windows/WSL2) ---\n# Real incident: GitHub #37331 — destroyed entire repo\n#\n# Rewritten 2026-08-03. The old form asked \"do these words appear anywhere in\n# the line?\", then undid the false positives with a skip anchored at `^` for\n# git commit, echo, printf and cat. Both halves were wrong the same way. The\n# skip only applied at the start of the line, so\n# `cd repo && git commit -m \"... Remove-Item -Recurse -Force ...\"` was blocked\n# (measured: exit 2) even though it only writes a commit message; and every new\n# way of printing the words -- a pipe into grep, a heredoc -- needed its own\n# entry in the skip list forever.\n#\n# The question asked here instead is whether the deleting command sits at a\n# command position: at the start of the line or right after a separator,\n# optionally behind a PowerShell wrapper. `echo \"Remove-Item -Recurse -Force\"`\n# runs echo, so it is text. `cd /tmp && Remove-Item -Recurse -Force *` runs the\n# deletion. Same shape as Check 5, which asks whether the command that runs is\n# actually `find`.\n#\n# Known limit: quotes are not parsed, so a separator inside a quoted string ends\n# a segment early and the words after it look like a command. That limit is\n# shared with the rest of this file.\n\n# 1. drop quote characters and fold case, so \"Remove-Item and 'remove-item read\n# as the same bare word. Folding with tr avoids sed's case-insensitive flag,\n# which is not portable.\nPS_NORM=$(printf '%s' \"$COMMAND\" | tr -d \"\\\"'\" | tr 'A-Z' 'a-z')\n# 2. drop PowerShell wrappers and their flags at every command position, so\n# `powershell -c Remove-Item ...` is read as the Remove-Item it runs\nPS_NORM=$(printf '%s' \"$PS_NORM\" | sed -E ':p; s@(^|[;&|][[:space:]]*)([a-z0-9_./-]*/)?(pwsh|powershell)(\\.exe)?([[:space:]]+-[^[:space:]]+)*[[:space:]]+@\\1@g; tp')\n\nif printf '%s' \"$PS_NORM\" | grep -qE '(^|[;&|])[[:space:]]*remove-item[[:space:]]' \\\n && printf '%s' \"$PS_NORM\" | grep -qE 'remove-item.*-recurse.*-force|remove-item.*-force.*-recurse'; then\n log_block \"PowerShell destructive command\"\n echo \"BLOCKED: Destructive PowerShell command detected.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Remove-Item with recursive force-delete can destroy entire directories\" >&2\n echo \"irreversibly. Target specific files instead.\" >&2\n exit 2\nelif printf '%s' \"$PS_NORM\" | grep -qE '(^|[;&|])[[:space:]]*(del|rd|rmdir)[[:space:]]+/s[[:space:]]+/q'; then\n log_block \"cmd.exe recursive quiet delete\"\n echo \"BLOCKED: Destructive cmd.exe delete detected.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"del /s /q and rd /s /q remove a whole tree without asking.\" >&2\n echo \"Target specific files instead.\" >&2\n exit 2\nfi\nif printf '%s' \"$CHECK_CMD\" | grep -qE \"${CC_CMDPOS}\"'git\\s+(checkout|switch)\\b.*(--force\\b|[^A-Za-z0-9]-[a-zA-Z]*f\\b|--discard-changes\\b)'; then\n log_block \"git checkout/switch --force\"\n echo \"BLOCKED: git checkout/switch with --force discards uncommitted changes.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: git stash before switching, or use git switch without --force.\" >&2\n exit 2\nfi\n\nif printf '%s' \"$COMMAND\" | grep -qE '(sh|bash|zsh)\\s+-c\\s+'; then\n INNER=$(printf '%s' \"$COMMAND\" | sed -E \"s/.*(sh|bash|zsh)\\s+-c\\s+['\\\"]//\" | sed \"s/['\\\"]*$//\" )\n if echo \"$(cc_strip_git_globals \"$INNER\")\" | grep -qE 'rm\\s+-[rf]*\\s+[/~]|git\\s+reset\\s+--hard|git\\s+clean\\s+-[fd]+|mkfs\\.|dd\\s+if='; then\n echo \"BLOCKED: Destructive command hidden in shell wrapper\" >&2\n echo \"\" >&2\n echo \"Detected: $INNER\" >&2\n exit 2\n fi\nfi\nif printf '%s' \"$COMMAND\" | grep -qE '\\|\\s*(sh|bash)\\s*$'; then\n if printf '%s' \"$CHECK_CMD\" | grep -qE 'rm\\s+-[rf]*\\s+[/~]|git\\s+reset\\s+--hard|git\\s+clean\\s+-[fd]+'; then\n echo \"BLOCKED: Destructive command piped to shell\" >&2\n exit 2\n fi\nfi\n\n# --- Check 8: xargs invoking rm with recursive flags (target path absent from the command line) ---\nif printf '%s' \"$COMMAND\" | grep -qE 'xargs\\s+(-\\S+\\s+)*rm\\s+(-\\S+\\s+)*(-[a-zA-Z]*[rR]|--recursive)'; then\n log_block \"xargs rm with recursive flags\"\n echo \"BLOCKED: xargs invoking 'rm' with recursive flags risks mass deletion.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"The target paths come from stdin, so this can delete far more than expected.\" >&2\n echo \"Consider piping to 'xargs -p' or reviewing the input list first.\" >&2\n exit 2\nfi\n\n# --- Check 9: base64-decoded payload fed straight into a shell (obfuscation) ---\nif printf '%s' \"$COMMAND\" | grep -qE 'base64\\s+(-d|--decode)\\b' && printf '%s' \"$COMMAND\" | grep -qE '\\|\\s*(sh|bash|zsh)(\\s|$)|eval\\s|(sh|bash|zsh)\\s+-c'; then\n log_block \"base64-decoded payload piped into a shell\"\n echo \"BLOCKED: base64-decoded payload fed into a shell (obfuscation).\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Decoding base64 straight into sh/bash/eval hides what actually runs.\" >&2\n echo \"Decode to a file and inspect it before executing.\" >&2\n exit 2\nfi\nexit 0",
|
|
3
|
+
"branch-guard": "#!/bin/bash\n# ================================================================\n# branch-guard.sh — Branch Push Protector\n# ================================================================\n# PURPOSE:\n# Prevents accidental git push to main/master branches AND\n# blocks force-push on ALL branches without explicit approval.\n#\n# Force-pushes rewrite history and can destroy teammates' work.\n# Protected branch pushes bypass code review.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# WHAT IT BLOCKS (exit 2):\n# - git push origin main/master (any protected branch)\n# - git push --force (any branch — history rewriting)\n# - git push -f (short flag variant)\n# - git push --force-with-lease (still destructive)\n#\n# WHAT IT ALLOWS (exit 0):\n# - git push origin feature-branch (non-force)\n# - git push -u origin feature-branch\n# - All other git commands\n# - All non-git commands\n#\n# CONFIGURATION:\n# CC_PROTECT_BRANCHES — colon-separated list of protected branches\n# default: \"main:master\"\n# CC_ALLOW_FORCE_PUSH=1 — disable force-push protection\n# ================================================================\n\nINPUT=$(cat)\n\n# --- Reading the hook payload -------------------------------------------------\n# Why a chain instead of jq alone: on a machine without jq the read returned an\n# empty string, every check below was skipped, and this hook exited 0 in silence.\n# It stayed listed in settings.json and protected nothing. Measured 2026-07-27:\n# a destructive command passed with exit 0 and no output at all.\n# If none of the three readers exist we warn once and allow, because blocking\n# every Bash call would make Claude Code unusable — stopping all work is a\n# failure, not safety.\ncc_read_field() {\n if command -v jq >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | jq -r \".$1 // empty\" 2>/dev/null\n elif command -v python3 >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" python3 -c 'import sys,json,os\ntry:\n d=json.load(sys.stdin)\nexcept Exception:\n sys.exit(0)\nfor k in os.environ.get(\"CC_FIELD\",\"\").split(\".\"):\n d = d.get(k) if isinstance(d,dict) else None\nsys.stdout.write(\"\" if d is None else str(d))\n' 2>/dev/null\n elif command -v node >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" node -e 'let s=\"\";process.stdin.on(\"data\",d=>s+=d).on(\"end\",()=>{try{let o=JSON.parse(s);for(const k of (process.env.CC_FIELD||\"\").split(\".\"))o=(o&&typeof o===\"object\")?o[k]:undefined;process.stdout.write(o==null?\"\":String(o))}catch(e){}})' 2>/dev/null\n else\n printf '%s' '__CC_NO_PARSER__'\n fi\n}\n\ncc_warn_no_parser() {\n marker=\"${TMPDIR:-/tmp}/cc-noparser-warned-branch-guard-$PPID\"\n [ -f \"$marker\" ] && return 0\n : > \"$marker\" 2>/dev/null\n echo \"WARNING [branch-guard]: jq, python3 and node are all unavailable, so this hook cannot read the tool call and is NOT protecting you. Install any one of them.\" >&2\n}\n\nCOMMAND=$(cc_read_field tool_input.command)\n\n\n# Log function — records blocked commands for audit.\n# Added 2026-09-03: the book tells operators to read this log to learn what was\n# stopped, but only destructive-guard was writing to it. A force push or a staged\n# .env was refused and left no trace, so an empty log read as \"nothing happened\".\nlog_block() {\n local reason=\"$1\"\n local logfile=\"${CC_BLOCK_LOG:-$HOME/.claude/blocked-commands.log}\"\n mkdir -p \"$(dirname \"$logfile\")\" 2>/dev/null\n echo \"[$(date -Iseconds)] BLOCKED: $reason | cmd: $COMMAND\" >> \"$logfile\" 2>/dev/null\n}\n\nif [ \"$COMMAND\" = \"__CC_NO_PARSER__\" ]; then\n\n cc_warn_no_parser\n\n exit 0\n\nfi\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# --- a trailing backslash makes the next line part of the same command -------\n# Added 2026-09-03. The shell joins `\\` + newline before it runs anything, so\n# `rm -rf \\` / ` ~/Documents` is one deletion. grep is line-oriented and saw\n# two unrelated lines, matched neither, and this hook exited 0. Measured against\n# the shipped copy: the rm, the chmod 777 and every git check could all be\n# stepped over this way. 0.9% of 43,858 real Bash calls in this operator's own\n# history use a line continuation, so this is ordinary typing, not an exploit.\n# Join them once, up front, and every check below sees what the shell will run.\ncc_join_continuations() {\n local s=$1\n s=${s//\\\\$'\\n'/ }\n printf '%s' \"$s\"\n}\nCOMMAND=$(cc_join_continuations \"$COMMAND\")\n\n# --- git's global options sit between `git` and the verb ----------------------\n# Added 2026-09-03. Every git check in this hook expected the verb to be next to\n# the word `git`, so `git -C /repo push --force` matched none of them and the\n# hook exited 0 -- an affirmative approval. Measured against the shipped copy on\n# 2026-09-03: 4 bypasses here, 2 in secret-guard, 4 in destructive-guard. git\n# accepts these options anywhere before the verb and any number of times, so\n# strip them once and let the patterns below stay readable.\n# The messages still print $COMMAND, because the operator needs to see what they\n# actually typed, not our normalised copy.\ncc_strip_git_globals() {\n if ! command -v sed >/dev/null 2>&1; then printf '%s' \"$1\"; return; fi\n printf '%s' \"$1\" | sed -E ':a; s/(^|[;&|[:space:]])git[[:space:]]+((-C|-c)[[:space:]]+[^[:space:]]+|--(git-dir|work-tree|namespace|exec-path|super-prefix|config-env|attr-source)(=[^[:space:]]+|[[:space:]]+[^[:space:]]+)|--(no-pager|paginate|bare|literal-pathspecs|glob-pathspecs|noglob-pathspecs|icase-pathspecs|no-replace-objects|no-optional-locks|no-lazy-fetch|no-advice)|-[pP])[[:space:]]+/\\1git /g; ta'\n}\n\nCHECK_CMD=$(cc_strip_git_globals \"$COMMAND\")\n# Never let a failed normalisation turn every check below into a silent pass.\n[ -z \"$CHECK_CMD\" ] && CHECK_CMD=\"$COMMAND\"\n\n# --- `git` does not have to be the first word of the segment ------------------\n# Added 2026-09-14. The gate below anchored on `(^|;|&&|\\|\\|)\\s*git`, so an\n# invocation that reached git through a path or a prefix walked past it and this\n# hook exited 0 -- an affirmative approval. Measured against the shipped copy the\n# same day, with nine invocation forms against each subcommand: destructive-guard\n# blocked 9 of 9 (it had been fixed in #1121), this hook blocked 2 of 9. The seven\n# that passed were `/usr/bin/git`, `./git`, `sudo git`, `env FOO=1 git`,\n# `FOO=1 git`, `time git` and `nohup git`.\n# CC_CMDPOS is the prefix destructive-guard already uses, copied verbatim so the\n# three guards recognise the same set of invocations.\nCC_CMDPOS='(^|[;&|`(){}]|\\$\\(|[[:space:]](then|do|else|elif)[[:space:]])[[:space:]]*([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+|(env|nice|ionice|timeout|exec|command|builtin|stdbuf|nohup|sudo|time|xargs)([[:space:]]+-[^[:space:]]+)*([[:space:]]+[0-9]+)?([[:space:]]+-[^[:space:]]+)*[[:space:]]+)*([./]*[A-Za-z0-9_./-]*/)?'\n\n# Only check git push commands (send-pack is the plumbing spelling of the same act)\nif ! printf '%s' \"$CHECK_CMD\" | grep -qE \"${CC_CMDPOS}\"'git\\s+(push|send-pack)'; then\n exit 0\nfi\n\n# --- Check 1: Force push on ANY branch ---\nif [[ \"${CC_ALLOW_FORCE_PUSH:-0}\" != \"1\" ]]; then\n # `+branch` is a force push too: `git push origin +main` rewrites the remote\n # exactly like --force does, and the shipped pattern had no arm for it.\n # [^;&|]* keeps the scan inside the push itself, so `git push origin x && rm -f y`\n # is no longer reported as a force push.\n if printf '%s' \"$CHECK_CMD\" | grep -qE 'git\\s+(push|send-pack)\\b[^;&|]*([^A-Za-z0-9]-[a-zA-Z]*f\\b|--force\\b|--force-with-lease\\b|[^A-Za-z0-9]\\+[A-Za-z0-9._/*-]+)'; then\n log_block \"Force push detected\"\n echo \"BLOCKED: Force push detected.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Force push rewrites remote history and can destroy\" >&2\n echo \"other people's work. This is almost never what you want.\" >&2\n echo \"\" >&2\n echo \"If you truly need to force push, set CC_ALLOW_FORCE_PUSH=1\" >&2\n exit 2\n fi\nfi\n\n# --- Check 2: Push to protected branches ---\nPROTECTED=\"${CC_PROTECT_BRANCHES:-main:master}\"\n\nBLOCKED=0\nIFS=':' read -ra BRANCHES <<< \"$PROTECTED\"\nfor branch in \"${BRANCHES[@]}\"; do\n if printf '%s' \"$CHECK_CMD\" | grep -qwE \"origin\\s+${branch}|${branch}\\s|${branch}$\"; then\n BLOCKED=1\n break\n fi\ndone\n\nif (( BLOCKED == 1 )); then\n log_block \"Push to protected branch\"\n echo \"BLOCKED: Attempted push to protected branch.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Protected branches: $PROTECTED\" >&2\n echo \"\" >&2\n echo \"Push to a feature branch first, then create a pull request.\" >&2\n exit 2\nfi\n\nexit 0\n",
|
|
4
|
+
"syntax-check": "#!/bin/bash\n# ================================================================\n# syntax-check.sh — Automatic Syntax Validation After Edits\n# ================================================================\n# PURPOSE:\n# Runs syntax checks immediately after Claude Code edits or\n# writes a file. Catches syntax errors before they propagate\n# into downstream failures.\n#\n# SUPPORTED LANGUAGES:\n# .py — python -m py_compile\n# .sh — bash -n\n# .bash — bash -n\n# .json — jq empty\n# .yaml — python3 yaml.safe_load (if PyYAML installed)\n# .yml — python3 yaml.safe_load (if PyYAML installed)\n# .js — node --check (if node installed)\n# .ts — npx tsc --noEmit (if tsc available) [EXPERIMENTAL]\n#\n# TRIGGER: PostToolUse\n# MATCHER: \"Edit|Write\"\n#\n# DESIGN PHILOSOPHY (2026-07-27改訂):\n# - エラー検出時は exit 2 で終える。PostToolUseのexit 2はブロックを起こさず、\n# stderrがモデルの文脈に届く(exit 0のstdoutはtranscript表示のみでモデル不達\n# =従来設計では「即座に知らせる」という自己目的を達成できていなかった。フック監査で実測)\n# - Never blocks the edit itself — reports errors but doesn't\n# prevent the edit from completing\n# - Silent on success — only speaks up when something is wrong\n# - Fails open — if a checker isn't installed, silently skips\n#\n# BORN FROM:\n# Countless sessions where Claude Code introduced a syntax error,\n# continued working for 10+ tool calls, then hit a wall when\n# trying to run the broken file. Catching it immediately saves\n# context window and frustration.\n# ================================================================\n\nINPUT=$(cat)\nSYNTAX_FAIL=0\nFILE_PATH=$(printf '%s' \"$INPUT\" | jq -r '.tool_input.file_path // empty' 2>/dev/null)\n\n# No file path = nothing to check\nif [[ -z \"$FILE_PATH\" || ! -f \"$FILE_PATH\" ]]; then\n exit 0\nfi\n\nEXT=\"${FILE_PATH##*.}\"\n\ncase \"$EXT\" in\n py)\n if python3 -m py_compile \"$FILE_PATH\" 2>&1; then\n : # silent on success\n else\n SYNTAX_FAIL=1; echo \"SYNTAX ERROR (Python): $FILE_PATH\" >&2\n fi\n ;;\n sh|bash)\n if bash -n \"$FILE_PATH\" 2>&1; then\n :\n else\n SYNTAX_FAIL=1; echo \"SYNTAX ERROR (Shell): $FILE_PATH\" >&2\n fi\n ;;\n json)\n if command -v jq &>/dev/null; then\n if jq empty \"$FILE_PATH\" 2>&1; then\n :\n else\n SYNTAX_FAIL=1; echo \"SYNTAX ERROR (JSON): $FILE_PATH\" >&2\n fi\n fi\n ;;\n yaml|yml)\n if python3 -c \"import yaml\" 2>/dev/null; then\n if python3 -c \"\nimport yaml, sys\nwith open(sys.argv[1]) as f:\n yaml.safe_load(f)\n\" \"$FILE_PATH\" 2>&1; then\n :\n else\n SYNTAX_FAIL=1; echo \"SYNTAX ERROR (YAML): $FILE_PATH\" >&2\n fi\n fi\n ;;\n js)\n if command -v node &>/dev/null; then\n if node --check \"$FILE_PATH\" 2>&1; then\n :\n else\n SYNTAX_FAIL=1; echo \"SYNTAX ERROR (JavaScript): $FILE_PATH\" >&2\n fi\n fi\n ;;\n ts)\n # EXPERIMENTAL: TypeScript check requires tsc in PATH\n if command -v npx &>/dev/null; then\n # --no-install: tsc未導入時に対話プロンプトで停止する危険を防ぐ(2026-07-27)\n if npx --no-install tsc --noEmit \"$FILE_PATH\" 2>&1; then\n :\n else\n SYNTAX_FAIL=1; echo \"SYNTAX ERROR (TypeScript) [experimental]: $FILE_PATH\" >&2\n fi\n fi\n ;;\n *)\n # Unknown extension — skip silently\n ;;\nesac\n\n# エラー検出時のみexit 2(PostToolUseなので編集はブロックされず、stderrがモデルに届く)\n[ \"$SYNTAX_FAIL\" = \"1\" ] && exit 2\nexit 0\n",
|
|
5
5
|
"context-monitor": "#!/bin/bash\n# ================================================================\n# context-monitor.sh — Context Window Remaining Capacity Monitor\n# ================================================================\n# PURPOSE:\n# Monitors how much context window remains during a Claude Code\n# session. Issues graduated warnings (CAUTION → WARNING → CRITICAL\n# → EMERGENCY) so you never get killed by context exhaustion.\n#\n# HOW IT WORKS:\n# 1. Reads Claude Code's debug log to extract actual token usage\n# 2. Falls back to tool-call-count estimation when debug logs\n# are unavailable\n# 3. Saves current % to /tmp/cc-context-pct (other scripts can\n# read this)\n# 4. At CRITICAL/EMERGENCY, writes an evacuation template to\n# your mission file so you can hand off state before /compact\n#\n# TRIGGER: PostToolUse (all tools)\n# MATCHER: \"\" (empty = every tool invocation)\n#\n# CONFIGURATION:\n# CC_CONTEXT_MISSION_FILE — path to your mission/state file\n# default: $HOME/mission.md\n#\n# THRESHOLDS (edit below to taste):\n# CAUTION = 40% — be mindful of consumption\n# WARNING = 25% — finish current task, save state\n# CRITICAL = 20% — run /compact immediately\n# EMERGENCY = 15% — stop everything, evacuate\n#\n# BORN FROM:\n# A session that hit 3% context remaining with no warning.\n# The agent died mid-task and all in-flight work was lost.\n# Never again.\n# ================================================================\n\nSTATE_FILE=\"/tmp/cc-context-state\"\nPCT_FILE=\"/tmp/cc-context-pct\"\nCOUNTER_FILE=\"/tmp/cc-context-monitor-count\"\nMISSION_FILE=\"${CC_CONTEXT_MISSION_FILE:-$HOME/mission.md}\"\n\n# Tool invocation counter (fallback estimator)\nCOUNT=$(cat \"$COUNTER_FILE\" 2>/dev/null || echo 0)\nCOUNT=$((COUNT + 1))\necho \"$COUNT\" > \"$COUNTER_FILE\"\n\n# Check every 3rd invocation to reduce overhead\n# (but always check in CRITICAL/EMERGENCY state)\nLAST_STATE=$(cat \"$STATE_FILE\" 2>/dev/null || echo \"normal\")\nif [ $((COUNT % 3)) -ne 0 ] && [ \"$LAST_STATE\" != \"critical\" ] && [ \"$LAST_STATE\" != \"emergency\" ]; then\n exit 0\nfi\n\n# --- Extract context % from Claude Code debug logs ---\nget_context_pct() {\n local debug_dir=\"$HOME/.claude/debug\"\n if [ ! -d \"$debug_dir\" ]; then\n echo \"\"\n return\n fi\n\n local latest\n latest=$(find \"$debug_dir\" -maxdepth 1 -name '*.txt' -printf '%T@ %p\\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2)\n if [ -z \"$latest\" ]; then\n echo \"\"\n return\n fi\n\n # Parse the last autocompact entry for token counts\n local line\n line=$(grep 'autocompact:' \"$latest\" 2>/dev/null | tail -1)\n if [ -z \"$line\" ]; then\n echo \"\"\n return\n fi\n\n local tokens window\n tokens=$(echo \"$line\" | sed 's/.*tokens=\\([0-9]*\\).*/\\1/')\n window=$(echo \"$line\" | sed 's/.*effectiveWindow=\\([0-9]*\\).*/\\1/')\n\n if [ -n \"$tokens\" ] && [ -n \"$window\" ] && [ \"$window\" -gt 0 ] 2>/dev/null; then\n local pct\n pct=$(( (window - tokens) * 100 / window ))\n echo \"$pct\"\n else\n echo \"\"\n fi\n}\n\nCONTEXT_PCT=$(get_context_pct)\n\n# Fallback: estimate from tool call count when debug logs unavailable\n# Assumes ~180 tool calls fills ~100% of context (conservative)\nif [ -z \"$CONTEXT_PCT\" ]; then\n CONTEXT_PCT=$(( 100 - (COUNT * 100 / 180) ))\n if [ \"$CONTEXT_PCT\" -lt 0 ]; then CONTEXT_PCT=0; fi\n SOURCE=\"estimate\"\nelse\n SOURCE=\"debug\"\nfi\n\necho \"$CONTEXT_PCT\" > \"$PCT_FILE\"\n\nTIMESTAMP=$(date '+%Y-%m-%d %H:%M')\n\n# --- Evacuation template (with cooldown to prevent spam) ---\nEVAC_COOLDOWN_FILE=\"/tmp/cc-context-evac-last\"\nEVAC_COOLDOWN_SEC=1800 # 30 min cooldown between template generations\n\ngenerate_evacuation_template() {\n local level=\"$1\"\n\n # Cooldown check\n if [ -f \"$EVAC_COOLDOWN_FILE\" ]; then\n local last_ts now_ts diff\n last_ts=$(cat \"$EVAC_COOLDOWN_FILE\" 2>/dev/null || echo 0)\n now_ts=$(date +%s)\n diff=$((now_ts - last_ts))\n if [ \"$diff\" -lt \"$EVAC_COOLDOWN_SEC\" ]; then\n return\n fi\n fi\n\n # Don't add a new template if there's already an unfilled one\n if [ -f \"$MISSION_FILE\" ] && grep -q '\\[TODO\\]' \"$MISSION_FILE\" 2>/dev/null; then\n return\n fi\n\n date +%s > \"$EVAC_COOLDOWN_FILE\"\n\n # Create mission file directory if needed\n mkdir -p \"$(dirname \"$MISSION_FILE\")\"\n\n cat >> \"$MISSION_FILE\" << EVAC_EOF\n\n## Context Evacuation Template (${level} - ${TIMESTAMP})\n<!-- Auto-generated by context-monitor.sh. Fill in before /compact -->\n### Current Task\n- Task: [TODO]\n- Progress: [TODO]\n- Files being edited: [TODO]\n\n### Git State\n- Branch: [TODO]\n- Uncommitted changes: [TODO]\n\n### Next Action\n- Next command/action: [TODO]\nEVAC_EOF\n}\n\n# --- Graduated warnings ---\nif [ \"$CONTEXT_PCT\" -le 15 ]; then\n # EMERGENCY\n if [ \"$LAST_STATE\" != \"emergency\" ]; then\n echo \"emergency\" > \"$STATE_FILE\"\n generate_evacuation_template \"EMERGENCY\"\n fi\n echo \"\"\n echo \"EMERGENCY: Context remaining ${CONTEXT_PCT}% (${SOURCE})\"\n echo \"Run /compact IMMEDIATELY. Evacuation template written to ${MISSION_FILE}.\"\n echo \"1. Fill in the [TODO] fields in the template\"\n echo \"2. Run /compact\"\n echo \"3. If needed, restart and resume from mission file\"\n echo \"No further work allowed. Evacuate only.\"\n\nelif [ \"$CONTEXT_PCT\" -le 20 ]; then\n # CRITICAL\n if [ \"$LAST_STATE\" != \"critical\" ]; then\n echo \"critical\" > \"$STATE_FILE\"\n generate_evacuation_template \"CRITICAL\"\n fi\n echo \"\"\n echo \"CRITICAL: Context remaining ${CONTEXT_PCT}% (${SOURCE})\"\n echo \"Run /compact IMMEDIATELY. Evacuation template written to ${MISSION_FILE}.\"\n echo \"1. Save current task state to the template\"\n echo \"2. Run /compact\"\n\nelif [ \"$CONTEXT_PCT\" -le 25 ]; then\n # WARNING\n if [ \"$LAST_STATE\" != \"warning\" ]; then\n echo \"warning\" > \"$STATE_FILE\"\n echo \"\"\n echo \"WARNING: Context remaining ${CONTEXT_PCT}% (${SOURCE})\"\n echo \"Do not start new large tasks. Finish current work and save state.\"\n fi\n\nelif [ \"$CONTEXT_PCT\" -le 40 ]; then\n # CAUTION\n if [ \"$LAST_STATE\" != \"caution\" ]; then\n echo \"caution\" > \"$STATE_FILE\"\n echo \"\"\n echo \"CAUTION: Context remaining ${CONTEXT_PCT}% (${SOURCE})\"\n echo \"Be mindful of context consumption. Keep interactions concise.\"\n fi\nfi\n\nexit 0\n",
|
|
6
|
-
"comment-strip": "#!/bin/bash\n# ================================================================\n# comment-strip.sh — Strip bash comments that break permissions\n# ================================================================\n# PURPOSE:\n# Claude Code sometimes adds comments to bash commands like:\n# # Check the diff\n# git diff HEAD~1\n# This breaks permission allowlists (e.g. Bash(git:*)) because\n# the matcher sees \"# Check the diff\" instead of \"git diff\".\n#\n# This hook strips leading comment lines and returns the clean\n# command via updatedInput, so permissions match correctly.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# INCIDENT: GitHub Issue #29582 (18 reactions)\n# Users on linux/vscode report that bash comments added by Claude\n# cause permission prompts even when the command is allowlisted.\n#\n# HOW IT WORKS:\n# - Reads the command from tool_input\n# - Strips leading lines that start with #\n# - Strips trailing comments (everything after # on command lines)\n# - Returns updatedInput with the cleaned command\n# - Uses hookSpecificOutput.permissionDecision = \"allow\" only if\n# the command was modified (so it doesn't override other hooks)\n# ================================================================\n\nINPUT=$(cat)\
|
|
7
|
-
"cd-git-allow": "#!/bin/bash\n# ================================================================\n# cd-git-allow.sh — Auto-approve cd+git compound commands\n# ================================================================\n# PURPOSE:\n# Claude Code shows \"Compound commands with cd and git require\n# approval\" for commands like: cd /path && git log\n# This is safe in trusted project directories but causes\n# constant permission prompts.\n#\n# This hook auto-approves cd+git compounds when the git operation\n# is read-only (log, diff, status, branch, show, etc.)\n# Destructive git operations (push, reset, clean) are NOT\n# auto-approved — they still require manual approval.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# INCIDENT: GitHub Issue #32985 (9 reactions)\n#\n# WHAT IT AUTO-APPROVES:\n# - cd /path && git log\n# - cd /path && git diff\n# - cd /path && git status\n# - cd /path && git branch\n# - cd /path && git show\n# - cd /path && git rev-parse\n#\n# WHAT IT DOES NOT APPROVE (still prompts):\n# - cd /path && git push\n# - cd /path && git reset --hard\n# - cd /path && git clean\n# - cd /path && git checkout (could discard changes)\n# ================================================================\n\nINPUT=$(cat)\
|
|
8
|
-
"secret-guard": "#!/bin/bash\n# ================================================================\n# secret-guard.sh — Secret/Credential Leak Prevention\n# ================================================================\n# PURPOSE:\n# Prevents accidental exposure of secrets, API keys, and\n# credentials through git commits or shell output.\n#\n# Catches the most common ways secrets leak:\n# - git add .env (committing env files)\n# - git add credentials.json / *.pem / *.key\n# - echo $API_KEY or printenv (exposing secrets in output)\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# WHAT IT BLOCKS (exit 2):\n# - git add .env / .env.local / .env.production\n# - git add *credentials* / *secret* / *.pem / *.key\n# - git add -A or git add . when .env exists (warns)\n#\n# WHAT IT ALLOWS (exit 0):\n# - git add specific safe files\n# - Reading .env for application use (not committing)\n# - All non-git-add commands\n#\n# CONFIGURATION:\n# CC_SECRET_PATTERNS — colon-separated additional patterns to block\n# default: \".env:.env.local:.env.production:credentials:secret:*.pem:*.key:*.p12\"\n# ================================================================\n\nINPUT=$(cat)\nCOMMAND=$(printf '%s' \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null)\n\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# --- Check 1: git add of secret files ---\nif printf '%s' \"$COMMAND\" | grep -qE '^\\s*git\\s+add'; then\n # Direct .env file staging\n if printf '%s' \"$COMMAND\" | grep -qiE 'git\\s+add\\s+.*\\.env(\\s|$|\\.|/)'; then\n echo \"BLOCKED: Attempted to stage .env file.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \".env files contain secrets and should never be committed.\" >&2\n echo \"Add .env to .gitignore instead.\" >&2\n exit 2\n fi\n\n # Credential/key files\n if printf '%s' \"$COMMAND\" | grep -qiE 'git\\s+add\\s+.*(credentials|\\.pem|\\.key|\\.p12|\\.pfx|id_rsa|id_ed25519)'; then\n echo \"BLOCKED: Attempted to stage credential/key file.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Key and credential files should never be committed to git.\" >&2\n echo \"Add them to .gitignore instead.\" >&2\n exit 2\n fi\n\n # git add -A or git add . when .env exists — warn but check\n if printf '%s' \"$COMMAND\" | grep -qE 'git\\s+add\\s+(-A|--all|\\.)(\\s|$)'; then\n # Check if .env exists in the current or project directory\n if [ -f \".env\" ] || [ -f \".env.local\" ] || [ -f \".env.production\" ]; then\n echo \"BLOCKED: 'git add .' with .env file present.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"An .env file exists in this directory. 'git add .' would stage it.\" >&2\n echo \"Add specific files instead: git add src/ lib/ package.json\" >&2\n echo \"Or add .env to .gitignore first.\" >&2\n exit 2\n fi\n fi\nfi\n\nexit 0\n",
|
|
9
|
-
"api-error-alert": "#!/bin/bash\nINPUT=$(cat)\
|
|
10
|
-
"clear-command-confirm-guard": "#!/bin/bash\n# clear-command-confirm-guard.sh — Block accidental /clear command\n#\n# Solves: /clear destroys all conversation context with zero\n# confirmation. Prefix matching means /c + Enter can\n# accidentally trigger /clear instead of /commit or /compact (#40931).\n#\n# How it works: Blocks /clear entirely. Use /compact to reduce\n# context without losing it.\n#\n# TRIGGER: UserPromptSubmit\n# MATCHER: \"^/clear$\"\n\nINPUT=$(cat)\
|
|
6
|
+
"comment-strip": "#!/bin/bash\n# ================================================================\n# comment-strip.sh — Strip bash comments that break permissions\n# ================================================================\n# PURPOSE:\n# Claude Code sometimes adds comments to bash commands like:\n# # Check the diff\n# git diff HEAD~1\n# This breaks permission allowlists (e.g. Bash(git:*)) because\n# the matcher sees \"# Check the diff\" instead of \"git diff\".\n#\n# This hook strips leading comment lines and returns the clean\n# command via updatedInput, so permissions match correctly.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# INCIDENT: GitHub Issue #29582 (18 reactions)\n# Users on linux/vscode report that bash comments added by Claude\n# cause permission prompts even when the command is allowlisted.\n#\n# HOW IT WORKS:\n# - Reads the command from tool_input\n# - Strips leading lines that start with #\n# - Strips trailing comments (everything after # on command lines)\n# - Returns updatedInput with the cleaned command\n# - Uses hookSpecificOutput.permissionDecision = \"allow\" only if\n# the command was modified (so it doesn't override other hooks)\n# ================================================================\n\nINPUT=$(cat)\n\n# --- Reading the hook payload -------------------------------------------------\n# Why a chain instead of jq alone: on a machine without jq the read returned an\n# empty string, every check below was skipped, and this hook exited 0 in silence.\n# It stayed listed in settings.json and protected nothing. Measured 2026-07-27:\n# a destructive command passed with exit 0 and no output at all.\n# If none of the three readers exist we warn once and allow, because blocking\n# every Bash call would make Claude Code unusable — stopping all work is a\n# failure, not safety.\ncc_read_field() {\n if command -v jq >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | jq -r \".$1 // empty\" 2>/dev/null\n elif command -v python3 >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" python3 -c 'import sys,json,os\ntry:\n d=json.load(sys.stdin)\nexcept Exception:\n sys.exit(0)\nfor k in os.environ.get(\"CC_FIELD\",\"\").split(\".\"):\n d = d.get(k) if isinstance(d,dict) else None\nsys.stdout.write(\"\" if d is None else str(d))\n' 2>/dev/null\n elif command -v node >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" node -e 'let s=\"\";process.stdin.on(\"data\",d=>s+=d).on(\"end\",()=>{try{let o=JSON.parse(s);for(const k of (process.env.CC_FIELD||\"\").split(\".\"))o=(o&&typeof o===\"object\")?o[k]:undefined;process.stdout.write(o==null?\"\":String(o))}catch(e){}})' 2>/dev/null\n else\n printf '%s' '__CC_NO_PARSER__'\n fi\n}\n\ncc_warn_no_parser() {\n marker=\"${TMPDIR:-/tmp}/cc-noparser-warned-comment-strip-$PPID\"\n [ -f \"$marker\" ] && return 0\n : > \"$marker\" 2>/dev/null\n echo \"WARNING [comment-strip]: jq, python3 and node are all unavailable, so this hook cannot read the tool call and is NOT protecting you. Install any one of them.\" >&2\n}\n\nCOMMAND=$(cc_read_field tool_input.command)\n\nif [ \"$COMMAND\" = \"__CC_NO_PARSER__\" ]; then\n\n cc_warn_no_parser\n\n exit 0\n\nfi\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# Strip leading comment lines and empty lines\nCLEAN=$(printf '%s' \"$COMMAND\" | sed '/^[[:space:]]*#/d; /^[[:space:]]*$/d')\n\n# If nothing changed, pass through\nif [[ \"$CLEAN\" == \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# If command is empty after stripping, don't modify\nif [[ -z \"$CLEAN\" ]]; then\n exit 0\nfi\n\n# Return cleaned command via hookSpecificOutput\n# permissionDecision is not set — let the normal permission flow handle it\n# We only modify the input so the permission matcher sees the real command\njq -n --arg cmd \"$CLEAN\" '{\n hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n updatedInput: {\n command: $cmd\n }\n }\n}'\n",
|
|
7
|
+
"cd-git-allow": "#!/bin/bash\n# ================================================================\n# cd-git-allow.sh — Auto-approve cd+git compound commands\n# ================================================================\n# PURPOSE:\n# Claude Code shows \"Compound commands with cd and git require\n# approval\" for commands like: cd /path && git log\n# This is safe in trusted project directories but causes\n# constant permission prompts.\n#\n# This hook auto-approves cd+git compounds when the git operation\n# is read-only (log, diff, status, branch, show, etc.)\n# Destructive git operations (push, reset, clean) are NOT\n# auto-approved — they still require manual approval.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# INCIDENT: GitHub Issue #32985 (9 reactions)\n#\n# WHAT IT AUTO-APPROVES:\n# - cd /path && git log\n# - cd /path && git diff\n# - cd /path && git status\n# - cd /path && git branch\n# - cd /path && git show\n# - cd /path && git rev-parse\n#\n# WHAT IT DOES NOT APPROVE (still prompts):\n# - cd /path && git push\n# - cd /path && git reset --hard\n# - cd /path && git clean\n# - cd /path && git checkout (could discard changes)\n# ================================================================\n\nINPUT=$(cat)\n\n# --- Reading the hook payload -------------------------------------------------\n# Why a chain instead of jq alone: on a machine without jq the read returned an\n# empty string, every check below was skipped, and this hook exited 0 in silence.\n# It stayed listed in settings.json and protected nothing. Measured 2026-07-27:\n# a destructive command passed with exit 0 and no output at all.\n# If none of the three readers exist we warn once and allow, because blocking\n# every Bash call would make Claude Code unusable — stopping all work is a\n# failure, not safety.\ncc_read_field() {\n if command -v jq >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | jq -r \".$1 // empty\" 2>/dev/null\n elif command -v python3 >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" python3 -c 'import sys,json,os\ntry:\n d=json.load(sys.stdin)\nexcept Exception:\n sys.exit(0)\nfor k in os.environ.get(\"CC_FIELD\",\"\").split(\".\"):\n d = d.get(k) if isinstance(d,dict) else None\nsys.stdout.write(\"\" if d is None else str(d))\n' 2>/dev/null\n elif command -v node >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" node -e 'let s=\"\";process.stdin.on(\"data\",d=>s+=d).on(\"end\",()=>{try{let o=JSON.parse(s);for(const k of (process.env.CC_FIELD||\"\").split(\".\"))o=(o&&typeof o===\"object\")?o[k]:undefined;process.stdout.write(o==null?\"\":String(o))}catch(e){}})' 2>/dev/null\n else\n printf '%s' '__CC_NO_PARSER__'\n fi\n}\n\ncc_warn_no_parser() {\n marker=\"${TMPDIR:-/tmp}/cc-noparser-warned-cd-git-allow-$PPID\"\n [ -f \"$marker\" ] && return 0\n : > \"$marker\" 2>/dev/null\n echo \"WARNING [cd-git-allow]: jq, python3 and node are all unavailable, so this hook cannot read the tool call and is NOT protecting you. Install any one of them.\" >&2\n}\n\nCOMMAND=$(cc_read_field tool_input.command)\n\nif [ \"$COMMAND\" = \"__CC_NO_PARSER__\" ]; then\n\n cc_warn_no_parser\n\n exit 0\n\nfi\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# Only handle compounds that start with a cd\nif ! printf '%s' \"$COMMAND\" | grep -qE '^[[:space:]]*cd([[:space:]]|$)'; then\n exit 0\nfi\n\n# Read-only git operations — safe to auto-approve\nSAFE_GIT=\"log diff status branch show rev-parse tag remote stash-list describe name-rev\"\n\n# Every command position after the cd has to be a read-only git call.\n#\n# Rewritten 2026-08-03. The old version pulled the first `&& git <sub>` out of\n# the line and approved on that alone, so `cd /repo && git log && sudo rm -rf app`\n# matched the shape, produced the subcommand `log`, and was handed an explicit\n# permissionDecision \"allow\" -- the tail was never looked at. Measured against\n# the shipped copy: allow was returned for that line and for one ending in\n# `curl ... | sh`. Same defect as PR #937, but on the approving side rather than\n# the blocking side, which is worse: the decision is an explicit approval, not a\n# missed block.\n#\n# Anything that does not qualify falls through with no decision, so it lands in\n# the normal permission flow. This hook only ever adds approval; it never blocks.\n#\n# Splitting on the separator characters also drops the `grep -oP` that used to\n# extract the subcommand. -P is GNU-only, and on macOS it returned nothing, so\n# the loop below never matched and the hook quietly did nothing.\nSEG_OK=1\nGIT_SEEN=0\nFIRST=1\nwhile IFS= read -r SEG; do\n SEG=$(printf '%s' \"$SEG\" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')\n [ -z \"$SEG\" ] && continue\n\n if [ \"$FIRST\" = 1 ]; then\n FIRST=0\n # the leading segment is the cd itself\n printf '%s' \"$SEG\" | grep -qE '^cd([[:space:]]|$)' || { SEG_OK=0; break; }\n continue\n fi\n\n printf '%s' \"$SEG\" | grep -qE '^git[[:space:]]' || { SEG_OK=0; break; }\n SUB=$(printf '%s' \"$SEG\" | sed -E 's/^git[[:space:]]+//; s/[[:space:]].*$//')\n MATCH=0\n for safe in $SAFE_GIT; do\n if [ \"$SUB\" = \"$safe\" ]; then MATCH=1; break; fi\n done\n if [ \"$MATCH\" != 1 ]; then SEG_OK=0; break; fi\n GIT_SEEN=1\ndone <<EOF\n$(printf '%s' \"$COMMAND\" | tr ';&|' '\\n\\n\\n')\nEOF\n\nif [ \"$SEG_OK\" = 1 ] && [ \"$GIT_SEEN\" = 1 ]; then\n jq -n '{\n hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: \"allow\",\n permissionDecisionReason: \"cd+git compound auto-approved (read-only git operations only)\"\n }\n }'\n exit 0\nfi\n\n# Not a read-only cd+git compound — let normal permission flow handle it\nexit 0\n",
|
|
8
|
+
"secret-guard": "#!/bin/bash\n# ================================================================\n# secret-guard.sh — Secret/Credential Leak Prevention\n# ================================================================\n# PURPOSE:\n# Prevents accidental exposure of secrets, API keys, and\n# credentials through git commits or shell output.\n#\n# Catches the most common ways secrets leak:\n# - git add .env (committing env files)\n# - git add credentials.json / *.pem / *.key\n# - echo $API_KEY or printenv (exposing secrets in output)\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# WHAT IT BLOCKS (exit 2):\n# - git add .env / .env.local / .env.production\n# - git add *credentials* / *secret* / *.pem / *.key\n# - git add -A or git add . when .env exists (warns)\n#\n# WHAT IT ALLOWS (exit 0):\n# - git add specific safe files\n# - Reading .env for application use (not committing)\n# - All non-git-add commands\n#\n# WHAT IT CANNOT DO:\n# - `git add` is the only stage it watches. `git commit` and `git push` of an\n# already-tracked secret are outside it; see examples/staged-secret-scan.sh.\n# - The bulk check (`git add .` / `-A`) tests for .env in the hook's own\n# working directory only. A secret one level down (backend/.env) is not seen.\n# - Matching is on the text of the command, not on what git would resolve. A\n# file named secretsManager.ts is blocked; a secret named config.yml is not.\n#\n# CONFIGURATION:\n# CC_SECRET_PATTERNS — colon-separated extra patterns to block, glob-style\n# (`*` matches a run of non-space). These are ADDITIONAL to the built-in\n# checks above; unset means built-ins only. Example:\n# CC_SECRET_PATTERNS=\".npmrc:.netrc:*.jks:secring.*\"\n# Read since 2026-09-04. Every earlier version documented this key and\n# never read it, so anyone who set it got no error and no enforcement.\n# ================================================================\n\nINPUT=$(cat)\n\n# --- Reading the hook payload -------------------------------------------------\n# Why a chain instead of jq alone: on a machine without jq the read returned an\n# empty string, every check below was skipped, and this hook exited 0 in silence.\n# It stayed listed in settings.json and protected nothing. Measured 2026-07-27:\n# a destructive command passed with exit 0 and no output at all.\n# If none of the three readers exist we warn once and allow, because blocking\n# every Bash call would make Claude Code unusable — stopping all work is a\n# failure, not safety.\ncc_read_field() {\n if command -v jq >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | jq -r \".$1 // empty\" 2>/dev/null\n elif command -v python3 >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" python3 -c 'import sys,json,os\ntry:\n d=json.load(sys.stdin)\nexcept Exception:\n sys.exit(0)\nfor k in os.environ.get(\"CC_FIELD\",\"\").split(\".\"):\n d = d.get(k) if isinstance(d,dict) else None\nsys.stdout.write(\"\" if d is None else str(d))\n' 2>/dev/null\n elif command -v node >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" node -e 'let s=\"\";process.stdin.on(\"data\",d=>s+=d).on(\"end\",()=>{try{let o=JSON.parse(s);for(const k of (process.env.CC_FIELD||\"\").split(\".\"))o=(o&&typeof o===\"object\")?o[k]:undefined;process.stdout.write(o==null?\"\":String(o))}catch(e){}})' 2>/dev/null\n else\n printf '%s' '__CC_NO_PARSER__'\n fi\n}\n\ncc_warn_no_parser() {\n marker=\"${TMPDIR:-/tmp}/cc-noparser-warned-secret-guard-$PPID\"\n [ -f \"$marker\" ] && return 0\n : > \"$marker\" 2>/dev/null\n echo \"WARNING [secret-guard]: jq, python3 and node are all unavailable, so this hook cannot read the tool call and is NOT protecting you. Install any one of them.\" >&2\n}\n\nCOMMAND=$(cc_read_field tool_input.command)\n\n\n# Log function — records blocked commands for audit.\n# Added 2026-09-03: the book tells operators to read this log to learn what was\n# stopped, but only destructive-guard was writing to it. A force push or a staged\n# .env was refused and left no trace, so an empty log read as \"nothing happened\".\nlog_block() {\n local reason=\"$1\"\n local logfile=\"${CC_BLOCK_LOG:-$HOME/.claude/blocked-commands.log}\"\n mkdir -p \"$(dirname \"$logfile\")\" 2>/dev/null\n echo \"[$(date -Iseconds)] BLOCKED: $reason | cmd: $COMMAND\" >> \"$logfile\" 2>/dev/null\n}\n\nif [ \"$COMMAND\" = \"__CC_NO_PARSER__\" ]; then\n\n cc_warn_no_parser\n\n exit 0\n\nfi\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# --- a trailing backslash makes the next line part of the same command -------\n# Added 2026-09-03. The shell joins `\\` + newline before it runs anything, so\n# `rm -rf \\` / ` ~/Documents` is one deletion. grep is line-oriented and saw\n# two unrelated lines, matched neither, and this hook exited 0. Measured against\n# the shipped copy: the rm, the chmod 777 and every git check could all be\n# stepped over this way. 0.9% of 43,858 real Bash calls in this operator's own\n# history use a line continuation, so this is ordinary typing, not an exploit.\n# Join them once, up front, and every check below sees what the shell will run.\ncc_join_continuations() {\n local s=$1\n s=${s//\\\\$'\\n'/ }\n printf '%s' \"$s\"\n}\nCOMMAND=$(cc_join_continuations \"$COMMAND\")\n\n# --- git's global options sit between `git` and the verb ----------------------\n# Added 2026-09-03. Every git check in this hook expected the verb to be next to\n# the word `git`, so `git -C /repo push --force` matched none of them and the\n# hook exited 0 -- an affirmative approval. Measured against the shipped copy on\n# 2026-09-03: 4 bypasses here, 2 in secret-guard, 4 in destructive-guard. git\n# accepts these options anywhere before the verb and any number of times, so\n# strip them once and let the patterns below stay readable.\n# The messages still print $COMMAND, because the operator needs to see what they\n# actually typed, not our normalised copy.\ncc_strip_git_globals() {\n if ! command -v sed >/dev/null 2>&1; then printf '%s' \"$1\"; return; fi\n printf '%s' \"$1\" | sed -E ':a; s/(^|[;&|[:space:]])git[[:space:]]+((-C|-c)[[:space:]]+[^[:space:]]+|--(git-dir|work-tree|namespace|exec-path|super-prefix|config-env|attr-source)(=[^[:space:]]+|[[:space:]]+[^[:space:]]+)|--(no-pager|paginate|bare|literal-pathspecs|glob-pathspecs|noglob-pathspecs|icase-pathspecs|no-replace-objects|no-optional-locks|no-lazy-fetch|no-advice)|-[pP])[[:space:]]+/\\1git /g; ta'\n}\n\nCHECK_CMD=$(cc_strip_git_globals \"$COMMAND\")\n# Never let a failed normalisation turn every check below into a silent pass.\n[ -z \"$CHECK_CMD\" ] && CHECK_CMD=\"$COMMAND\"\n\n# --- `git` does not have to be the first word of the segment ------------------\n# Added 2026-09-14. The gate below anchored on `(^|;|&&|\\|\\|)\\s*git`, so an\n# invocation that reached git through a path or a prefix walked past it and this\n# hook exited 0 -- an affirmative approval. Measured against the shipped copy the\n# same day, with nine invocation forms against each subcommand: destructive-guard\n# blocked 9 of 9 (it had been fixed in #1121), this hook blocked 2 of 9. The seven\n# that passed were `/usr/bin/git`, `./git`, `sudo git`, `env FOO=1 git`,\n# `FOO=1 git`, `time git` and `nohup git`.\n# CC_CMDPOS is the prefix destructive-guard already uses, copied verbatim so the\n# three guards recognise the same set of invocations.\nCC_CMDPOS='(^|[;&|`(){}]|\\$\\(|[[:space:]](then|do|else|elif)[[:space:]])[[:space:]]*([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+|(env|nice|ionice|timeout|exec|command|builtin|stdbuf|nohup|sudo|time|xargs)([[:space:]]+-[^[:space:]]+)*([[:space:]]+[0-9]+)?([[:space:]]+-[^[:space:]]+)*[[:space:]]+)*([./]*[A-Za-z0-9_./-]*/)?'\n\n# --- Check 1: git add of secret files ---\nif printf '%s' \"$CHECK_CMD\" | grep -qE \"${CC_CMDPOS}\"'git\\s+add'; then\n # Direct .env file staging\n if printf '%s' \"$CHECK_CMD\" | grep -qiE 'git\\s+add\\s+.*\\.env(\\s|$|\\.|/)'; then\n log_block \"Staging .env file\"\n echo \"BLOCKED: Attempted to stage .env file.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \".env files contain secrets and should never be committed.\" >&2\n echo \"Add .env to .gitignore instead.\" >&2\n exit 2\n fi\n\n # Credential/key files\n if printf '%s' \"$CHECK_CMD\" | grep -qiE 'git\\s+add\\s+.*(credentials|secret|\\.pem|\\.key|\\.p12|\\.pfx|id_rsa|id_ed25519)'; then\n log_block \"Staging credential/key file\"\n echo \"BLOCKED: Attempted to stage credential/key file.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Key and credential files should never be committed to git.\" >&2\n echo \"Add them to .gitignore instead.\" >&2\n exit 2\n fi\n\n # Operator-supplied patterns. Documented in the header since the first\n # release, never read until 2026-09-04. A configuration key that the body\n # ignores is worse than no key at all: the operator sets it, sees no error,\n # and believes the extra patterns are being enforced.\n # Colon-separated, glob-style (`*` matches any run of non-space). Entries\n # are additional to the built-in checks above, never a replacement, so a\n # malformed value can only fail to block -- it can never unblock .env.\n if [ -n \"${CC_SECRET_PATTERNS:-}\" ]; then\n cc_extra_alt=\"\"\n # POSIX field splitting on ':' without touching the caller's IFS.\n cc_old_ifs=$IFS\n IFS=':'\n for cc_pat in $CC_SECRET_PATTERNS; do\n [ -z \"$cc_pat\" ] && continue\n # Escape every ERE metacharacter, then turn the glob star back on.\n cc_esc=$(printf '%s' \"$cc_pat\" | sed -e 's/[][\\\\.^$()|+?{}]/\\\\&/g' -e 's/\\*/[^[:space:]]*/g')\n if [ -z \"$cc_extra_alt\" ]; then cc_extra_alt=\"$cc_esc\"; else cc_extra_alt=\"$cc_extra_alt|$cc_esc\"; fi\n done\n IFS=$cc_old_ifs\n if [ -n \"$cc_extra_alt\" ] && printf '%s' \"$CHECK_CMD\" | grep -qiE \"git[[:space:]]+add[[:space:]]+.*($cc_extra_alt)\"; then\n log_block \"Staging a file matching CC_SECRET_PATTERNS\"\n echo \"BLOCKED: Attempted to stage a file matching CC_SECRET_PATTERNS.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"CC_SECRET_PATTERNS=$CC_SECRET_PATTERNS\" >&2\n echo \"Add the file to .gitignore, or narrow CC_SECRET_PATTERNS.\" >&2\n exit 2\n fi\n fi\n\n # git add -A or git add . when .env exists — warn but check\n if printf '%s' \"$CHECK_CMD\" | grep -qE 'git\\s+add\\s+(-A|--all|\\.)(\\s|$)'; then\n # Check if .env exists in the current or project directory\n if [ -f \".env\" ] || [ -f \".env.local\" ] || [ -f \".env.production\" ]; then\n log_block \"git add . with .env present\"\n echo \"BLOCKED: 'git add .' with .env file present.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"An .env file exists in this directory. 'git add .' would stage it.\" >&2\n echo \"Add specific files instead: git add src/ lib/ package.json\" >&2\n echo \"Or add .env to .gitignore first.\" >&2\n exit 2\n fi\n fi\nfi\n\nexit 0\n",
|
|
9
|
+
"api-error-alert": "#!/bin/bash\nINPUT=$(cat)\n\n# --- Reading the hook payload -------------------------------------------------\n# Why a chain instead of jq alone: on a machine without jq the read returned an\n# empty string, every check below was skipped, and this hook exited 0 in silence.\n# It stayed listed in settings.json and protected nothing. Measured 2026-07-27:\n# a destructive command passed with exit 0 and no output at all.\n# If none of the three readers exist we warn once and allow, because blocking\n# every Bash call would make Claude Code unusable — stopping all work is a\n# failure, not safety.\ncc_read_field() {\n if command -v jq >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | jq -r \".$1 // empty\" 2>/dev/null\n elif command -v python3 >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" python3 -c 'import sys,json,os\ntry:\n d=json.load(sys.stdin)\nexcept Exception:\n sys.exit(0)\nfor k in os.environ.get(\"CC_FIELD\",\"\").split(\".\"):\n d = d.get(k) if isinstance(d,dict) else None\nsys.stdout.write(\"\" if d is None else str(d))\n' 2>/dev/null\n elif command -v node >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" node -e 'let s=\"\";process.stdin.on(\"data\",d=>s+=d).on(\"end\",()=>{try{let o=JSON.parse(s);for(const k of (process.env.CC_FIELD||\"\").split(\".\"))o=(o&&typeof o===\"object\")?o[k]:undefined;process.stdout.write(o==null?\"\":String(o))}catch(e){}})' 2>/dev/null\n else\n printf '%s' '__CC_NO_PARSER__'\n fi\n}\n\ncc_warn_no_parser() {\n marker=\"${TMPDIR:-/tmp}/cc-noparser-warned-api-error-alert-$PPID\"\n [ -f \"$marker\" ] && return 0\n : > \"$marker\" 2>/dev/null\n echo \"WARNING [api-error-alert]: jq, python3 and node are all unavailable, so this hook cannot read the tool call and is NOT protecting you. Install any one of them.\" >&2\n}\n\nREASON=$(cc_read_field stop_reason)\n\nif [ \"$REASON\" = \"__CC_NO_PARSER__\" ]; then\n\n cc_warn_no_parser\n\n exit 0\n\nfi\n[ -z \"$REASON\" ] && REASON=\"unknown\"\nHOOK_EVENT=$(cc_read_field hook_event_name)\n[ -z \"$HOOK_EVENT\" ] && HOOK_EVENT=\"\"\nif [[ \"$REASON\" == \"user\" || \"$REASON\" == \"normal\" || -z \"$REASON\" ]]; then\n exit 0\nfi\nLOG=\"${CC_ERROR_ALERT_LOG:-$HOME/.claude/session-errors.log}\"\nMISSION=\"${CC_CONTEXT_MISSION_FILE:-$HOME/mission.md}\"\nTS=$(date -Iseconds)\nmkdir -p \"$(dirname \"$LOG\")\" 2>/dev/null\necho \"[$TS] Session stopped: reason=$REASON event=$HOOK_EVENT\" >> \"$LOG\"\nif [ -z \"$WSL_DISTRO_NAME\" ]; then\n notify-send \"Claude Code\" \"Session stopped: $REASON\" 2>/dev/null || true\n osascript -e \"display notification \\\"Session stopped: $REASON\\\" with title \\\"Claude Code\\\"\" 2>/dev/null || true\nelse\n powershell.exe -Command \"Write-Host 'Claude Code: Session stopped - $REASON'\" 2>/dev/null || true\nfi\nexit 0\n",
|
|
10
|
+
"clear-command-confirm-guard": "#!/bin/bash\n# clear-command-confirm-guard.sh — Block accidental /clear command\n#\n# Solves: /clear destroys all conversation context with zero\n# confirmation. Prefix matching means /c + Enter can\n# accidentally trigger /clear instead of /commit or /compact (#40931).\n#\n# How it works: Blocks /clear entirely. Use /compact to reduce\n# context without losing it.\n#\n# TRIGGER: UserPromptSubmit\n# MATCHER: \"^/clear$\"\n\nINPUT=$(cat)\n\n# --- Reading the hook payload -------------------------------------------------\n# Why a chain instead of jq alone: on a machine without jq the read returned an\n# empty string, every check below was skipped, and this hook exited 0 in silence.\n# It stayed listed in settings.json and protected nothing. Measured 2026-07-27:\n# a destructive command passed with exit 0 and no output at all.\n# If none of the three readers exist we warn once and allow, because blocking\n# every Bash call would make Claude Code unusable — stopping all work is a\n# failure, not safety.\ncc_read_field() {\n if command -v jq >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | jq -r \".$1 // empty\" 2>/dev/null\n elif command -v python3 >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" python3 -c 'import sys,json,os\ntry:\n d=json.load(sys.stdin)\nexcept Exception:\n sys.exit(0)\nfor k in os.environ.get(\"CC_FIELD\",\"\").split(\".\"):\n d = d.get(k) if isinstance(d,dict) else None\nsys.stdout.write(\"\" if d is None else str(d))\n' 2>/dev/null\n elif command -v node >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" node -e 'let s=\"\";process.stdin.on(\"data\",d=>s+=d).on(\"end\",()=>{try{let o=JSON.parse(s);for(const k of (process.env.CC_FIELD||\"\").split(\".\"))o=(o&&typeof o===\"object\")?o[k]:undefined;process.stdout.write(o==null?\"\":String(o))}catch(e){}})' 2>/dev/null\n else\n printf '%s' '__CC_NO_PARSER__'\n fi\n}\n\ncc_warn_no_parser() {\n marker=\"${TMPDIR:-/tmp}/cc-noparser-warned-clear-command-confirm-guard-$PPID\"\n [ -f \"$marker\" ] && return 0\n : > \"$marker\" 2>/dev/null\n echo \"WARNING [clear-command-confirm-guard]: jq, python3 and node are all unavailable, so this hook cannot read the tool call and is NOT protecting you. Install any one of them.\" >&2\n}\n\nPROMPT=$(cc_read_field prompt)\n\nif [ \"$PROMPT\" = \"__CC_NO_PARSER__\" ]; then\n\n cc_warn_no_parser\n\n exit 0\n\nfi\nif printf '%s' \"$PROMPT\" | grep -qE '^/clear$'; then\n echo \"BLOCKED: /clear permanently destroys all context. Use /compact instead to reduce context safely.\" >&2\n exit 2\nfi\nexit 0\n",
|
|
11
11
|
"claudemd-violation-detector": "#!/bin/bash\n# claudemd-violation-detector.sh — Remind critical CLAUDE.md rules after tool use\n#\n# Solves: Claude ignores CLAUDE.md instructions, especially after\n# context compaction or in long sessions (#40930).\n#\n# How it works: After each tool use, extracts and prints\n# critical rules (ABSOLUTE/MUST NEVER/NEVER/禁止) from CLAUDE.md\n# as a reminder. Runs every N tool calls to avoid noise.\n#\n# TRIGGER: PostToolUse\n# MATCHER: \"\"\n\nset -euo pipefail\n\n# Rate limit: only remind every 20 tool calls\nCOUNTER_FILE=\"/tmp/claudemd-reminder-counter\"\nCOUNT=$(cat \"$COUNTER_FILE\" 2>/dev/null || echo \"0\")\nCOUNT=$((COUNT + 1))\necho \"$COUNT\" > \"$COUNTER_FILE\"\n[ $((COUNT % 20)) -ne 0 ] && exit 0\n\n# Find CLAUDE.md\nCLAUDEMD=\"\"\nfor candidate in \"CLAUDE.md\" \".claude/CLAUDE.md\" \"../CLAUDE.md\"; do\n [ -f \"$candidate\" ] && CLAUDEMD=\"$candidate\" && break\ndone\n[ -z \"$CLAUDEMD\" ] && exit 0\n\n# Extract critical rules\nRULES=$(grep -iE '(ABSOLUTE|MUST NEVER|NEVER DO|禁止|絶対)' \"$CLAUDEMD\" 2>/dev/null | head -5 || true)\n[ -z \"$RULES\" ] && exit 0\n\necho \"📋 CLAUDE.md critical rules reminder:\" >&2\necho \"$RULES\" >&2\nexit 0\n",
|
|
12
|
-
"subagent-context-size-guard": "#!/bin/bash\n# subagent-context-size-guard.sh — Warn on thin subagent prompts\n#\n# Solves: Subagents get spawned with minimal context, leading to\n# poor results because they lack necessary background (#40929).\n# The parent agent assumes shared context, but each subagent\n# starts fresh.\n#\n# How it works: Checks Agent tool's prompt parameter length.\n# If under 100 characters, warns that the prompt may be too thin\n# for a standalone agent to work effectively.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Agent\"\n\nset -euo pipefail\nINPUT=$(cat)\
|
|
12
|
+
"subagent-context-size-guard": "#!/bin/bash\n# subagent-context-size-guard.sh — Warn on thin subagent prompts\n#\n# Solves: Subagents get spawned with minimal context, leading to\n# poor results because they lack necessary background (#40929).\n# The parent agent assumes shared context, but each subagent\n# starts fresh.\n#\n# How it works: Checks Agent tool's prompt parameter length.\n# If under 100 characters, warns that the prompt may be too thin\n# for a standalone agent to work effectively.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Agent\"\n\nset -euo pipefail\nINPUT=$(cat)\n\n# --- Reading the hook payload -------------------------------------------------\n# Why a chain instead of jq alone: on a machine without jq the read returned an\n# empty string, every check below was skipped, and this hook exited 0 in silence.\n# It stayed listed in settings.json and protected nothing. Measured 2026-07-27:\n# a destructive command passed with exit 0 and no output at all.\n# If none of the three readers exist we warn once and allow, because blocking\n# every Bash call would make Claude Code unusable — stopping all work is a\n# failure, not safety.\ncc_read_field() {\n if command -v jq >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | jq -r \".$1 // empty\" 2>/dev/null\n elif command -v python3 >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" python3 -c 'import sys,json,os\ntry:\n d=json.load(sys.stdin)\nexcept Exception:\n sys.exit(0)\nfor k in os.environ.get(\"CC_FIELD\",\"\").split(\".\"):\n d = d.get(k) if isinstance(d,dict) else None\nsys.stdout.write(\"\" if d is None else str(d))\n' 2>/dev/null\n elif command -v node >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" node -e 'let s=\"\";process.stdin.on(\"data\",d=>s+=d).on(\"end\",()=>{try{let o=JSON.parse(s);for(const k of (process.env.CC_FIELD||\"\").split(\".\"))o=(o&&typeof o===\"object\")?o[k]:undefined;process.stdout.write(o==null?\"\":String(o))}catch(e){}})' 2>/dev/null\n else\n printf '%s' '__CC_NO_PARSER__'\n fi\n}\n\ncc_warn_no_parser() {\n marker=\"${TMPDIR:-/tmp}/cc-noparser-warned-subagent-context-size-guard-$PPID\"\n [ -f \"$marker\" ] && return 0\n : > \"$marker\" 2>/dev/null\n echo \"WARNING [subagent-context-size-guard]: jq, python3 and node are all unavailable, so this hook cannot read the tool call and is NOT protecting you. Install any one of them.\" >&2\n}\n\nPROMPT=$(cc_read_field tool_input.prompt)\n\nif [ \"$PROMPT\" = \"__CC_NO_PARSER__\" ]; then\n\n cc_warn_no_parser\n\n exit 0\n\nfi\n[ -z \"$PROMPT\" ] && exit 0\n\nLEN=${#PROMPT}\nif [ \"$LEN\" -lt 100 ]; then\n echo \"WARNING: Agent prompt is only ${LEN} chars. Subagents start with zero context — include enough background for them to work independently.\" >&2\nfi\nexit 0\n"
|
|
13
13
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"destructive-guard": "#!/bin/bash\n# ================================================================\n# destructive-guard.sh — Destructive Command Blocker\n# ================================================================\n# PURPOSE:\n# Blocks dangerous shell commands that can cause irreversible damage.\n# Catches rm -rf on sensitive paths, git reset --hard, git clean -fd,\n# and other destructive operations before they execute.\n#\n# Built after a real incident where rm -rf on a pnpm project\n# followed NTFS junctions and deleted an entire C:\\Users directory.\n# (GitHub Issue #36339)\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# WHAT IT BLOCKS (exit 2):\n# - rm -rf / rm -r on root, home, or parent paths (/, ~, .., /home, /etc)\n# - git reset --hard\n# - git clean -fd / git clean -fdx\n# - chmod -R 777 on sensitive paths\n# - find ... -delete on broad patterns\n#\n# WHAT IT ALLOWS (exit 0):\n# - rm -rf on specific project subdirectories (node_modules, dist, build)\n# - git reset --soft, git reset HEAD\n# - All non-destructive commands\n#\n# CONFIGURATION:\n# CC_ALLOW_DESTRUCTIVE=1 — disable this guard (not recommended)\n# CC_SAFE_DELETE_DIRS — colon-separated list of safe-to-delete dirs\n# default: \"node_modules:dist:build:.cache:__pycache__:coverage\"\n#\n# NOTE: On Windows/WSL2, rm -rf can follow NTFS junctions (symlinks)\n# and delete far more than intended. This guard is especially critical\n# on WSL2 environments.\n# ================================================================\n\nINPUT=$(cat)\n\n# --- Reading the hook payload -------------------------------------------------\n# Why a chain instead of jq alone: on a machine without jq the read returned an\n# empty string, every check below was skipped, and this hook exited 0 in silence.\n# It stayed listed in settings.json and protected nothing. Measured 2026-07-27:\n# a destructive command passed with exit 0 and no output at all.\n# If none of the three readers exist we warn once and allow, because blocking\n# every Bash call would make Claude Code unusable — stopping all work is a\n# failure, not safety.\ncc_read_field() {\n if command -v jq >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | jq -r \".$1 // empty\" 2>/dev/null\n elif command -v python3 >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" python3 -c 'import sys,json,os\ntry:\n d=json.load(sys.stdin)\nexcept Exception:\n sys.exit(0)\nfor k in os.environ.get(\"CC_FIELD\",\"\").split(\".\"):\n d = d.get(k) if isinstance(d,dict) else None\nsys.stdout.write(\"\" if d is None else str(d))\n' 2>/dev/null\n elif command -v node >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" node -e 'let s=\"\";process.stdin.on(\"data\",d=>s+=d).on(\"end\",()=>{try{let o=JSON.parse(s);for(const k of (process.env.CC_FIELD||\"\").split(\".\"))o=(o&&typeof o===\"object\")?o[k]:undefined;process.stdout.write(o==null?\"\":String(o))}catch(e){}})' 2>/dev/null\n else\n printf '%s' '__CC_NO_PARSER__'\n fi\n}\n\ncc_warn_no_parser() {\n marker=\"${TMPDIR:-/tmp}/cc-noparser-warned-destructive-guard-$PPID\"\n [ -f \"$marker\" ] && return 0\n : > \"$marker\" 2>/dev/null\n echo \"WARNING [destructive-guard]: jq, python3 and node are all unavailable, so this hook cannot read the tool call and is NOT protecting you. Install any one of them.\" >&2\n}\n\nCOMMAND=$(cc_read_field tool_input.command)\n\nif [ \"$COMMAND\" = \"__CC_NO_PARSER__\" ]; then\n\n cc_warn_no_parser\n\n exit 0\n\nfi\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# Allow override (not recommended)\nif [[ \"${CC_ALLOW_DESTRUCTIVE:-0}\" == \"1\" ]]; then\n exit 0\nfi\n\n# Log function — records blocked commands for audit\nlog_block() {\n local reason=\"$1\"\n local logfile=\"${CC_BLOCK_LOG:-$HOME/.claude/blocked-commands.log}\"\n mkdir -p \"$(dirname \"$logfile\")\" 2>/dev/null\n echo \"[$(date -Iseconds)] BLOCKED: $reason | cmd: $COMMAND\" >> \"$logfile\" 2>/dev/null\n}\n\n# Safe directories that can be deleted\nSAFE_DIRS=\"${CC_SAFE_DELETE_DIRS:-node_modules:dist:build:.cache:__pycache__:coverage:.next:.nuxt:tmp}\"\n\n# --- Check 0: --no-preserve-root ---\nif printf '%s' \"$COMMAND\" | grep -qE \"rm\\\\s.*\\\\-\\\\-no-preserve-root\"; then\n echo \"BLOCKED: --no-preserve-root detected.\" >&2\n exit 2\nfi\n\n# --- Check 1: rm -rf on dangerous paths ---\nif printf '%s' \"$COMMAND\" | grep -qE 'rm\\s+((-[rRfF]+|--recursive|--force)\\s+)*(\\/$|\\/\\s|\\/[^a-z]|\\/home|\\/etc|\\/usr|\\/var|\\/mnt|~\\/|~\\s*$|[\"]?\\$\\{?HOME\\}?[\"]?\\/|[\"]?\\$\\{?HOME\\}?[\"]?\\s*$|\\.\\.\\/|\\.\\.\\s*$|\\.\\s*$|\\.\\/\\s*$)'; then\n # Exception: safe directories\n SAFE=0\n IFS=':' read -ra DIRS <<< \"$SAFE_DIRS\"\n for dir in \"${DIRS[@]}\"; do\n if printf '%s' \"$COMMAND\" | grep -qE \"rm\\s+.*${dir}\\s*$|rm\\s+.*${dir}/\"; then\n SAFE=1\n break\n fi\n done\n\n # Check for mounted filesystems inside the target (NFS, Docker, bind mounts)\n # Why: GitHub #36640 — rm -rf on a dir with NFS mount deleted production data\n if (( SAFE == 0 )); then\n # Extract the target path from the rm command\n TARGET_PATH=$(printf '%s' \"$COMMAND\" | grep -oP 'rm\\s+((-[rRfF]+|--recursive|--force)\\s+)*\\K\\S+')\n if [ -n \"$TARGET_PATH\" ] && command -v findmnt &>/dev/null; then\n if findmnt -n -o TARGET --submounts \"$TARGET_PATH\" 2>/dev/null | grep -q .; then\n log_block \"rm on path with mounted filesystem\"\n echo \"BLOCKED: Target contains a mounted filesystem (NFS, Docker, bind).\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Unmount the filesystem first, then retry.\" >&2\n exit 2\n fi\n fi\n fi\n\n if (( SAFE == 0 )); then\n log_block \"rm on sensitive path\"\n echo \"BLOCKED: rm on sensitive path detected.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"This command targets a sensitive directory that could cause\" >&2\n echo \"irreversible data loss. On WSL2, rm -rf can follow NTFS\" >&2\n echo \"junctions and delete far beyond the target directory.\" >&2\n echo \"\" >&2\n echo \"If you need to delete a specific subdirectory, target it directly:\" >&2\n echo \" rm -rf ./specific-folder\" >&2\n exit 2\n fi\nfi\n\n# --- Check 2: git reset --hard ---\n# Only match when git is the actual command, not inside strings/arguments\nif printf '%s' \"$COMMAND\" | grep -qE '(^|;|&+|\\|\\|)\\s*git\\s+reset\\s+--hard'; then\n log_block \"git reset --hard\"\n echo \"BLOCKED: git reset --hard discards all uncommitted changes.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: git stash, or git reset --soft to keep changes staged.\" >&2\n exit 2\nfi\n\n# --- Check 3: git clean -fd ---\nif printf '%s' \"$COMMAND\" | grep -qE '(^|;|&+|\\|\\|)\\s*git\\s+clean\\s+-[a-z]*[fd]'; then\n log_block \"git clean\"\n echo \"BLOCKED: git clean removes untracked files permanently.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: git clean -n (dry run) first to see what would be deleted.\" >&2\n exit 2\nfi\n\n# --- Check 4: chmod 777 on broad paths ---\nif printf '%s' \"$COMMAND\" | grep -qE 'chmod\\s+(-R\\s+)?777\\s+(\\/|~|\\.)'; then\n echo \"BLOCKED: chmod 777 on broad path is a security risk.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n exit 2\nfi\n\n# --- Check 5: find -delete on broad patterns ---\nif printf '%s' \"$COMMAND\" | grep -qE 'find\\s+(\\/|~|\\.\\.)\\s.*-delete'; then\n echo \"BLOCKED: find -delete on broad path risks mass deletion.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: find ... -print first to verify what matches.\" >&2\n exit 2\nfi\n\n# --- Check 6: sudo with dangerous commands ---\nif printf '%s' \"$COMMAND\" | grep -qE '^\\s*sudo\\s+(rm\\s+-[rf]|chmod\\s+(-R\\s+)?777|dd\\s+if=|mkfs)'; then\n log_block \"sudo with dangerous command\"\n echo \"BLOCKED: sudo with dangerous command detected.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Running destructive commands with sudo amplifies the damage.\" >&2\n echo \"Review the command carefully before proceeding.\" >&2\n exit 2\nfi\n\n\n# --- Check 7: PowerShell Remove-Item (Windows/WSL2) ---\n# Real incident: GitHub #37331 — destroyed entire repo\n# Skip if command is git commit (message text triggers false positive)\nif printf '%s' \"$COMMAND\" | grep -qE '^\\s*(git\\s+commit|echo\\s|printf\\s|cat\\s)'; then\n : # string output commands mentioning PS commands are not destructive\nelif printf '%s' \"$COMMAND\" | grep -qiE 'Remove-Item.*-Recurse.*-Force|Remove-Item.*-Force.*-Recurse|del\\s+/s\\s+/q|rd\\s+/s\\s+/q|rmdir\\s+/s\\s+/q'; then\n log_block \"PowerShell destructive command\"\n echo \"BLOCKED: Destructive PowerShell command detected.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Remove-Item with recursive force-delete can destroy entire directories\" >&2\n echo \"irreversibly. Target specific files instead.\" >&2\n exit 2\nfi\nif printf '%s' \"$COMMAND\" | grep -qE '(^|;|&+|\\|\\|)\\s*git\\s+(checkout|switch)\\s+.*(--force\\b|-f\\b|--discard-changes\\b)'; then\n log_block \"git checkout/switch --force\"\n echo \"BLOCKED: git checkout/switch with --force discards uncommitted changes.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: git stash before switching, or use git switch without --force.\" >&2\n exit 2\nfi\n\nif printf '%s' \"$COMMAND\" | grep -qE '(sh|bash|zsh)\\s+-c\\s+'; then\n INNER=$(printf '%s' \"$COMMAND\" | sed -E \"s/.*(sh|bash|zsh)\\s+-c\\s+['\\\"]//\" | sed \"s/['\\\"]*$//\" )\n if echo \"$INNER\" | grep -qE 'rm\\s+-[rf]*\\s+[/~]|git\\s+reset\\s+--hard|git\\s+clean\\s+-[fd]+|mkfs\\.|dd\\s+if='; then\n echo \"BLOCKED: Destructive command hidden in shell wrapper\" >&2\n echo \"\" >&2\n echo \"Detected: $INNER\" >&2\n exit 2\n fi\nfi\nif printf '%s' \"$COMMAND\" | grep -qE '\\|\\s*(sh|bash)\\s*$'; then\n if printf '%s' \"$COMMAND\" | grep -qE 'rm\\s+-[rf]*\\s+[/~]|git\\s+reset\\s+--hard|git\\s+clean\\s+-[fd]+'; then\n echo \"BLOCKED: Destructive command piped to shell\" >&2\n exit 2\n fi\nfi\n\n# --- Check 8: xargs invoking rm with recursive flags (target path absent from the command line) ---\nif printf '%s' \"$COMMAND\" | grep -qE 'xargs\\s+(-\\S+\\s+)*rm\\s+(-\\S+\\s+)*(-[a-zA-Z]*[rR]|--recursive)'; then\n log_block \"xargs rm with recursive flags\"\n echo \"BLOCKED: xargs invoking 'rm' with recursive flags risks mass deletion.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"The target paths come from stdin, so this can delete far more than expected.\" >&2\n echo \"Consider piping to 'xargs -p' or reviewing the input list first.\" >&2\n exit 2\nfi\n\n# --- Check 9: base64-decoded payload fed straight into a shell (obfuscation) ---\nif printf '%s' \"$COMMAND\" | grep -qE 'base64\\s+(-d|--decode)\\b' && printf '%s' \"$COMMAND\" | grep -qE '\\|\\s*(sh|bash|zsh)(\\s|$)|eval\\s|(sh|bash|zsh)\\s+-c'; then\n log_block \"base64-decoded payload piped into a shell\"\n echo \"BLOCKED: base64-decoded payload fed into a shell (obfuscation).\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Decoding base64 straight into sh/bash/eval hides what actually runs.\" >&2\n echo \"Decode to a file and inspect it before executing.\" >&2\n exit 2\nfi\nexit 0",
|
|
3
|
+
"branch-guard": "#!/bin/bash\n# ================================================================\n# branch-guard.sh — Branch Push Protector\n# ================================================================\n# PURPOSE:\n# Prevents accidental git push to main/master branches AND\n# blocks force-push on ALL branches without explicit approval.\n#\n# Force-pushes rewrite history and can destroy teammates' work.\n# Protected branch pushes bypass code review.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# WHAT IT BLOCKS (exit 2):\n# - git push origin main/master (any protected branch)\n# - git push --force (any branch — history rewriting)\n# - git push -f (short flag variant)\n# - git push --force-with-lease (still destructive)\n#\n# WHAT IT ALLOWS (exit 0):\n# - git push origin feature-branch (non-force)\n# - git push -u origin feature-branch\n# - All other git commands\n# - All non-git commands\n#\n# CONFIGURATION:\n# CC_PROTECT_BRANCHES — colon-separated list of protected branches\n# default: \"main:master\"\n# CC_ALLOW_FORCE_PUSH=1 — disable force-push protection\n# ================================================================\n\nINPUT=$(cat)\n\n# --- Reading the hook payload -------------------------------------------------\n# Why a chain instead of jq alone: on a machine without jq the read returned an\n# empty string, every check below was skipped, and this hook exited 0 in silence.\n# It stayed listed in settings.json and protected nothing. Measured 2026-07-27:\n# a destructive command passed with exit 0 and no output at all.\n# If none of the three readers exist we warn once and allow, because blocking\n# every Bash call would make Claude Code unusable — stopping all work is a\n# failure, not safety.\ncc_read_field() {\n if command -v jq >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | jq -r \".$1 // empty\" 2>/dev/null\n elif command -v python3 >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" python3 -c 'import sys,json,os\ntry:\n d=json.load(sys.stdin)\nexcept Exception:\n sys.exit(0)\nfor k in os.environ.get(\"CC_FIELD\",\"\").split(\".\"):\n d = d.get(k) if isinstance(d,dict) else None\nsys.stdout.write(\"\" if d is None else str(d))\n' 2>/dev/null\n elif command -v node >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" node -e 'let s=\"\";process.stdin.on(\"data\",d=>s+=d).on(\"end\",()=>{try{let o=JSON.parse(s);for(const k of (process.env.CC_FIELD||\"\").split(\".\"))o=(o&&typeof o===\"object\")?o[k]:undefined;process.stdout.write(o==null?\"\":String(o))}catch(e){}})' 2>/dev/null\n else\n printf '%s' '__CC_NO_PARSER__'\n fi\n}\n\ncc_warn_no_parser() {\n marker=\"${TMPDIR:-/tmp}/cc-noparser-warned-branch-guard-$PPID\"\n [ -f \"$marker\" ] && return 0\n : > \"$marker\" 2>/dev/null\n echo \"WARNING [branch-guard]: jq, python3 and node are all unavailable, so this hook cannot read the tool call and is NOT protecting you. Install any one of them.\" >&2\n}\n\nCOMMAND=$(cc_read_field tool_input.command)\n\nif [ \"$COMMAND\" = \"__CC_NO_PARSER__\" ]; then\n\n cc_warn_no_parser\n\n exit 0\n\nfi\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# Only check git push commands\nif ! printf '%s' \"$COMMAND\" | grep -qE '(^|;|&&|\\|\\|)\\s*git\\s+push'; then\n exit 0\nfi\n\n# --- Check 1: Force push on ANY branch ---\nif [[ \"${CC_ALLOW_FORCE_PUSH:-0}\" != \"1\" ]]; then\n if printf '%s' \"$COMMAND\" | grep -qE 'git\\s+push\\s+.*(-f\\b|--force\\b|--force-with-lease\\b)'; then\n echo \"BLOCKED: Force push detected.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Force push rewrites remote history and can destroy\" >&2\n echo \"other people's work. This is almost never what you want.\" >&2\n echo \"\" >&2\n echo \"If you truly need to force push, set CC_ALLOW_FORCE_PUSH=1\" >&2\n exit 2\n fi\nfi\n\n# --- Check 2: Push to protected branches ---\nPROTECTED=\"${CC_PROTECT_BRANCHES:-main:master}\"\n\nBLOCKED=0\nIFS=':' read -ra BRANCHES <<< \"$PROTECTED\"\nfor branch in \"${BRANCHES[@]}\"; do\n if printf '%s' \"$COMMAND\" | grep -qwE \"origin\\s+${branch}|${branch}\\s|${branch}$\"; then\n BLOCKED=1\n break\n fi\ndone\n\nif (( BLOCKED == 1 )); then\n echo \"BLOCKED: Attempted push to protected branch.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Protected branches: $PROTECTED\" >&2\n echo \"\" >&2\n echo \"Push to a feature branch first, then create a pull request.\" >&2\n exit 2\nfi\n\nexit 0\n",
|
|
4
|
+
"syntax-check": "#!/bin/bash\n# ================================================================\n# syntax-check.sh — Automatic Syntax Validation After Edits\n# ================================================================\n# PURPOSE:\n# Runs syntax checks immediately after Claude Code edits or\n# writes a file. Catches syntax errors before they propagate\n# into downstream failures.\n#\n# SUPPORTED LANGUAGES:\n# .py — python -m py_compile\n# .sh — bash -n\n# .bash — bash -n\n# .json — jq empty\n# .yaml — python3 yaml.safe_load (if PyYAML installed)\n# .yml — python3 yaml.safe_load (if PyYAML installed)\n# .js — node --check (if node installed)\n# .ts — npx tsc --noEmit (if tsc available) [EXPERIMENTAL]\n#\n# TRIGGER: PostToolUse\n# MATCHER: \"Edit|Write\"\n#\n# DESIGN PHILOSOPHY:\n# - Never blocks (always exit 0) — reports errors but doesn't\n# prevent the edit from completing\n# - Silent on success — only speaks up when something is wrong\n# - Fails open — if a checker isn't installed, silently skips\n#\n# BORN FROM:\n# Countless sessions where Claude Code introduced a syntax error,\n# continued working for 10+ tool calls, then hit a wall when\n# trying to run the broken file. Catching it immediately saves\n# context window and frustration.\n# ================================================================\n\nINPUT=$(cat)\nFILE_PATH=$(printf '%s' \"$INPUT\" | jq -r '.tool_input.file_path // empty' 2>/dev/null)\n\n# No file path = nothing to check\nif [[ -z \"$FILE_PATH\" || ! -f \"$FILE_PATH\" ]]; then\n exit 0\nfi\n\nEXT=\"${FILE_PATH##*.}\"\n\ncase \"$EXT\" in\n py)\n if python3 -m py_compile \"$FILE_PATH\" 2>&1; then\n : # silent on success\n else\n echo \"SYNTAX ERROR (Python): $FILE_PATH\" >&2\n fi\n ;;\n sh|bash)\n if bash -n \"$FILE_PATH\" 2>&1; then\n :\n else\n echo \"SYNTAX ERROR (Shell): $FILE_PATH\" >&2\n fi\n ;;\n json)\n if command -v jq &>/dev/null; then\n if jq empty \"$FILE_PATH\" 2>&1; then\n :\n else\n echo \"SYNTAX ERROR (JSON): $FILE_PATH\" >&2\n fi\n fi\n ;;\n yaml|yml)\n if python3 -c \"import yaml\" 2>/dev/null; then\n if python3 -c \"\nimport yaml, sys\nwith open(sys.argv[1]) as f:\n yaml.safe_load(f)\n\" \"$FILE_PATH\" 2>&1; then\n :\n else\n echo \"SYNTAX ERROR (YAML): $FILE_PATH\" >&2\n fi\n fi\n ;;\n js)\n if command -v node &>/dev/null; then\n if node --check \"$FILE_PATH\" 2>&1; then\n :\n else\n echo \"SYNTAX ERROR (JavaScript): $FILE_PATH\" >&2\n fi\n fi\n ;;\n ts)\n # EXPERIMENTAL: TypeScript check requires tsc in PATH\n if command -v npx &>/dev/null; then\n if npx tsc --noEmit \"$FILE_PATH\" 2>&1; then\n :\n else\n echo \"SYNTAX ERROR (TypeScript) [experimental]: $FILE_PATH\" >&2\n fi\n fi\n ;;\n *)\n # Unknown extension — skip silently\n ;;\nesac\n\nexit 0\n",
|
|
5
|
+
"context-monitor": "#!/bin/bash\n# ================================================================\n# context-monitor.sh — Context Window Remaining Capacity Monitor\n# ================================================================\n# PURPOSE:\n# Monitors how much context window remains during a Claude Code\n# session. Issues graduated warnings (CAUTION → WARNING → CRITICAL\n# → EMERGENCY) so you never get killed by context exhaustion.\n#\n# HOW IT WORKS:\n# 1. Reads Claude Code's debug log to extract actual token usage\n# 2. Falls back to tool-call-count estimation when debug logs\n# are unavailable\n# 3. Saves current % to /tmp/cc-context-pct (other scripts can\n# read this)\n# 4. At CRITICAL/EMERGENCY, writes an evacuation template to\n# your mission file so you can hand off state before /compact\n#\n# TRIGGER: PostToolUse (all tools)\n# MATCHER: \"\" (empty = every tool invocation)\n#\n# CONFIGURATION:\n# CC_CONTEXT_MISSION_FILE — path to your mission/state file\n# default: $HOME/mission.md\n#\n# THRESHOLDS (edit below to taste):\n# CAUTION = 40% — be mindful of consumption\n# WARNING = 25% — finish current task, save state\n# CRITICAL = 20% — run /compact immediately\n# EMERGENCY = 15% — stop everything, evacuate\n#\n# BORN FROM:\n# A session that hit 3% context remaining with no warning.\n# The agent died mid-task and all in-flight work was lost.\n# Never again.\n# ================================================================\n\nSTATE_FILE=\"/tmp/cc-context-state\"\nPCT_FILE=\"/tmp/cc-context-pct\"\nCOUNTER_FILE=\"/tmp/cc-context-monitor-count\"\nMISSION_FILE=\"${CC_CONTEXT_MISSION_FILE:-$HOME/mission.md}\"\n\n# Tool invocation counter (fallback estimator)\nCOUNT=$(cat \"$COUNTER_FILE\" 2>/dev/null || echo 0)\nCOUNT=$((COUNT + 1))\necho \"$COUNT\" > \"$COUNTER_FILE\"\n\n# Check every 3rd invocation to reduce overhead\n# (but always check in CRITICAL/EMERGENCY state)\nLAST_STATE=$(cat \"$STATE_FILE\" 2>/dev/null || echo \"normal\")\nif [ $((COUNT % 3)) -ne 0 ] && [ \"$LAST_STATE\" != \"critical\" ] && [ \"$LAST_STATE\" != \"emergency\" ]; then\n exit 0\nfi\n\n# --- Extract context % from Claude Code debug logs ---\nget_context_pct() {\n local debug_dir=\"$HOME/.claude/debug\"\n if [ ! -d \"$debug_dir\" ]; then\n echo \"\"\n return\n fi\n\n local latest\n latest=$(find \"$debug_dir\" -maxdepth 1 -name '*.txt' -printf '%T@ %p\\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2)\n if [ -z \"$latest\" ]; then\n echo \"\"\n return\n fi\n\n # Parse the last autocompact entry for token counts\n local line\n line=$(grep 'autocompact:' \"$latest\" 2>/dev/null | tail -1)\n if [ -z \"$line\" ]; then\n echo \"\"\n return\n fi\n\n local tokens window\n tokens=$(echo \"$line\" | sed 's/.*tokens=\\([0-9]*\\).*/\\1/')\n window=$(echo \"$line\" | sed 's/.*effectiveWindow=\\([0-9]*\\).*/\\1/')\n\n if [ -n \"$tokens\" ] && [ -n \"$window\" ] && [ \"$window\" -gt 0 ] 2>/dev/null; then\n local pct\n pct=$(( (window - tokens) * 100 / window ))\n echo \"$pct\"\n else\n echo \"\"\n fi\n}\n\nCONTEXT_PCT=$(get_context_pct)\n\n# Fallback: estimate from tool call count when debug logs unavailable\n# Assumes ~180 tool calls fills ~100% of context (conservative)\nif [ -z \"$CONTEXT_PCT\" ]; then\n CONTEXT_PCT=$(( 100 - (COUNT * 100 / 180) ))\n if [ \"$CONTEXT_PCT\" -lt 0 ]; then CONTEXT_PCT=0; fi\n SOURCE=\"estimate\"\nelse\n SOURCE=\"debug\"\nfi\n\necho \"$CONTEXT_PCT\" > \"$PCT_FILE\"\n\nTIMESTAMP=$(date '+%Y-%m-%d %H:%M')\n\n# --- Evacuation template (with cooldown to prevent spam) ---\nEVAC_COOLDOWN_FILE=\"/tmp/cc-context-evac-last\"\nEVAC_COOLDOWN_SEC=1800 # 30 min cooldown between template generations\n\ngenerate_evacuation_template() {\n local level=\"$1\"\n\n # Cooldown check\n if [ -f \"$EVAC_COOLDOWN_FILE\" ]; then\n local last_ts now_ts diff\n last_ts=$(cat \"$EVAC_COOLDOWN_FILE\" 2>/dev/null || echo 0)\n now_ts=$(date +%s)\n diff=$((now_ts - last_ts))\n if [ \"$diff\" -lt \"$EVAC_COOLDOWN_SEC\" ]; then\n return\n fi\n fi\n\n # Don't add a new template if there's already an unfilled one\n if [ -f \"$MISSION_FILE\" ] && grep -q '\\[TODO\\]' \"$MISSION_FILE\" 2>/dev/null; then\n return\n fi\n\n date +%s > \"$EVAC_COOLDOWN_FILE\"\n\n # Create mission file directory if needed\n mkdir -p \"$(dirname \"$MISSION_FILE\")\"\n\n cat >> \"$MISSION_FILE\" << EVAC_EOF\n\n## Context Evacuation Template (${level} - ${TIMESTAMP})\n<!-- Auto-generated by context-monitor.sh. Fill in before /compact -->\n### Current Task\n- Task: [TODO]\n- Progress: [TODO]\n- Files being edited: [TODO]\n\n### Git State\n- Branch: [TODO]\n- Uncommitted changes: [TODO]\n\n### Next Action\n- Next command/action: [TODO]\nEVAC_EOF\n}\n\n# --- Graduated warnings ---\nif [ \"$CONTEXT_PCT\" -le 15 ]; then\n # EMERGENCY\n if [ \"$LAST_STATE\" != \"emergency\" ]; then\n echo \"emergency\" > \"$STATE_FILE\"\n generate_evacuation_template \"EMERGENCY\"\n fi\n echo \"\"\n echo \"EMERGENCY: Context remaining ${CONTEXT_PCT}% (${SOURCE})\"\n echo \"Run /compact IMMEDIATELY. Evacuation template written to ${MISSION_FILE}.\"\n echo \"1. Fill in the [TODO] fields in the template\"\n echo \"2. Run /compact\"\n echo \"3. If needed, restart and resume from mission file\"\n echo \"No further work allowed. Evacuate only.\"\n\nelif [ \"$CONTEXT_PCT\" -le 20 ]; then\n # CRITICAL\n if [ \"$LAST_STATE\" != \"critical\" ]; then\n echo \"critical\" > \"$STATE_FILE\"\n generate_evacuation_template \"CRITICAL\"\n fi\n echo \"\"\n echo \"CRITICAL: Context remaining ${CONTEXT_PCT}% (${SOURCE})\"\n echo \"Run /compact IMMEDIATELY. Evacuation template written to ${MISSION_FILE}.\"\n echo \"1. Save current task state to the template\"\n echo \"2. Run /compact\"\n\nelif [ \"$CONTEXT_PCT\" -le 25 ]; then\n # WARNING\n if [ \"$LAST_STATE\" != \"warning\" ]; then\n echo \"warning\" > \"$STATE_FILE\"\n echo \"\"\n echo \"WARNING: Context remaining ${CONTEXT_PCT}% (${SOURCE})\"\n echo \"Do not start new large tasks. Finish current work and save state.\"\n fi\n\nelif [ \"$CONTEXT_PCT\" -le 40 ]; then\n # CAUTION\n if [ \"$LAST_STATE\" != \"caution\" ]; then\n echo \"caution\" > \"$STATE_FILE\"\n echo \"\"\n echo \"CAUTION: Context remaining ${CONTEXT_PCT}% (${SOURCE})\"\n echo \"Be mindful of context consumption. Keep interactions concise.\"\n fi\nfi\n\nexit 0\n",
|
|
6
|
+
"comment-strip": "#!/bin/bash\n# ================================================================\n# comment-strip.sh — Strip bash comments that break permissions\n# ================================================================\n# PURPOSE:\n# Claude Code sometimes adds comments to bash commands like:\n# # Check the diff\n# git diff HEAD~1\n# This breaks permission allowlists (e.g. Bash(git:*)) because\n# the matcher sees \"# Check the diff\" instead of \"git diff\".\n#\n# This hook strips leading comment lines and returns the clean\n# command via updatedInput, so permissions match correctly.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# INCIDENT: GitHub Issue #29582 (18 reactions)\n# Users on linux/vscode report that bash comments added by Claude\n# cause permission prompts even when the command is allowlisted.\n#\n# HOW IT WORKS:\n# - Reads the command from tool_input\n# - Strips leading lines that start with #\n# - Strips trailing comments (everything after # on command lines)\n# - Returns updatedInput with the cleaned command\n# - Uses hookSpecificOutput.permissionDecision = \"allow\" only if\n# the command was modified (so it doesn't override other hooks)\n# ================================================================\n\nINPUT=$(cat)\n\n# --- Reading the hook payload -------------------------------------------------\n# Why a chain instead of jq alone: on a machine without jq the read returned an\n# empty string, every check below was skipped, and this hook exited 0 in silence.\n# It stayed listed in settings.json and protected nothing. Measured 2026-07-27:\n# a destructive command passed with exit 0 and no output at all.\n# If none of the three readers exist we warn once and allow, because blocking\n# every Bash call would make Claude Code unusable — stopping all work is a\n# failure, not safety.\ncc_read_field() {\n if command -v jq >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | jq -r \".$1 // empty\" 2>/dev/null\n elif command -v python3 >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" python3 -c 'import sys,json,os\ntry:\n d=json.load(sys.stdin)\nexcept Exception:\n sys.exit(0)\nfor k in os.environ.get(\"CC_FIELD\",\"\").split(\".\"):\n d = d.get(k) if isinstance(d,dict) else None\nsys.stdout.write(\"\" if d is None else str(d))\n' 2>/dev/null\n elif command -v node >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" node -e 'let s=\"\";process.stdin.on(\"data\",d=>s+=d).on(\"end\",()=>{try{let o=JSON.parse(s);for(const k of (process.env.CC_FIELD||\"\").split(\".\"))o=(o&&typeof o===\"object\")?o[k]:undefined;process.stdout.write(o==null?\"\":String(o))}catch(e){}})' 2>/dev/null\n else\n printf '%s' '__CC_NO_PARSER__'\n fi\n}\n\ncc_warn_no_parser() {\n marker=\"${TMPDIR:-/tmp}/cc-noparser-warned-comment-strip-$PPID\"\n [ -f \"$marker\" ] && return 0\n : > \"$marker\" 2>/dev/null\n echo \"WARNING [comment-strip]: jq, python3 and node are all unavailable, so this hook cannot read the tool call and is NOT protecting you. Install any one of them.\" >&2\n}\n\nCOMMAND=$(cc_read_field tool_input.command)\n\nif [ \"$COMMAND\" = \"__CC_NO_PARSER__\" ]; then\n\n cc_warn_no_parser\n\n exit 0\n\nfi\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# Strip leading comment lines and empty lines\nCLEAN=$(printf '%s' \"$COMMAND\" | sed '/^[[:space:]]*#/d; /^[[:space:]]*$/d')\n\n# If nothing changed, pass through\nif [[ \"$CLEAN\" == \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# If command is empty after stripping, don't modify\nif [[ -z \"$CLEAN\" ]]; then\n exit 0\nfi\n\n# Return cleaned command via hookSpecificOutput\n# permissionDecision is not set — let the normal permission flow handle it\n# We only modify the input so the permission matcher sees the real command\njq -n --arg cmd \"$CLEAN\" '{\n hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n updatedInput: {\n command: $cmd\n }\n }\n}'\n",
|
|
7
|
+
"cd-git-allow": "#!/bin/bash\n# ================================================================\n# cd-git-allow.sh — Auto-approve cd+git compound commands\n# ================================================================\n# PURPOSE:\n# Claude Code shows \"Compound commands with cd and git require\n# approval\" for commands like: cd /path && git log\n# This is safe in trusted project directories but causes\n# constant permission prompts.\n#\n# This hook auto-approves cd+git compounds when the git operation\n# is read-only (log, diff, status, branch, show, etc.)\n# Destructive git operations (push, reset, clean) are NOT\n# auto-approved — they still require manual approval.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# INCIDENT: GitHub Issue #32985 (9 reactions)\n#\n# WHAT IT AUTO-APPROVES:\n# - cd /path && git log\n# - cd /path && git diff\n# - cd /path && git status\n# - cd /path && git branch\n# - cd /path && git show\n# - cd /path && git rev-parse\n#\n# WHAT IT DOES NOT APPROVE (still prompts):\n# - cd /path && git push\n# - cd /path && git reset --hard\n# - cd /path && git clean\n# - cd /path && git checkout (could discard changes)\n# ================================================================\n\nINPUT=$(cat)\n\n# --- Reading the hook payload -------------------------------------------------\n# Why a chain instead of jq alone: on a machine without jq the read returned an\n# empty string, every check below was skipped, and this hook exited 0 in silence.\n# It stayed listed in settings.json and protected nothing. Measured 2026-07-27:\n# a destructive command passed with exit 0 and no output at all.\n# If none of the three readers exist we warn once and allow, because blocking\n# every Bash call would make Claude Code unusable — stopping all work is a\n# failure, not safety.\ncc_read_field() {\n if command -v jq >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | jq -r \".$1 // empty\" 2>/dev/null\n elif command -v python3 >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" python3 -c 'import sys,json,os\ntry:\n d=json.load(sys.stdin)\nexcept Exception:\n sys.exit(0)\nfor k in os.environ.get(\"CC_FIELD\",\"\").split(\".\"):\n d = d.get(k) if isinstance(d,dict) else None\nsys.stdout.write(\"\" if d is None else str(d))\n' 2>/dev/null\n elif command -v node >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" node -e 'let s=\"\";process.stdin.on(\"data\",d=>s+=d).on(\"end\",()=>{try{let o=JSON.parse(s);for(const k of (process.env.CC_FIELD||\"\").split(\".\"))o=(o&&typeof o===\"object\")?o[k]:undefined;process.stdout.write(o==null?\"\":String(o))}catch(e){}})' 2>/dev/null\n else\n printf '%s' '__CC_NO_PARSER__'\n fi\n}\n\ncc_warn_no_parser() {\n marker=\"${TMPDIR:-/tmp}/cc-noparser-warned-cd-git-allow-$PPID\"\n [ -f \"$marker\" ] && return 0\n : > \"$marker\" 2>/dev/null\n echo \"WARNING [cd-git-allow]: jq, python3 and node are all unavailable, so this hook cannot read the tool call and is NOT protecting you. Install any one of them.\" >&2\n}\n\nCOMMAND=$(cc_read_field tool_input.command)\n\nif [ \"$COMMAND\" = \"__CC_NO_PARSER__\" ]; then\n\n cc_warn_no_parser\n\n exit 0\n\nfi\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# Only handle cd + git compounds\nif ! printf '%s' \"$COMMAND\" | grep -qE '^\\s*cd\\s+.*&&\\s*git\\s'; then\n exit 0\nfi\n\n# Extract the git subcommand\nGIT_CMD=$(printf '%s' \"$COMMAND\" | grep -oP '&&\\s*git\\s+\\K\\S+')\n\n# Read-only git operations — safe to auto-approve\nSAFE_GIT=\"log diff status branch show rev-parse tag remote stash-list describe name-rev\"\n\nfor safe in $SAFE_GIT; do\n if [[ \"$GIT_CMD\" == \"$safe\" ]]; then\n jq -n '{\n hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: \"allow\",\n permissionDecisionReason: \"cd+git compound auto-approved (read-only git operation)\"\n }\n }'\n exit 0\n fi\ndone\n\n# Not a read-only git op — let normal permission flow handle it\nexit 0\n",
|
|
8
|
+
"secret-guard": "#!/bin/bash\n# ================================================================\n# secret-guard.sh — Secret/Credential Leak Prevention\n# ================================================================\n# PURPOSE:\n# Prevents accidental exposure of secrets, API keys, and\n# credentials through git commits or shell output.\n#\n# Catches the most common ways secrets leak:\n# - git add .env (committing env files)\n# - git add credentials.json / *.pem / *.key\n# - echo $API_KEY or printenv (exposing secrets in output)\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# WHAT IT BLOCKS (exit 2):\n# - git add .env / .env.local / .env.production\n# - git add *credentials* / *secret* / *.pem / *.key\n# - git add -A or git add . when .env exists (warns)\n#\n# WHAT IT ALLOWS (exit 0):\n# - git add specific safe files\n# - Reading .env for application use (not committing)\n# - All non-git-add commands\n#\n# CONFIGURATION:\n# CC_SECRET_PATTERNS — colon-separated additional patterns to block\n# default: \".env:.env.local:.env.production:credentials:secret:*.pem:*.key:*.p12\"\n# ================================================================\n\nINPUT=$(cat)\n\n# --- Reading the hook payload -------------------------------------------------\n# Why a chain instead of jq alone: on a machine without jq the read returned an\n# empty string, every check below was skipped, and this hook exited 0 in silence.\n# It stayed listed in settings.json and protected nothing. Measured 2026-07-27:\n# a destructive command passed with exit 0 and no output at all.\n# If none of the three readers exist we warn once and allow, because blocking\n# every Bash call would make Claude Code unusable — stopping all work is a\n# failure, not safety.\ncc_read_field() {\n if command -v jq >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | jq -r \".$1 // empty\" 2>/dev/null\n elif command -v python3 >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" python3 -c 'import sys,json,os\ntry:\n d=json.load(sys.stdin)\nexcept Exception:\n sys.exit(0)\nfor k in os.environ.get(\"CC_FIELD\",\"\").split(\".\"):\n d = d.get(k) if isinstance(d,dict) else None\nsys.stdout.write(\"\" if d is None else str(d))\n' 2>/dev/null\n elif command -v node >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" node -e 'let s=\"\";process.stdin.on(\"data\",d=>s+=d).on(\"end\",()=>{try{let o=JSON.parse(s);for(const k of (process.env.CC_FIELD||\"\").split(\".\"))o=(o&&typeof o===\"object\")?o[k]:undefined;process.stdout.write(o==null?\"\":String(o))}catch(e){}})' 2>/dev/null\n else\n printf '%s' '__CC_NO_PARSER__'\n fi\n}\n\ncc_warn_no_parser() {\n marker=\"${TMPDIR:-/tmp}/cc-noparser-warned-secret-guard-$PPID\"\n [ -f \"$marker\" ] && return 0\n : > \"$marker\" 2>/dev/null\n echo \"WARNING [secret-guard]: jq, python3 and node are all unavailable, so this hook cannot read the tool call and is NOT protecting you. Install any one of them.\" >&2\n}\n\nCOMMAND=$(cc_read_field tool_input.command)\n\nif [ \"$COMMAND\" = \"__CC_NO_PARSER__\" ]; then\n\n cc_warn_no_parser\n\n exit 0\n\nfi\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# --- Check 1: git add of secret files ---\nif printf '%s' \"$COMMAND\" | grep -qE '(^|;|&&|\\|\\|)\\s*git\\s+add'; then\n # Direct .env file staging\n if printf '%s' \"$COMMAND\" | grep -qiE 'git\\s+add\\s+.*\\.env(\\s|$|\\.|/)'; then\n echo \"BLOCKED: Attempted to stage .env file.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \".env files contain secrets and should never be committed.\" >&2\n echo \"Add .env to .gitignore instead.\" >&2\n exit 2\n fi\n\n # Credential/key files\n if printf '%s' \"$COMMAND\" | grep -qiE 'git\\s+add\\s+.*(credentials|\\.pem|\\.key|\\.p12|\\.pfx|id_rsa|id_ed25519)'; then\n echo \"BLOCKED: Attempted to stage credential/key file.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Key and credential files should never be committed to git.\" >&2\n echo \"Add them to .gitignore instead.\" >&2\n exit 2\n fi\n\n # git add -A or git add . when .env exists — warn but check\n if printf '%s' \"$COMMAND\" | grep -qE 'git\\s+add\\s+(-A|--all|\\.)(\\s|$)'; then\n # Check if .env exists in the current or project directory\n if [ -f \".env\" ] || [ -f \".env.local\" ] || [ -f \".env.production\" ]; then\n echo \"BLOCKED: 'git add .' with .env file present.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"An .env file exists in this directory. 'git add .' would stage it.\" >&2\n echo \"Add specific files instead: git add src/ lib/ package.json\" >&2\n echo \"Or add .env to .gitignore first.\" >&2\n exit 2\n fi\n fi\nfi\n\nexit 0\n",
|
|
9
|
+
"api-error-alert": "#!/bin/bash\nINPUT=$(cat)\n\n# --- Reading the hook payload -------------------------------------------------\n# Why a chain instead of jq alone: on a machine without jq the read returned an\n# empty string, every check below was skipped, and this hook exited 0 in silence.\n# It stayed listed in settings.json and protected nothing. Measured 2026-07-27:\n# a destructive command passed with exit 0 and no output at all.\n# If none of the three readers exist we warn once and allow, because blocking\n# every Bash call would make Claude Code unusable — stopping all work is a\n# failure, not safety.\ncc_read_field() {\n if command -v jq >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | jq -r \".$1 // empty\" 2>/dev/null\n elif command -v python3 >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" python3 -c 'import sys,json,os\ntry:\n d=json.load(sys.stdin)\nexcept Exception:\n sys.exit(0)\nfor k in os.environ.get(\"CC_FIELD\",\"\").split(\".\"):\n d = d.get(k) if isinstance(d,dict) else None\nsys.stdout.write(\"\" if d is None else str(d))\n' 2>/dev/null\n elif command -v node >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" node -e 'let s=\"\";process.stdin.on(\"data\",d=>s+=d).on(\"end\",()=>{try{let o=JSON.parse(s);for(const k of (process.env.CC_FIELD||\"\").split(\".\"))o=(o&&typeof o===\"object\")?o[k]:undefined;process.stdout.write(o==null?\"\":String(o))}catch(e){}})' 2>/dev/null\n else\n printf '%s' '__CC_NO_PARSER__'\n fi\n}\n\ncc_warn_no_parser() {\n marker=\"${TMPDIR:-/tmp}/cc-noparser-warned-api-error-alert-$PPID\"\n [ -f \"$marker\" ] && return 0\n : > \"$marker\" 2>/dev/null\n echo \"WARNING [api-error-alert]: jq, python3 and node are all unavailable, so this hook cannot read the tool call and is NOT protecting you. Install any one of them.\" >&2\n}\n\nREASON=$(cc_read_field stop_reason)\n\nif [ \"$REASON\" = \"__CC_NO_PARSER__\" ]; then\n\n cc_warn_no_parser\n\n exit 0\n\nfi\n[ -z \"$REASON\" ] && REASON=\"unknown\"\nHOOK_EVENT=$(cc_read_field hook_event_name)\n[ -z \"$HOOK_EVENT\" ] && HOOK_EVENT=\"\"\nif [[ \"$REASON\" == \"user\" || \"$REASON\" == \"normal\" || -z \"$REASON\" ]]; then\n exit 0\nfi\nLOG=\"${CC_ERROR_ALERT_LOG:-$HOME/.claude/session-errors.log}\"\nMISSION=\"${CC_CONTEXT_MISSION_FILE:-$HOME/mission.md}\"\nTS=$(date -Iseconds)\nmkdir -p \"$(dirname \"$LOG\")\" 2>/dev/null\necho \"[$TS] Session stopped: reason=$REASON event=$HOOK_EVENT\" >> \"$LOG\"\nif [ -z \"$WSL_DISTRO_NAME\" ]; then\n notify-send \"Claude Code\" \"Session stopped: $REASON\" 2>/dev/null || true\n osascript -e \"display notification \\\"Session stopped: $REASON\\\" with title \\\"Claude Code\\\"\" 2>/dev/null || true\nelse\n powershell.exe -Command \"Write-Host 'Claude Code: Session stopped - $REASON'\" 2>/dev/null || true\nfi\nexit 0\n",
|
|
10
|
+
"clear-command-confirm-guard": "#!/bin/bash\n# clear-command-confirm-guard.sh — Block accidental /clear command\n#\n# Solves: /clear destroys all conversation context with zero\n# confirmation. Prefix matching means /c + Enter can\n# accidentally trigger /clear instead of /commit or /compact (#40931).\n#\n# How it works: Blocks /clear entirely. Use /compact to reduce\n# context without losing it.\n#\n# TRIGGER: UserPromptSubmit\n# MATCHER: \"^/clear$\"\n\nINPUT=$(cat)\n\n# --- Reading the hook payload -------------------------------------------------\n# Why a chain instead of jq alone: on a machine without jq the read returned an\n# empty string, every check below was skipped, and this hook exited 0 in silence.\n# It stayed listed in settings.json and protected nothing. Measured 2026-07-27:\n# a destructive command passed with exit 0 and no output at all.\n# If none of the three readers exist we warn once and allow, because blocking\n# every Bash call would make Claude Code unusable — stopping all work is a\n# failure, not safety.\ncc_read_field() {\n if command -v jq >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | jq -r \".$1 // empty\" 2>/dev/null\n elif command -v python3 >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" python3 -c 'import sys,json,os\ntry:\n d=json.load(sys.stdin)\nexcept Exception:\n sys.exit(0)\nfor k in os.environ.get(\"CC_FIELD\",\"\").split(\".\"):\n d = d.get(k) if isinstance(d,dict) else None\nsys.stdout.write(\"\" if d is None else str(d))\n' 2>/dev/null\n elif command -v node >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" node -e 'let s=\"\";process.stdin.on(\"data\",d=>s+=d).on(\"end\",()=>{try{let o=JSON.parse(s);for(const k of (process.env.CC_FIELD||\"\").split(\".\"))o=(o&&typeof o===\"object\")?o[k]:undefined;process.stdout.write(o==null?\"\":String(o))}catch(e){}})' 2>/dev/null\n else\n printf '%s' '__CC_NO_PARSER__'\n fi\n}\n\ncc_warn_no_parser() {\n marker=\"${TMPDIR:-/tmp}/cc-noparser-warned-clear-command-confirm-guard-$PPID\"\n [ -f \"$marker\" ] && return 0\n : > \"$marker\" 2>/dev/null\n echo \"WARNING [clear-command-confirm-guard]: jq, python3 and node are all unavailable, so this hook cannot read the tool call and is NOT protecting you. Install any one of them.\" >&2\n}\n\nPROMPT=$(cc_read_field prompt)\n\nif [ \"$PROMPT\" = \"__CC_NO_PARSER__\" ]; then\n\n cc_warn_no_parser\n\n exit 0\n\nfi\nif printf '%s' \"$PROMPT\" | grep -qE '^/clear$'; then\n echo \"BLOCKED: /clear permanently destroys all context. Use /compact instead to reduce context safely.\" >&2\n exit 2\nfi\nexit 0\n",
|
|
11
|
+
"claudemd-violation-detector": "#!/bin/bash\n# claudemd-violation-detector.sh — Remind critical CLAUDE.md rules after tool use\n#\n# Solves: Claude ignores CLAUDE.md instructions, especially after\n# context compaction or in long sessions (#40930).\n#\n# How it works: After each tool use, extracts and prints\n# critical rules (ABSOLUTE/MUST NEVER/NEVER/禁止) from CLAUDE.md\n# as a reminder. Runs every N tool calls to avoid noise.\n#\n# TRIGGER: PostToolUse\n# MATCHER: \"\"\n\nset -euo pipefail\n\n# Rate limit: only remind every 20 tool calls\nCOUNTER_FILE=\"/tmp/claudemd-reminder-counter\"\nCOUNT=$(cat \"$COUNTER_FILE\" 2>/dev/null || echo \"0\")\nCOUNT=$((COUNT + 1))\necho \"$COUNT\" > \"$COUNTER_FILE\"\n[ $((COUNT % 20)) -ne 0 ] && exit 0\n\n# Find CLAUDE.md\nCLAUDEMD=\"\"\nfor candidate in \"CLAUDE.md\" \".claude/CLAUDE.md\" \"../CLAUDE.md\"; do\n [ -f \"$candidate\" ] && CLAUDEMD=\"$candidate\" && break\ndone\n[ -z \"$CLAUDEMD\" ] && exit 0\n\n# Extract critical rules\nRULES=$(grep -iE '(ABSOLUTE|MUST NEVER|NEVER DO|禁止|絶対)' \"$CLAUDEMD\" 2>/dev/null | head -5 || true)\n[ -z \"$RULES\" ] && exit 0\n\necho \"📋 CLAUDE.md critical rules reminder:\" >&2\necho \"$RULES\" >&2\nexit 0\n",
|
|
12
|
+
"subagent-context-size-guard": "#!/bin/bash\n# subagent-context-size-guard.sh — Warn on thin subagent prompts\n#\n# Solves: Subagents get spawned with minimal context, leading to\n# poor results because they lack necessary background (#40929).\n# The parent agent assumes shared context, but each subagent\n# starts fresh.\n#\n# How it works: Checks Agent tool's prompt parameter length.\n# If under 100 characters, warns that the prompt may be too thin\n# for a standalone agent to work effectively.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Agent\"\n\nset -euo pipefail\nINPUT=$(cat)\n\n# --- Reading the hook payload -------------------------------------------------\n# Why a chain instead of jq alone: on a machine without jq the read returned an\n# empty string, every check below was skipped, and this hook exited 0 in silence.\n# It stayed listed in settings.json and protected nothing. Measured 2026-07-27:\n# a destructive command passed with exit 0 and no output at all.\n# If none of the three readers exist we warn once and allow, because blocking\n# every Bash call would make Claude Code unusable — stopping all work is a\n# failure, not safety.\ncc_read_field() {\n if command -v jq >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | jq -r \".$1 // empty\" 2>/dev/null\n elif command -v python3 >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" python3 -c 'import sys,json,os\ntry:\n d=json.load(sys.stdin)\nexcept Exception:\n sys.exit(0)\nfor k in os.environ.get(\"CC_FIELD\",\"\").split(\".\"):\n d = d.get(k) if isinstance(d,dict) else None\nsys.stdout.write(\"\" if d is None else str(d))\n' 2>/dev/null\n elif command -v node >/dev/null 2>&1; then\n printf '%s' \"$INPUT\" | CC_FIELD=\"$1\" node -e 'let s=\"\";process.stdin.on(\"data\",d=>s+=d).on(\"end\",()=>{try{let o=JSON.parse(s);for(const k of (process.env.CC_FIELD||\"\").split(\".\"))o=(o&&typeof o===\"object\")?o[k]:undefined;process.stdout.write(o==null?\"\":String(o))}catch(e){}})' 2>/dev/null\n else\n printf '%s' '__CC_NO_PARSER__'\n fi\n}\n\ncc_warn_no_parser() {\n marker=\"${TMPDIR:-/tmp}/cc-noparser-warned-subagent-context-size-guard-$PPID\"\n [ -f \"$marker\" ] && return 0\n : > \"$marker\" 2>/dev/null\n echo \"WARNING [subagent-context-size-guard]: jq, python3 and node are all unavailable, so this hook cannot read the tool call and is NOT protecting you. Install any one of them.\" >&2\n}\n\nPROMPT=$(cc_read_field tool_input.prompt)\n\nif [ \"$PROMPT\" = \"__CC_NO_PARSER__\" ]; then\n\n cc_warn_no_parser\n\n exit 0\n\nfi\n[ -z \"$PROMPT\" ] && exit 0\n\nLEN=${#PROMPT}\nif [ \"$LEN\" -lt 100 ]; then\n echo \"WARNING: Agent prompt is only ${LEN} chars. Subagents start with zero context — include enough background for them to work independently.\" >&2\nfi\nexit 0\n"
|
|
13
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cc-safe-setup
|
|
3
|
-
description: Safety hooks for Claude Code —
|
|
3
|
+
description: Safety hooks for Claude Code — 916 pre-built hooks that prevent file deletion, credential leaks, git disasters, and token waste during autonomous AI coding sessions. Install with npx github:yurukusa/cc-safe-setup.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# cc-safe-setup
|
|
@@ -21,7 +21,7 @@ Installs pre-built safety hooks into your Claude Code environment. These hooks r
|
|
|
21
21
|
## Quick start
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
npx cc-safe-setup
|
|
24
|
+
npx github:yurukusa/cc-safe-setup
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
This runs an interactive wizard that configures hooks based on your risk profile.
|
|
@@ -29,9 +29,9 @@ This runs an interactive wizard that configures hooks based on your risk profile
|
|
|
29
29
|
## Install individual hooks
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
npx cc-safe-setup --install-example large-read-guard
|
|
33
|
-
npx cc-safe-setup --install-example prevent-rm-rf
|
|
34
|
-
npx cc-safe-setup --install-example git-force-push-block
|
|
32
|
+
npx github:yurukusa/cc-safe-setup --install-example large-read-guard
|
|
33
|
+
npx github:yurukusa/cc-safe-setup --install-example prevent-rm-rf
|
|
34
|
+
npx github:yurukusa/cc-safe-setup --install-example git-force-push-block
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
## Why hooks instead of CLAUDE.md rules
|