cc-safe-setup 10.8.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 +211 -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,9 @@ 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');
|
|
112
|
+
const WHY_IDX = process.argv.findIndex(a => a === '--why');
|
|
113
|
+
const WHY_HOOK = WHY_IDX !== -1 ? process.argv[WHY_IDX + 1] : null;
|
|
111
114
|
|
|
112
115
|
if (HELP) {
|
|
113
116
|
console.log(`
|
|
@@ -139,6 +142,8 @@ if (HELP) {
|
|
|
139
142
|
npx cc-safe-setup --doctor Diagnose why hooks aren't working
|
|
140
143
|
npx cc-safe-setup --watch Live dashboard of blocked commands
|
|
141
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
|
|
146
|
+
npx cc-safe-setup --why <hook> Why this hook exists (real incident + issue link)
|
|
142
147
|
npx cc-safe-setup --replay Replay blocked commands timeline (demo/review)
|
|
143
148
|
npx cc-safe-setup --guard "<rule>" Instantly enforce a rule (generate + install + activate)
|
|
144
149
|
npx cc-safe-setup --diff-hooks <path> Compare hooks between two settings files
|
|
@@ -922,6 +927,210 @@ async function fullSetup() {
|
|
|
922
927
|
console.log();
|
|
923
928
|
}
|
|
924
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
|
+
|
|
1070
|
+
async function why(hookName) {
|
|
1071
|
+
const WHY_DATA = {
|
|
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' },
|
|
1073
|
+
'branch-guard': { issue: '#36640', incident: 'Autonomous Claude pushed untested code to main at 3am', url: 'https://github.com/anthropics/claude-code/issues/36640' },
|
|
1074
|
+
'secret-guard': { issue: '#16561', incident: 'API keys committed to public repo via git add .', url: 'https://github.com/anthropics/claude-code/issues/16561' },
|
|
1075
|
+
'block-database-wipe': { issue: '#37405', incident: 'Production database wiped by migrate:fresh', url: 'https://github.com/anthropics/claude-code/issues/37405' },
|
|
1076
|
+
'uncommitted-work-guard': { issue: '#37888', incident: 'Claude destroyed uncommitted work twice in same session', url: 'https://github.com/anthropics/claude-code/issues/37888' },
|
|
1077
|
+
'test-deletion-guard': { issue: '#38050', incident: 'Claude deleted failing tests instead of fixing code', url: 'https://github.com/anthropics/claude-code/issues/38050' },
|
|
1078
|
+
'fact-check-gate': { issue: '#38057', incident: 'Claude wrote false claims in technical docs without reading source', url: 'https://github.com/anthropics/claude-code/issues/38057' },
|
|
1079
|
+
'token-budget-guard': { issue: '#38029', incident: 'Session consumed $342 in tokens without user knowing', url: 'https://github.com/anthropics/claude-code/issues/38029' },
|
|
1080
|
+
'protect-dotfiles': { issue: '#37478', incident: '.bashrc and environment files overwritten', url: 'https://github.com/anthropics/claude-code/issues/37478' },
|
|
1081
|
+
'scope-guard': { issue: '#36233', incident: 'Entire Mac filesystem deleted by out-of-scope operation', url: 'https://github.com/anthropics/claude-code/issues/36233' },
|
|
1082
|
+
'case-sensitive-guard': { issue: '#37875', incident: 'exFAT case collision caused data loss via rm -rf', url: 'https://github.com/anthropics/claude-code/issues/37875' },
|
|
1083
|
+
'prompt-injection-guard': { issue: '#38046', incident: 'Prompt injection found in /insights output', url: 'https://github.com/anthropics/claude-code/issues/38046' },
|
|
1084
|
+
'overwrite-guard': { issue: '#37595', incident: '/export overwrites existing files without warning', url: 'https://github.com/anthropics/claude-code/issues/37595' },
|
|
1085
|
+
'memory-write-guard': { issue: '#38040', incident: 'No way to see what Claude writes to ~/.claude/', url: 'https://github.com/anthropics/claude-code/issues/38040' },
|
|
1086
|
+
'context-monitor': { issue: '#6527', incident: 'Sessions silently lost all state after 150+ tool calls', url: 'https://github.com/anthropics/claude-code/issues/6527' },
|
|
1087
|
+
'comment-strip': { issue: '#29582', incident: 'Bash comments in hook commands broke permission matching', url: 'https://github.com/anthropics/claude-code/issues/29582' },
|
|
1088
|
+
'cd-git-allow': { issue: '#32985', incident: 'cd+git compounds spammed permission prompts endlessly', url: 'https://github.com/anthropics/claude-code/issues/32985' },
|
|
1089
|
+
'strict-allowlist': { issue: '#37471', incident: 'Denylist model creates arms race — Claude finds bypasses', url: 'https://github.com/anthropics/claude-code/issues/37471' },
|
|
1090
|
+
'error-memory-guard': { issue: 'common', incident: 'Claude retries the same failing command 10+ times' },
|
|
1091
|
+
'typosquat-guard': { issue: 'supply-chain', incident: 'Misspelled package names can install malware' },
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
console.log();
|
|
1095
|
+
if (!hookName) {
|
|
1096
|
+
console.log(c.bold + ' cc-safe-setup --why <hook-name>' + c.reset);
|
|
1097
|
+
console.log(c.dim + ' Show why a hook exists — the real incident that inspired it.' + c.reset);
|
|
1098
|
+
console.log();
|
|
1099
|
+
console.log(' Examples:');
|
|
1100
|
+
console.log(c.dim + ' npx cc-safe-setup --why destructive-guard' + c.reset);
|
|
1101
|
+
console.log(c.dim + ' npx cc-safe-setup --why token-budget-guard' + c.reset);
|
|
1102
|
+
console.log();
|
|
1103
|
+
console.log(` ${Object.keys(WHY_DATA).length} hooks have documented incidents.`);
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
const name = hookName.replace('.sh', '');
|
|
1108
|
+
const data = WHY_DATA[name];
|
|
1109
|
+
if (!data) {
|
|
1110
|
+
console.log(c.yellow + ` No incident documented for "${name}".` + c.reset);
|
|
1111
|
+
console.log(c.dim + ' This hook may have been created proactively.' + c.reset);
|
|
1112
|
+
console.log();
|
|
1113
|
+
console.log(c.dim + ` Hooks with documented incidents: ${Object.keys(WHY_DATA).join(', ')}` + c.reset);
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
console.log(c.bold + ` Why "${name}" exists` + c.reset);
|
|
1118
|
+
console.log();
|
|
1119
|
+
console.log(c.red + ' Incident:' + c.reset);
|
|
1120
|
+
console.log(' ' + data.incident);
|
|
1121
|
+
console.log();
|
|
1122
|
+
if (data.url) {
|
|
1123
|
+
console.log(c.blue + ' Source:' + c.reset);
|
|
1124
|
+
console.log(' ' + data.url);
|
|
1125
|
+
}
|
|
1126
|
+
if (data.issue && data.issue !== 'common' && data.issue !== 'supply-chain') {
|
|
1127
|
+
console.log(c.dim + ` GitHub Issue: ${data.issue}` + c.reset);
|
|
1128
|
+
}
|
|
1129
|
+
console.log();
|
|
1130
|
+
console.log(c.dim + ' Install: npx cc-safe-setup --install-example ' + name + c.reset);
|
|
1131
|
+
console.log();
|
|
1132
|
+
}
|
|
1133
|
+
|
|
925
1134
|
async function replay() {
|
|
926
1135
|
console.log();
|
|
927
1136
|
console.log(c.bold + ' cc-safe-setup --replay' + c.reset);
|
|
@@ -4051,6 +4260,8 @@ async function main() {
|
|
|
4051
4260
|
if (FULL) return fullSetup();
|
|
4052
4261
|
if (DOCTOR) return doctor();
|
|
4053
4262
|
if (WATCH) return watch();
|
|
4263
|
+
if (SUGGEST) return suggest();
|
|
4264
|
+
if (WHY_IDX !== -1) return why(WHY_HOOK);
|
|
4054
4265
|
if (REPLAY) return replay();
|
|
4055
4266
|
if (GUARD_IDX !== -1) return guard(GUARD_DESC);
|
|
4056
4267
|
if (DIFF_HOOKS_IDX !== -1) return diffHooks(DIFF_HOOKS);
|
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": {
|