cc-safe-setup 10.9.0 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/session-snapshot.md +9 -9
- package/README.md +1 -1
- package/cc-safe-setup-export.json +6 -1
- package/index.mjs +143 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Session Snapshot (auto-generated)
|
|
2
|
-
Updated: 2026-03-
|
|
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:
|
|
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/
|
|
21
|
-
./examples/
|
|
22
|
-
./examples/
|
|
23
|
-
./examples/
|
|
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**.
|
|
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-
|
|
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",
|
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": "
|
|
3
|
+
"version": "11.0.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": {
|