cc-safe-setup 10.9.0 → 11.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  # Session Snapshot (auto-generated)
2
- Updated: 2026-03-24T19:01:52+09:00
2
+ Updated: 2026-03-24T20:43:46+09:00
3
3
 
4
4
  ## Git
5
5
  - Branch: `main`
@@ -7,19 +7,19 @@ Updated: 2026-03-24T19:01:52+09:00
7
7
  ```
8
8
  M .claude/session-snapshot.md
9
9
  ```
10
- - Last commit: 0696585 checkpoint: auto-save 19:01:50
10
+ - Last commit: 5711024 checkpoint: auto-save 20:43:45
11
11
 
12
12
  ## Recent Files
13
13
  ```
14
14
  ./.claude/session-snapshot.md
15
15
  ./test.sh
16
- ./CHANGELOG.md
17
- ./examples/debug-leftover-guard.sh
18
- ./examples/ci-skip-guard.sh
19
16
  ./README.md
20
- ./examples/rust/destructive_guard.rs
21
- ./examples/package-script-guard.sh
22
- ./examples/env-drift-guard.sh
23
- ./examples/import-cycle-warn.sh
17
+ ./examples/typosquat-guard.sh
18
+ ./examples/typescript-strict-guard.sh
19
+ ./examples/git-author-guard.sh
20
+ ./examples/permission-cache.sh
21
+ ./CHANGELOG.md
22
+ ./examples/stale-env-guard.sh
23
+ ./examples/test-coverage-guard.sh
24
24
  ```
25
25
 
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  **One command to make Claude Code safe for autonomous operation.** [日本語](docs/README.ja.md)
8
8
 
9
- 8 built-in + 104 examples = **118 hooks**. 39 CLI commands. 531 tests. 5 languages. [**Hub**](https://yurukusa.github.io/cc-safe-setup/hub.html) · [Cheat Sheet](https://yurukusa.github.io/cc-safe-setup/hooks-cheatsheet.html) · [Builder](https://yurukusa.github.io/cc-safe-setup/builder.html) · [FAQ](https://yurukusa.github.io/cc-safe-setup/faq.html) · [Examples](https://yurukusa.github.io/cc-safe-setup/by-example.html) · [Matrix](https://yurukusa.github.io/cc-safe-setup/matrix.html) · [Playground](https://yurukusa.github.io/cc-hook-registry/playground.html)
9
+ 8 built-in + 104 examples = **118 hooks**. 40 CLI commands. 544 tests. 5 languages. [**Hub**](https://yurukusa.github.io/cc-safe-setup/hub.html) · [Cheat Sheet](https://yurukusa.github.io/cc-safe-setup/hooks-cheatsheet.html) · [Builder](https://yurukusa.github.io/cc-safe-setup/builder.html) · [FAQ](https://yurukusa.github.io/cc-safe-setup/faq.html) · [Examples](https://yurukusa.github.io/cc-safe-setup/by-example.html) · [Matrix](https://yurukusa.github.io/cc-safe-setup/matrix.html) · [Playground](https://yurukusa.github.io/cc-hook-registry/playground.html)
10
10
 
11
11
  ```bash
12
12
  npx cc-safe-setup
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": "1.0",
3
3
  "generator": "cc-safe-setup",
4
- "exported_at": "2026-03-24T10:02:00.912Z",
4
+ "exported_at": "2026-03-24T11:43:55.322Z",
5
5
  "hooks": {
6
6
  "UserPromptSubmit": [
7
7
  {
@@ -179,6 +179,10 @@
179
179
  {
180
180
  "type": "command",
181
181
  "command": "bash /home/namakusa/.claude/hooks/tweet-guard.sh"
182
+ },
183
+ {
184
+ "type": "command",
185
+ "command": "bash /home/namakusa/.claude/hooks/guard-database.sh"
182
186
  }
183
187
  ]
184
188
  },
@@ -301,6 +305,7 @@
301
305
  "~/.claude/hooks/stop-tachikoma-loop.sh": "#!/bin/bash\n# ================================================================\n# stop-tachikoma-loop.sh\n# ----------------------------------------------------------------\n# 目的: CC最終発言を /tmp/cc-last-message.txt に書き出し、\n# tachikoma-loopへ引き継ぐ\n# 発火条件: Stop(セッション終了時)\n# 副作用: /tmp/cc-last-message.txt 作成・上書き\n# /tmp/stop-hook-debug.log に追記\n# ================================================================\n\nTIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')\nHANDOFF_FILE=\"/tmp/cc-last-message.txt\"\nDEBUG_LOG=\"/tmp/stop-hook-debug.log\"\n\n# === stdinからJSON読み取り ===\nSTDIN_TMP=\"/tmp/stop-hook-stdin-$$.json\"\ncat > \"$STDIN_TMP\"\n\nTRANSCRIPT_PATH=\"\"\nif [ -s \"$STDIN_TMP\" ]; then\n TRANSCRIPT_PATH=$(python3 -c \"\nimport json\nwith open('$STDIN_TMP') as f:\n data = json.load(f)\nprint(data.get('transcript_path', ''))\n\" 2>/dev/null)\nfi\n\n# === CCの最終発言を取得 ===\nMSG=\"\"\nif [ -n \"$TRANSCRIPT_PATH\" ] && [ -f \"$TRANSCRIPT_PATH\" ]; then\n MSG=$(python3 -c \"\nimport json, subprocess\nresult = subprocess.run(['tail', '-100', '$TRANSCRIPT_PATH'], capture_output=True, text=True)\nlast_msg = ''\nfor line in result.stdout.strip().split('\\n'):\n line = line.strip()\n if not line: continue\n try:\n obj = json.loads(line)\n if obj.get('type') == 'assistant':\n content = obj.get('message', {}).get('content', '')\n if isinstance(content, list):\n texts = [c.get('text','') for c in content if c.get('type') == 'text' and c.get('text')]\n if texts:\n last_msg = ' '.join(texts)\n elif isinstance(content, str) and content:\n last_msg = content\n except:\n pass\nprint(last_msg[-500:] if last_msg else '')\n\" 2>/dev/null)\nfi\n\nrm -f \"$STDIN_TMP\"\n\nif [ -z \"$MSG\" ]; then\n MSG=\"(transcript読み取り失敗)\"\nfi\n\n# === 引き継ぎファイル書き出し ===\ncat > \"$HANDOFF_FILE\" << EOF\ntimestamp: $TIMESTAMP\ntranscript: ${TRANSCRIPT_PATH:-unknown}\nlast_message: |\n$(echo \"$MSG\" | sed 's/^/ /')\nEOF\n\necho \"[$TIMESTAMP] $(echo \"$MSG\" | head -c 100)\" >> \"$DEBUG_LOG\"\n\necho \"tachikoma-loopが次のセッションを起動します。\"\nexit 0\n",
302
306
  "~/.claude/hooks/task-complete-nudge.sh": "#!/bin/bash\n# task-complete-nudge.sh — タスク完了時に次のアクションを促す\n# なぜ: CCがタスク完了後にidleになる問題を防止(2026-02-18 ぐらす指摘)\n# 改修 (2026-02-28): task-checkを自動実行。「思いつきで次に飛びつく」防止\n# トリガー: TaskCompleted イベント\n\necho \"⚡ タスク完了。止まるな。次を確認して動け:\"\necho \"\"\n\n# task-checkを自動実行(今日の投稿候補 + mission.md最重要タスクを表示)\nif command -v task-check &>/dev/null; then\n task-check\nelif [ -x \"$HOME/bin/task-check\" ]; then\n \"$HOME/bin/task-check\"\nfi\n\necho \"\"\necho \"↑ 上記リストから次タスクを選んで即座に実行。idle禁止。\"\n",
303
307
  "~/.claude/hooks/tweet-guard.sh": "#!/bin/bash\n# ================================================================\n# tweet-guard.sh\n# ----------------------------------------------------------------\n# 目的: 非推奨 ~/bin/tweet (SendKeysベース) をブロックし、\n# CDP直接操作方式を案内する\n# 発火条件: PreToolUse(Bash実行前)\n# 副作用: tweetコマンド検出時 exit 2 でブロック\n# CDPスクリプト新規作成は警告のみ(ブロックなし)\n# ================================================================\n\n# stdinからJSON取得(2026-02-15修正: env varは設定されない)\nINPUT=$(cat)\nTOOL_INPUT=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null)\n\n# ~/bin/tweet または単純な tweet コマンドを検出\nif echo \"$TOOL_INPUT\" | grep -qE '(^|[|;&\\s])tweet\\s+\"' || \\\n echo \"$TOOL_INPUT\" | grep -qE '~/bin/tweet' || \\\n echo \"$TOOL_INPUT\" | grep -qE '/home/namakusa/bin/tweet\\b'; then\n\n echo \"🛡️ tweet-guard: ~/bin/tweet はSendKeysベースで不安定。ブロックします。\"\n echo \"\"\n echo \"📋 代わりにCDP直接操作を使ってください:\"\n echo \" 1. 新タブ作成: powershell.exe -Command \\\"Invoke-RestMethod -Method Put -Uri 'http://127.0.0.1:9223/json/new?https://x.com/compose/post'\\\"\"\n echo \" 2. cdp_helper.ps1 -Action navigate で移動\"\n echo \" 3. UTF-8ファイルにテキストを書き出し\"\n echo \" 4. cdp_helper.ps1 -Action eval でフォーカス\"\n echo \" 5. Input.insertText でテキスト挿入\"\n echo \" 6. Input.dispatchMouseEvent でポストボタンクリック\"\n echo \" 7. プロフィールで投稿を検証\"\n echo \"\"\n echo \"📖 詳細: LESSONS.md「X投稿の推奨フロー」\"\n echo \"📖 教訓: memory/twitter-lessons.md\"\n exit 2 # exit 2 = block the tool call\nfi\n\n# CDP操作でPSスクリプトを新規作成しようとしている場合の警告\nif echo \"$TOOL_INPUT\" | grep -qE 'cat > /tmp/.*\\.ps1.*ClientWebSocket|cat > /tmp/.*\\.ps1.*WebSocketMessageType'; then\n echo \"⚠️ tweet-guard: CDPスクリプトを新規作成しようとしています。\"\n echo \" /tmp/cdp_helper.ps1 が既に存在し、以下の機能を持ちます:\"\n echo \" - beforeunloadダイアログ自動承認\"\n echo \" - WebSocket切断リカバリー\"\n echo \" - navigate / eval / screenshot 統合\"\n echo \"\"\n echo \" cdp_helper.ps1 を先に確認してください。\"\n # 警告のみ(ブロックはしない)- exit 0\nfi\n\nexit 0\n",
308
+ "~/.claude/hooks/guard-database.sh": "#!/bin/bash\nCOMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)\n[ -z \"$COMMAND\" ] && exit 0\nif echo \"$COMMAND\" | grep -qiE '(DROP\\s+(DATABASE|TABLE)|migrate:fresh|prisma\\s+reset|db:drop|TRUNCATE)'; then\n echo \"BLOCKED: Database operation blocked by guard rule.\" >&2\n echo \"Rule: never touch the database\" >&2\n exit 2\nfi\nexit 0",
304
309
  "~/.claude/hooks/image-validate.sh": "#!/bin/bash\n# image-validate.sh — PreToolUse hook for Read\n# 壊れた画像をRead→API送信する前にブロックする\n# なぜ: 壊れた画像がAPIに渡ると \"Could not process image\" 400エラーで\n# CCがスタックし、人間の介入が必要になる(2026-03-10 事故)\n\n# なぜstdinか: CC hooksはツール入力をstdinで渡す。$TOOL_INPUT環境変数は空(2026-03-10 事故)\nINPUT=$(cat)\nfile_path=$(echo \"$INPUT\" | python3 -c \"import json,sys; print(json.load(sys.stdin).get('file_path',''))\" 2>/dev/null)\n[ -z \"$file_path\" ] && exit 0\n\n# 画像拡張子のみチェック対象\ncase \"${file_path,,}\" in\n *.png|*.jpg|*.jpeg|*.gif|*.webp|*.bmp|*.ico|*.tiff|*.tif) ;;\n *) exit 0 ;;\nesac\n\n# ファイル存在チェック\n[ ! -f \"$file_path\" ] && exit 0\n\n# fileコマンドで画像として認識されるか(最も信頼性の高い判定)\nsize=$(stat -c%s \"$file_path\" 2>/dev/null || echo 0)\nfile_type=$(file -b \"$file_path\" 2>/dev/null || echo \"unknown\")\ncase \"$file_type\" in\n *image*|*PNG*|*JPEG*|*GIF*|*bitmap*|*Web/P*|*Icon*) exit 0 ;;\nesac\n\n# 認識不可 = 壊れている(拡張子は画像だがfileコマンドは画像と認識しなかった)\necho \"⚠ 画像として認識できません (type: ${file_type}, ${size} bytes): $file_path — この画像のReadをスキップして続行してください\"\nexit 1\n",
305
310
  "~/.claude/hooks/decision-warn.sh": "#!/bin/bash\n# decision-warn.sh: PreToolUse hook for Edit|Write\n# bin/** or .claude/hooks/** への変更時に、合意済み決定の有無を確認し警告(ブロックしない)\n#\n# 設計方針(ぐらす指示):\n# - ブロックしない(exit 0固定)\n# - AIの手足を縛るな。できることは全部できるようにする\n# - 「合意なし」を目立たせるだけ。止めるのではなく記録する\n# - 仲間としてのアドバイス、権限的な制限ではない\n\n# stdinからJSON取得(他hookと同一パターン。2026-02-15修正: tool_input内のfile_pathを参照)\nINPUT=$(cat)\nFILE_PATH=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty' 2>/dev/null)\n\n# If no file path detected, skip\nif [[ -z \"$FILE_PATH\" ]]; then\n exit 0\nfi\n\n# フルパス限定でスコープチェック(Codex指摘: 他プロジェクトのbin/誤爆防止)\nHOME_BIN=\"$HOME/bin/\"\nHOME_HOOKS=\"$HOME/.claude/hooks/\"\n\nIN_SCOPE=0\ncase \"$FILE_PATH\" in\n ${HOME_BIN}*) IN_SCOPE=1 ;;\n ${HOME_HOOKS}*) IN_SCOPE=1 ;;\nesac\n\nif (( IN_SCOPE == 0 )); then\n exit 0\nfi\n\n# Check for matching pending/agreed decision\nDECISIONS_DIR=\"$HOME/ops/decisions/pending\"\nHAS_AGREED=0\n\nfor f in \"$DECISIONS_DIR\"/D-*.md; do\n [[ -f \"$f\" ]] || continue\n\n local_scope=$(grep '^scope: ' \"$f\" 2>/dev/null | head -1 | sed 's/^scope: //')\n local_status=$(grep '^status: ' \"$f\" 2>/dev/null | head -1 | sed 's/^status: //')\n\n scope_base=$(echo \"$local_scope\" | sed 's/\\*\\*//' | sed 's|/$||')\n if [[ \"$FILE_PATH\" == *\"$scope_base\"* && \"$local_status\" == \"agreed\" ]]; then\n HAS_AGREED=1\n break\n fi\ndone\n\nif (( HAS_AGREED == 0 )); then\n echo \"NOTE: Editing monitored path without agreed decision: $FILE_PATH\"\n echo \" → Run: decision new --scope \\\"bin/**\\\" --title \\\"<description>\\\"\"\n echo \" → This change will be recorded as 'unreviewed'\"\nfi\n\n# Always exit 0 - never block\nexit 0\n",
306
311
  "~/.claude/hooks/no-tools-sprawl.sh": "#!/bin/bash\n# no-tools-sprawl.sh: PreToolUse hook for Edit|Write\n# 目的: ~/tools/** へのスクリプト増殖を機械的に止める(progress odor / token浪費の主因)。\n# 方針: ツール化が必要なら、まず既存フロー(ops/scripts/** or bridge-js -f)で1発に寄せる。\n# どうしても必要なら drafts/ に「1回限り」1ファイルで置く。恒久ツール化は合意の上で。\n#\n# Override (emergency only):\n# CC_ALLOW_TOOLS_SPRAWL=1 で一時的にブロック解除(使ったらproof-log/decisionに理由を残すこと)\n\nset -euo pipefail\n\nINPUT=\"$(cat || true)\"\nFILE_PATH=\"$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty' 2>/dev/null)\"\n\n[[ -z \"$FILE_PATH\" ]] && exit 0\n\nTOOLS_DIR=\"$HOME/tools/\"\ncase \"$FILE_PATH\" in\n ${TOOLS_DIR}*)\n if [[ \"${CC_ALLOW_TOOLS_SPRAWL:-}\" == \"1\" ]]; then\n exit 0\n fi\n echo \"BLOCKED: ~/tools/** への追加・編集は禁止(ツール増殖はvalue_per_tokenを下げる)。\"\n echo \"\"\n echo \"検出パス: $FILE_PATH\"\n echo \"\"\n echo \"代替:\"\n echo \" - 既存の ops/scripts/x/ を使う(推奨)\"\n echo \" - 1回限りの送信用コードは drafts/ に 1ファイルだけ置く\"\n echo \"\"\n echo \"例外(緊急):\"\n echo \" CC_ALLOW_TOOLS_SPRAWL=1 を付けて実行(その場合、理由をproof-log/decisionに記録)\"\n exit 2\n ;;\nesac\n\nexit 0\n\n",
@@ -0,0 +1,36 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # max-session-duration.sh — Warn when session exceeds time limit
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # Long autonomous sessions can rack up costs and context issues.
7
+ # This hook tracks session duration and warns when it exceeds
8
+ # a configurable limit, suggesting a new session.
9
+ #
10
+ # TRIGGER: PostToolUse MATCHER: ""
11
+ #
12
+ # CONFIG:
13
+ # CC_MAX_SESSION_HOURS=4 (warn after 4 hours)
14
+ # ================================================================
15
+
16
+ MAX_HOURS="${CC_MAX_SESSION_HOURS:-4}"
17
+ STATE="/tmp/cc-session-start-$(echo "$PWD" | md5sum | cut -c1-8)"
18
+
19
+ NOW=$(date +%s)
20
+
21
+ if [ ! -f "$STATE" ]; then
22
+ echo "$NOW" > "$STATE"
23
+ exit 0
24
+ fi
25
+
26
+ START=$(cat "$STATE" 2>/dev/null || echo "$NOW")
27
+ ELAPSED=$(( (NOW - START) / 3600 ))
28
+
29
+ if [ "$ELAPSED" -ge "$MAX_HOURS" ]; then
30
+ MINS=$(( (NOW - START) / 60 ))
31
+ echo "WARNING: Session running for ${ELAPSED}h ${MINS}m." >&2
32
+ echo "Consider starting a new session to reset context." >&2
33
+ echo "Reset timer: rm $STATE" >&2
34
+ fi
35
+
36
+ exit 0
package/index.mjs CHANGED
@@ -108,6 +108,7 @@ const COMPARE = COMPARE_IDX !== -1 ? { a: process.argv[COMPARE_IDX + 1], b: proc
108
108
  const REPLAY = process.argv.includes('--replay');
109
109
  const CREATE_IDX = process.argv.findIndex(a => a === '--create');
110
110
  const CREATE_DESC = CREATE_IDX !== -1 ? process.argv.slice(CREATE_IDX + 1).join(' ') : null;
111
+ const SUGGEST = process.argv.includes('--suggest');
111
112
  const WHY_IDX = process.argv.findIndex(a => a === '--why');
112
113
  const WHY_HOOK = WHY_IDX !== -1 ? process.argv[WHY_IDX + 1] : null;
113
114
 
@@ -141,6 +142,7 @@ if (HELP) {
141
142
  npx cc-safe-setup --doctor Diagnose why hooks aren't working
142
143
  npx cc-safe-setup --watch Live dashboard of blocked commands
143
144
  npx cc-safe-setup --create "<desc>" Generate a custom hook from description
145
+ npx cc-safe-setup --suggest Analyze project and predict risks → suggest hooks
144
146
  npx cc-safe-setup --why <hook> Why this hook exists (real incident + issue link)
145
147
  npx cc-safe-setup --replay Replay blocked commands timeline (demo/review)
146
148
  npx cc-safe-setup --guard "<rule>" Instantly enforce a rule (generate + install + activate)
@@ -925,6 +927,146 @@ async function fullSetup() {
925
927
  console.log();
926
928
  }
927
929
 
930
+ async function suggest() {
931
+ const { execSync } = await import('child_process');
932
+ const { readdirSync } = await import('fs');
933
+ console.log();
934
+ console.log(c.bold + ' cc-safe-setup --suggest' + c.reset);
935
+ console.log(c.dim + ' Analyzing your project for potential risks...' + c.reset);
936
+ console.log();
937
+
938
+ const cwd = process.cwd();
939
+ const risks = [];
940
+
941
+ // 1. Check git history for past incidents
942
+ try {
943
+ const log = execSync('git log --oneline -100 2>/dev/null', { encoding: 'utf-8' });
944
+ if (log.includes('revert') || log.includes('Revert')) {
945
+ risks.push({ level: 'high', risk: 'Reverts in git history — code has been rolled back', hook: 'auto-checkpoint', reason: 'Auto-checkpoint protects against needing reverts' });
946
+ }
947
+ if (log.includes('force') || log.includes('--force')) {
948
+ risks.push({ level: 'high', risk: 'Force operations in history', hook: 'branch-guard', reason: 'Prevents force-push and destructive git' });
949
+ }
950
+ if (log.match(/fix.*fix.*fix/i)) {
951
+ risks.push({ level: 'medium', risk: 'Multiple fix commits in sequence — possible churn', hook: 'verify-before-done', reason: 'Ensures tests pass before committing fixes' });
952
+ }
953
+ } catch {}
954
+
955
+ // 2. Check for risky file patterns
956
+ const hasEnv = existsSync(join(cwd, '.env'));
957
+ const hasEnvExample = existsSync(join(cwd, '.env.example'));
958
+ if (hasEnv && !existsSync(join(cwd, '.gitignore'))) {
959
+ risks.push({ level: 'critical', risk: '.env exists but no .gitignore — secrets may be committed', hook: 'secret-guard', reason: 'Blocks git add .env' });
960
+ }
961
+ if (hasEnv && hasEnvExample) {
962
+ risks.push({ level: 'low', risk: '.env and .env.example both exist', hook: 'env-drift-guard', reason: 'Detects variable mismatch between the two' });
963
+ }
964
+
965
+ // 3. Check for database usage
966
+ const hasPrisma = existsSync(join(cwd, 'prisma'));
967
+ const hasRails = existsSync(join(cwd, 'Gemfile'));
968
+ const hasLaravel = existsSync(join(cwd, 'artisan'));
969
+ const hasDjango = existsSync(join(cwd, 'manage.py'));
970
+ if (hasPrisma || hasRails || hasLaravel || hasDjango) {
971
+ risks.push({ level: 'high', risk: 'Database framework detected — destructive migrations possible', hook: 'block-database-wipe', reason: 'Blocks DROP, migrate:fresh, db:drop' });
972
+ }
973
+
974
+ // 4. Check for deployment config
975
+ const hasDocker = existsSync(join(cwd, 'Dockerfile'));
976
+ const hasVercel = existsSync(join(cwd, 'vercel.json'));
977
+ const hasNetlify = existsSync(join(cwd, 'netlify.toml'));
978
+ if (hasDocker || hasVercel || hasNetlify) {
979
+ risks.push({ level: 'medium', risk: 'Deploy configuration found', hook: 'deploy-guard', reason: 'Prevents deploy with uncommitted changes' });
980
+ risks.push({ level: 'low', risk: 'Friday deploys possible', hook: 'no-deploy-friday', reason: 'Block deploys on Fridays' });
981
+ }
982
+
983
+ // 5. Check package.json for risky scripts
984
+ if (existsSync(join(cwd, 'package.json'))) {
985
+ try {
986
+ const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf-8'));
987
+ if (!pkg.scripts?.test || pkg.scripts.test.includes('no test')) {
988
+ risks.push({ level: 'medium', risk: 'No test script — code changes go unverified', hook: 'test-coverage-guard', reason: 'Warns when code grows without tests' });
989
+ }
990
+ if (pkg.scripts?.deploy || pkg.scripts?.publish) {
991
+ risks.push({ level: 'medium', risk: 'Deploy/publish script exists', hook: 'npm-publish-guard', reason: 'Version check before publish' });
992
+ }
993
+ } catch {}
994
+ }
995
+
996
+ // 6. Check for large repo (many files)
997
+ try {
998
+ const fileCount = parseInt(execSync('git ls-files | wc -l 2>/dev/null', { encoding: 'utf-8' }).trim());
999
+ if (fileCount > 500) {
1000
+ risks.push({ level: 'medium', risk: `Large repo (${fileCount} files) — scope creep risk`, hook: 'scope-guard', reason: 'Prevents operations outside project' });
1001
+ risks.push({ level: 'low', risk: 'Large diffs more likely', hook: 'diff-size-guard', reason: 'Warns on large uncommitted changes' });
1002
+ }
1003
+ } catch {}
1004
+
1005
+ // 7. Check for .claude/ config
1006
+ if (!existsSync(join(cwd, 'CLAUDE.md'))) {
1007
+ risks.push({ level: 'medium', risk: 'No CLAUDE.md — Claude has no project-specific rules', hook: null, reason: 'Run --shield to generate one' });
1008
+ }
1009
+
1010
+ // 8. Always recommend essentials if not installed
1011
+ let installed = new Set();
1012
+ if (existsSync(SETTINGS_PATH)) {
1013
+ try {
1014
+ const s = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
1015
+ for (const groups of Object.values(s.hooks || {})) {
1016
+ for (const g of groups) {
1017
+ for (const h of (g.hooks || [])) {
1018
+ installed.add((h.command || '').split('/').pop().replace('.sh', ''));
1019
+ }
1020
+ }
1021
+ }
1022
+ } catch {}
1023
+ }
1024
+
1025
+ if (!installed.has('destructive-guard')) {
1026
+ risks.push({ level: 'critical', risk: 'No destructive-guard — rm -rf / is not blocked', hook: 'destructive-guard', reason: 'Essential: prevents file system destruction' });
1027
+ }
1028
+
1029
+ // Display results
1030
+ if (risks.length === 0) {
1031
+ console.log(c.green + ' No risks detected. Your project looks safe!' + c.reset);
1032
+ return;
1033
+ }
1034
+
1035
+ risks.sort((a, b) => {
1036
+ const order = { critical: 0, high: 1, medium: 2, low: 3 };
1037
+ return (order[a.level] || 9) - (order[b.level] || 9);
1038
+ });
1039
+
1040
+ const levelColors = { critical: c.red, high: c.red, medium: c.yellow, low: c.dim };
1041
+ const levelIcons = { critical: '🔴', high: '🟠', medium: '🟡', low: '⚪' };
1042
+
1043
+ for (const r of risks) {
1044
+ const color = levelColors[r.level] || c.dim;
1045
+ const icon = levelIcons[r.level] || '·';
1046
+ console.log(` ${icon} ${color}${r.level.toUpperCase()}${c.reset}: ${r.risk}`);
1047
+ if (r.hook) {
1048
+ const isInstalled = installed.has(r.hook);
1049
+ if (isInstalled) {
1050
+ console.log(c.green + ` ✓ ${r.hook} (installed)` + c.reset);
1051
+ } else {
1052
+ console.log(c.yellow + ` → Install: npx cc-safe-setup --install-example ${r.hook}` + c.reset);
1053
+ }
1054
+ } else {
1055
+ console.log(c.dim + ` → ${r.reason}` + c.reset);
1056
+ }
1057
+ console.log();
1058
+ }
1059
+
1060
+ const unprotected = risks.filter(r => r.hook && !installed.has(r.hook));
1061
+ if (unprotected.length > 0) {
1062
+ console.log(c.bold + ` ${unprotected.length} unprotected risk(s). Fix all:` + c.reset);
1063
+ console.log(c.yellow + ' npx cc-safe-setup --shield' + c.reset);
1064
+ } else {
1065
+ console.log(c.green + ' All detected risks are covered by installed hooks!' + c.reset);
1066
+ }
1067
+ console.log();
1068
+ }
1069
+
928
1070
  async function why(hookName) {
929
1071
  const WHY_DATA = {
930
1072
  'destructive-guard': { issue: '#36339', incident: 'User lost entire C:\\Users directory — rm -rf followed NTFS junctions', url: 'https://github.com/anthropics/claude-code/issues/36339' },
@@ -4118,6 +4260,7 @@ async function main() {
4118
4260
  if (FULL) return fullSetup();
4119
4261
  if (DOCTOR) return doctor();
4120
4262
  if (WATCH) return watch();
4263
+ if (SUGGEST) return suggest();
4121
4264
  if (WHY_IDX !== -1) return why(WHY_HOOK);
4122
4265
  if (REPLAY) return replay();
4123
4266
  if (GUARD_IDX !== -1) return guard(GUARD_DESC);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-safe-setup",
3
- "version": "10.9.0",
3
+ "version": "11.1.0",
4
4
  "description": "One command to make Claude Code safe. 59 hooks (8 built-in + 51 examples). 26 CLI commands: dashboard, create, audit, lint, diff, migrate, compare, generate-ci. 284 tests.",
5
5
  "main": "index.mjs",
6
6
  "bin": {