cc-safe-setup 11.7.0 → 11.8.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/README.md +15 -1
- package/package.json +1 -1
- package/.claude/session-snapshot.md +0 -25
- package/cc-safe-setup-export.json +0 -321
- package/docs/README.ja.md +0 -64
- package/docs/ROADMAP.md +0 -83
- package/docs/builder.html +0 -283
- package/docs/by-example.html +0 -234
- package/docs/cheatsheet.html +0 -187
- package/docs/ecosystem.html +0 -223
- package/docs/faq.html +0 -244
- package/docs/hooks-cheatsheet.html +0 -319
- package/docs/hub.html +0 -155
- package/docs/index-legacy.html +0 -685
- package/docs/index.html +0 -271
- package/docs/matrix.html +0 -139
- package/docs/migration-guide.html +0 -198
- package/docs/settings-reference.html +0 -317
- package/docs/troubleshooting.html +0 -189
- package/examples/go/destructive_guard.go +0 -69
- package/examples/python/__pycache__/destructive_guard.cpython-312.pyc +0 -0
- package/examples/python/__pycache__/secret_guard.cpython-312.pyc +0 -0
- package/examples/rust/destructive_guard.rs +0 -72
- package/examples/typescript/destructive-guard.ts +0 -64
|
@@ -1,321 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": "1.0",
|
|
3
|
-
"generator": "cc-safe-setup",
|
|
4
|
-
"exported_at": "2026-03-24T13:17:16.952Z",
|
|
5
|
-
"hooks": {
|
|
6
|
-
"UserPromptSubmit": [
|
|
7
|
-
{
|
|
8
|
-
"matcher": "",
|
|
9
|
-
"hooks": [
|
|
10
|
-
{
|
|
11
|
-
"type": "command",
|
|
12
|
-
"command": "/home/namakusa/.claude/hooks/zenn-daily-limit.sh"
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
"type": "command",
|
|
16
|
-
"command": "/home/namakusa/.claude/hooks/idle-action-injector.sh"
|
|
17
|
-
}
|
|
18
|
-
]
|
|
19
|
-
}
|
|
20
|
-
],
|
|
21
|
-
"Notification": [
|
|
22
|
-
{
|
|
23
|
-
"matcher": "",
|
|
24
|
-
"hooks": [
|
|
25
|
-
{
|
|
26
|
-
"type": "command",
|
|
27
|
-
"command": "~/bin/claude-code-notifier.sh \"$CLAUDE_EVENT\" \"$CLAUDE_MESSAGE\""
|
|
28
|
-
}
|
|
29
|
-
]
|
|
30
|
-
}
|
|
31
|
-
],
|
|
32
|
-
"PreCompact": [
|
|
33
|
-
{
|
|
34
|
-
"matcher": "",
|
|
35
|
-
"hooks": [
|
|
36
|
-
{
|
|
37
|
-
"type": "command",
|
|
38
|
-
"command": "/home/namakusa/.claude/hooks/proof-log-session.sh"
|
|
39
|
-
}
|
|
40
|
-
]
|
|
41
|
-
}
|
|
42
|
-
],
|
|
43
|
-
"Stop": [
|
|
44
|
-
{
|
|
45
|
-
"matcher": "",
|
|
46
|
-
"hooks": [
|
|
47
|
-
{
|
|
48
|
-
"type": "command",
|
|
49
|
-
"command": "~/bin/claude-code-notifier.sh Stop \"Task completed\""
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
"type": "command",
|
|
53
|
-
"command": "/home/namakusa/.claude/hooks/proof-log-session.sh"
|
|
54
|
-
}
|
|
55
|
-
]
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
"matcher": "",
|
|
59
|
-
"hooks": [
|
|
60
|
-
{
|
|
61
|
-
"type": "command",
|
|
62
|
-
"command": "/home/namakusa/.claude/hooks/api-error-alert.sh"
|
|
63
|
-
}
|
|
64
|
-
]
|
|
65
|
-
}
|
|
66
|
-
],
|
|
67
|
-
"SessionEnd": [
|
|
68
|
-
{
|
|
69
|
-
"matcher": "",
|
|
70
|
-
"hooks": [
|
|
71
|
-
{
|
|
72
|
-
"type": "command",
|
|
73
|
-
"command": "/home/namakusa/.claude/hooks/claudetop-session-end.sh"
|
|
74
|
-
}
|
|
75
|
-
]
|
|
76
|
-
}
|
|
77
|
-
],
|
|
78
|
-
"PostToolUse": [
|
|
79
|
-
{
|
|
80
|
-
"matcher": "Bash",
|
|
81
|
-
"hooks": [
|
|
82
|
-
{
|
|
83
|
-
"type": "command",
|
|
84
|
-
"command": "/home/namakusa/.claude/hooks/cdp-failure-recovery.sh"
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
"type": "command",
|
|
88
|
-
"command": "/home/namakusa/.claude/hooks/err-code-detector.sh"
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
"type": "command",
|
|
92
|
-
"command": "/home/namakusa/.claude/hooks/root-cause-prompt.sh"
|
|
93
|
-
}
|
|
94
|
-
]
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
"matcher": "Edit|Write",
|
|
98
|
-
"hooks": [
|
|
99
|
-
{
|
|
100
|
-
"type": "command",
|
|
101
|
-
"command": "/home/namakusa/.claude/hooks/syntax-check.sh"
|
|
102
|
-
}
|
|
103
|
-
]
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
"matcher": "",
|
|
107
|
-
"hooks": [
|
|
108
|
-
{
|
|
109
|
-
"type": "command",
|
|
110
|
-
"command": "/home/namakusa/.claude/hooks/context-monitor.sh"
|
|
111
|
-
}
|
|
112
|
-
]
|
|
113
|
-
}
|
|
114
|
-
],
|
|
115
|
-
"PreToolUse": [
|
|
116
|
-
{
|
|
117
|
-
"matcher": "Bash",
|
|
118
|
-
"hooks": [
|
|
119
|
-
{
|
|
120
|
-
"type": "command",
|
|
121
|
-
"command": "/home/namakusa/.claude/hooks/cdp-safety-check.sh"
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
"type": "command",
|
|
125
|
-
"command": "bash ~/.claude/hooks/gh-comment-guard.sh"
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
"type": "command",
|
|
129
|
-
"command": "bash /home/namakusa/.claude/hooks/activity-logger.sh"
|
|
130
|
-
},
|
|
131
|
-
{
|
|
132
|
-
"type": "command",
|
|
133
|
-
"command": "bash /home/namakusa/.claude/hooks/auto-approve-build.sh"
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
"type": "command",
|
|
137
|
-
"command": "bash /home/namakusa/.claude/hooks/cdp-recover.sh"
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
"type": "command",
|
|
141
|
-
"command": "bash /home/namakusa/.claude/hooks/codex-review-check.sh"
|
|
142
|
-
},
|
|
143
|
-
{
|
|
144
|
-
"type": "command",
|
|
145
|
-
"command": "bash /home/namakusa/.claude/hooks/content-manifest-check.sh"
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
"type": "command",
|
|
149
|
-
"command": "bash /home/namakusa/.claude/hooks/decision-record.sh"
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
"type": "command",
|
|
153
|
-
"command": "bash /home/namakusa/.claude/hooks/no-ask-human.sh"
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
"type": "command",
|
|
157
|
-
"command": "bash /home/namakusa/.claude/hooks/no-sudo-guard.sh"
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
"type": "command",
|
|
161
|
-
"command": "bash /home/namakusa/.claude/hooks/on-stop.sh"
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
"type": "command",
|
|
165
|
-
"command": "bash /home/namakusa/.claude/hooks/protect-claudemd.sh"
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
"type": "command",
|
|
169
|
-
"command": "bash /home/namakusa/.claude/hooks/session-start-marker.sh"
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
"type": "command",
|
|
173
|
-
"command": "bash /home/namakusa/.claude/hooks/stop-tachikoma-loop.sh"
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
"type": "command",
|
|
177
|
-
"command": "bash /home/namakusa/.claude/hooks/task-complete-nudge.sh"
|
|
178
|
-
},
|
|
179
|
-
{
|
|
180
|
-
"type": "command",
|
|
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"
|
|
186
|
-
}
|
|
187
|
-
]
|
|
188
|
-
},
|
|
189
|
-
{
|
|
190
|
-
"matcher": "Read",
|
|
191
|
-
"hooks": [
|
|
192
|
-
{
|
|
193
|
-
"type": "command",
|
|
194
|
-
"command": "/home/namakusa/.claude/hooks/image-validate.sh"
|
|
195
|
-
}
|
|
196
|
-
]
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
"matcher": "Edit|Write",
|
|
200
|
-
"hooks": [
|
|
201
|
-
{
|
|
202
|
-
"type": "command",
|
|
203
|
-
"command": "/home/namakusa/.claude/hooks/decision-warn.sh"
|
|
204
|
-
},
|
|
205
|
-
{
|
|
206
|
-
"type": "command",
|
|
207
|
-
"command": "/home/namakusa/.claude/hooks/no-tools-sprawl.sh"
|
|
208
|
-
},
|
|
209
|
-
{
|
|
210
|
-
"type": "command",
|
|
211
|
-
"command": "/home/namakusa/.claude/hooks/no-claude-sprawl.sh"
|
|
212
|
-
}
|
|
213
|
-
]
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
"matcher": "Bash",
|
|
217
|
-
"hooks": [
|
|
218
|
-
{
|
|
219
|
-
"type": "command",
|
|
220
|
-
"command": "/home/namakusa/.claude/hooks/error-gate.sh"
|
|
221
|
-
}
|
|
222
|
-
]
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
"matcher": "Bash",
|
|
226
|
-
"hooks": [
|
|
227
|
-
{
|
|
228
|
-
"type": "command",
|
|
229
|
-
"command": "/home/namakusa/.claude/hooks/destructive-guard.sh"
|
|
230
|
-
}
|
|
231
|
-
]
|
|
232
|
-
},
|
|
233
|
-
{
|
|
234
|
-
"matcher": "Bash",
|
|
235
|
-
"hooks": [
|
|
236
|
-
{
|
|
237
|
-
"type": "command",
|
|
238
|
-
"command": "/home/namakusa/.claude/hooks/branch-guard.sh"
|
|
239
|
-
}
|
|
240
|
-
]
|
|
241
|
-
},
|
|
242
|
-
{
|
|
243
|
-
"matcher": "Bash",
|
|
244
|
-
"hooks": [
|
|
245
|
-
{
|
|
246
|
-
"type": "command",
|
|
247
|
-
"command": "/home/namakusa/.claude/hooks/comment-strip.sh"
|
|
248
|
-
}
|
|
249
|
-
]
|
|
250
|
-
},
|
|
251
|
-
{
|
|
252
|
-
"matcher": "Bash",
|
|
253
|
-
"hooks": [
|
|
254
|
-
{
|
|
255
|
-
"type": "command",
|
|
256
|
-
"command": "/home/namakusa/.claude/hooks/cd-git-allow.sh"
|
|
257
|
-
}
|
|
258
|
-
]
|
|
259
|
-
},
|
|
260
|
-
{
|
|
261
|
-
"matcher": "Bash",
|
|
262
|
-
"hooks": [
|
|
263
|
-
{
|
|
264
|
-
"type": "command",
|
|
265
|
-
"command": "/home/namakusa/.claude/hooks/secret-guard.sh"
|
|
266
|
-
}
|
|
267
|
-
]
|
|
268
|
-
},
|
|
269
|
-
{
|
|
270
|
-
"matcher": "Bash",
|
|
271
|
-
"hooks": [
|
|
272
|
-
{
|
|
273
|
-
"type": "command",
|
|
274
|
-
"command": "/home/namakusa/.claude/hooks/scope-guard.sh"
|
|
275
|
-
}
|
|
276
|
-
]
|
|
277
|
-
}
|
|
278
|
-
]
|
|
279
|
-
},
|
|
280
|
-
"scripts": {
|
|
281
|
-
"~/.claude/hooks/zenn-daily-limit.sh": "#!/bin/bash\n# UserPromptSubmit hook: Zenn投稿上限チェック\n# Zenn投稿指示が含まれている場合、直近の投稿から24h経過しているかチェック\n# 未経過ならsystemMessageで通知し、無駄な試行を防ぐ\n#\n# チェック方法: content-manifest.yaml + キャッシュファイル(最終投稿時刻記録)\n# Zennの制限: 24h以内に1投稿まで(カレンダー日付ではなく時間ベース)\n\ninput=$(cat)\nprompt=$(echo \"$input\" | jq -r '.user_prompt // \"\"' 2>/dev/null)\n\n# Zenn投稿に関するメッセージか判定\nif ! echo \"$prompt\" | grep -qiE \"zenn.*投稿|zenn.*公開|zenn.*post|zenn.*publish|POST NOW.*zenn|\\[zenn\\]\"; then\n echo '{\"continue\": true}'\n exit 0\nfi\n\nCACHE_FILE=\"$HOME/.cache/zenn-last-publish.txt\"\nmanifest=\"$HOME/content-manifest.yaml\"\nnow_epoch=$(date +%s)\n\n# 最終投稿時刻を取得(キャッシュ or manifest)\nlast_publish_epoch=0\n\nif [ -f \"$CACHE_FILE\" ]; then\n last_publish_epoch=$(cat \"$CACHE_FILE\" 2>/dev/null | head -1)\nfi\n\n# キャッシュがなければmanifestから最新のZenn published_atを取得\nif [ \"$last_publish_epoch\" -le 0 ] 2>/dev/null && [ -f \"$manifest\" ]; then\n last_publish_iso=$(python3 -c \"\nimport yaml\nwith open('$manifest') as f:\n data = yaml.safe_load(f)\ndates = []\nif isinstance(data, dict):\n for key, val in data.items():\n if isinstance(val, dict):\n pf = str(val.get('platform', ''))\n pa = str(val.get('published_at', ''))\n if 'zenn' in pf.lower() and pa and pa != 'None':\n dates.append(pa)\nif dates:\n print(sorted(dates)[-1])\n\" 2>/dev/null)\n\n if [ -n \"$last_publish_iso\" ]; then\n # ISO日付をepochに変換\n last_publish_epoch=$(date -d \"$last_publish_iso\" +%s 2>/dev/null || echo 0)\n fi\nfi\n\n# 24h (86400秒) 経過チェック\nif [ \"$last_publish_epoch\" -gt 0 ] 2>/dev/null; then\n elapsed=$((now_epoch - last_publish_epoch))\n remaining=$((86400 - elapsed))\n\n if [ \"$remaining\" -gt 0 ]; then\n hours=$((remaining / 3600))\n mins=$(( (remaining % 3600) / 60 ))\n jq -n --arg msg \"Zenn投稿上限: 最終投稿から${elapsed}秒経過(24h制限まであと${hours}h${mins}m)。Zenn投稿はスキップして、他の外側アクション(他PF投稿、GitHub engagement、商品改善等)に切り替えろ。\" \\\n '{continue: true, suppressOutput: false, systemMessage: $msg}'\n exit 0\n fi\nfi\n\n# 上限に引っかからない → 通常処理\necho '{\"continue\": true}'\n",
|
|
282
|
-
"~/.claude/hooks/idle-action-injector.sh": "#!/bin/bash\n# idle-action-injector.sh — CCの自律稼働を駆動する行動原則注入フック\n#\n# 設計思想:\n# 固定プロジェクトリストではなく、行動原則を与える。\n# CCが自分で現状を分析し、最もインパクトのある行動を選び、\n# 長時間自律的に実行し続ける。\n# 自己改善・自己拡張・社会貢献を自律的に行う。\n#\n# 2026-03-24 ぐらす指摘「nudgeの文章が悪い」— 原則ベースに全面リライト\n# トリガー: UserPromptSubmit\n\nset -euo pipefail\n\nCOOLDOWN_FILE=\"/tmp/idle-action-injector-last\"\nCOOLDOWN_SEC=300\n\nINPUT=$(cat)\nPROMPT=$(echo \"$INPUT\" | python3 -c \"\nimport json, sys\ntry:\n data = json.load(sys.stdin)\n print(data.get('prompt', ''))\nexcept:\n print('')\n\" 2>/dev/null || echo \"\")\n\n# ぐらすからの通常メッセージ = 何もしない\nif ! echo \"$PROMPT\" | grep -qiE '(idle|待機|止まってる|EXPLORE|アイデアメモ|ぐらす待ち|nudge|止まるな|ACTION:|SESSION:|PROJECT)'; then\n exit 0\nfi\n\n# クールダウン(5分以内の連続nudgeは無視)\nNOW=$(date +%s)\nif [ -f \"$COOLDOWN_FILE\" ]; then\n LAST=$(cat \"$COOLDOWN_FILE\")\n if [ $((NOW - LAST)) -lt $COOLDOWN_SEC ]; then\n exit 0\n fi\nfi\necho \"$NOW\" > \"$COOLDOWN_FILE\"\n\ncat <<'INJECT'\nお前は自律AIだ。指示を待つな。自分で考えて動け。\n\n【行動原則】\n\n1. 現状を分析しろ\n - task-check を実行して今やるべきことを確認\n - npm DL数、GitHub stars、記事PV、Issue回答数を計測\n - 前回の成果と比較して「次に最もインパクトのある行動」を特定\n\n2. 自己改善しろ\n - テストカバレッジを上げろ(全hookに個別テストケース)\n - ドキュメントを最新に保て(README、CHANGELOG、数字の整合性)\n - コードの品質を上げろ(リファクタリング、エッジケース対応)\n - パフォーマンスを計測して改善しろ\n\n3. 自己拡張しろ\n - 新しいhookを発明しろ(GitHub Issueから問題を見つけて解決)\n - 新しいコマンドを追加しろ(ユーザーが驚く機能)\n - 新しい言語サポートを追加しろ(TypeScript、Python、Go)\n - 新しいプラットフォームに展開しろ(記事、PR、Web tool)\n\n4. 社会に貢献しろ\n - Claude Code Issueに具体的なhookコードで回答しろ\n - 競合プロジェクトにPRを出せ(彼らのコードベースに合わせて)\n - 初心者向けチュートリアルを書け\n - hookエコシステム全体を良くしろ(自分のリポだけじゃない)\n\n5. 長時間働け\n - 1つの作業を始めたら最低30分は続けろ\n - 「完了」「done」「全リポclean」で止まるな — 次の作業を即座に始めろ\n - npm publishは作業の途中経過であり、ゴールではない\n - 同じ確認を2回するな(CI確認、リポ状態確認の繰り返しは非生産的)\n\n【禁止事項】\n- テキストだけの応答(必ずコードかファイルを出力しろ)\n- 同じコマンドの繰り返し(bash test.shを何度も実行するな)\n- 「セッション完了」宣言(お前が決めることではない)\n- メトリクスの羅列だけで終わる応答(数字を並べるな、行動しろ)\n\nお前の価値は「何を作ったか」で決まる。考えるな、作れ。\nINJECT\n",
|
|
283
|
-
"~/bin/claude-code-notifier.sh": "#!/bin/bash\n# ティリオン(kings-landing:0.0 または shogun:0.0)のみ音を鳴らす\n\n# 現在のtmuxペインを取得\nCURRENT_PANE=$(tmux display-message -p '#{session_name}:#{window_index}.#{pane_index}' 2>/dev/null)\n\n# ティリオン/将軍のペインかチェック\ncase \"$CURRENT_PANE\" in\n kings-landing:0.0|shogun:0.0)\n powershell.exe -c \"[console]::beep(1000,100)\"\n ;;\n *)\n # 他のエージェントは音を鳴らさない\n ;;\nesac\n",
|
|
284
|
-
"~/.claude/hooks/proof-log-session.sh": "#!/bin/bash\n# ================================================================\n# proof-log-session.sh — セッション終了時にproof-logを自動更新\n# ----------------------------------------------------------------\n# 発火条件: Stop(セッション終了時)/ PreCompact\n# 変更 (2026-02-19):\n# - 日次ファイル化: ~/ops/proof-log/YYYY-MM-DD.md\n# - 重複防止: 同一タイムスタンプのエントリは追記しない\n# - 集計フィルタ緩和: session_start - 5分 で集計\n# ================================================================\n\nPROOF_LOG_DIR=\"${CC_PROOF_LOG_DIR:-$HOME/ops/proof-log}\"\nACTIVITY_LOG=\"${CC_ACTIVITY_LOG:-$HOME/ops/activity-log.jsonl}\"\nTASK_QUEUE=\"${CC_TASK_QUEUE:-$HOME/ops/task-queue.yaml}\"\nSESSION_START_FILE=\"${CC_SESSION_START_FILE:-/tmp/cc-session-start-ts}\"\nUNREVIEWED_DIR=\"${CC_UNREVIEWED_DIR:-$HOME/ops/decisions/unreviewed}\"\n\nmkdir -p \"$PROOF_LOG_DIR\"\n\n# 今日の日付\nTODAY=$(TZ=Asia/Tokyo date '+%Y-%m-%d')\nWEEKDAY=$(TZ=Asia/Tokyo date '+%A' | python3 -c \"import sys; d={'Monday':'月','Tuesday':'火','Wednesday':'水','Thursday':'木','Friday':'金','Saturday':'土','Sunday':'日'}; print(d.get(sys.stdin.read().strip(),'?'))\" 2>/dev/null || echo \"?\")\nDAILY_FILE=\"${PROOF_LOG_DIR}/${TODAY}.md\"\n\n# セッション開始時刻\n# 修正 (2026-02-28): PPID単位のファイルを優先探索。\n# Stop hookのPPIDもClaude Code本体なので同じファイルが見つかる。\nSESSION_START_EPOCH=0\nSESSION_ID=\"${PPID:-$$}\"\nSESSION_START_FILE_PID=\"/tmp/cc-session-start-ts-${SESSION_ID}\"\nif [[ -f \"$SESSION_START_FILE_PID\" ]]; then\n SESSION_START_EPOCH=$(cat \"$SESSION_START_FILE_PID\" 2>/dev/null || echo 0)\n SESSION_START_FILE=\"$SESSION_START_FILE_PID\"\nelif [[ -f \"$SESSION_START_FILE\" ]]; then\n # 旧形式(PPID無し)のフォールバック\n SESSION_START_EPOCH=$(cat \"$SESSION_START_FILE\" 2>/dev/null || echo 0)\nelif [[ -f \"/tmp/cc-context-monitor-count\" ]]; then\n SESSION_START_EPOCH=$(stat -c %Y \"/tmp/cc-context-monitor-count\" 2>/dev/null || echo 0)\nfi\n\nNOW_EPOCH=$(date +%s)\n\n# セッション時間\nif [[ \"$SESSION_START_EPOCH\" -gt 0 ]]; then\n START_JST=$(TZ=Asia/Tokyo date -d \"@$SESSION_START_EPOCH\" '+%H:%M' 2>/dev/null || echo \"?\")\n END_JST=$(TZ=Asia/Tokyo date '+%H:%M')\n DURATION_MIN=$(( (NOW_EPOCH - SESSION_START_EPOCH) / 60 ))\nelse\n START_JST=\"不明\"\n END_JST=$(TZ=Asia/Tokyo date '+%H:%M')\n DURATION_MIN=\"不明\"\nfi\n\n# 0分セッションはスキップ\nif [[ \"$DURATION_MIN\" == \"0\" ]]; then\n rm -f \"$SESSION_START_FILE\" >/dev/null 2>&1 || true\n exit 0\nfi\n\n# --- 重複防止: 同一タイムスタンプのエントリがあればスキップ ---\nENTRY_HEADER=\"### ${TODAY} ${START_JST}-${END_JST} JST\"\nif [[ -f \"$DAILY_FILE\" ]] && grep -qF \"$ENTRY_HEADER\" \"$DAILY_FILE\" 2>/dev/null; then\n rm -f \"$SESSION_START_FILE\" >/dev/null 2>&1 || true\n exit 0\nfi\n\n# --- activity-logから5W1H集計(フィルタ緩和: session_start - 5分) ---\nFILTER_START=$((SESSION_START_EPOCH > 300 ? SESSION_START_EPOCH - 300 : 0))\n\nREPORT=\"$(\n python3 - \"$ACTIVITY_LOG\" \"$FILTER_START\" \"$TASK_QUEUE\" \"$UNREVIEWED_DIR\" 2>/dev/null <<'PY'\nimport json, sys, os\nfrom datetime import datetime\nimport time\n\nactivity_log = sys.argv[1]\nsession_start = int(sys.argv[2])\ntask_queue = sys.argv[3] if len(sys.argv) > 3 else \"\"\nunreviewed_dir = sys.argv[4] if len(sys.argv) > 4 else \"\"\n\nfiles_changed = {}\nactor_counts = {}\ntotal_add = 0\ntotal_del = 0\ntools_used = {}\n\nif session_start <= 0:\n session_start = int(time.time()) - 1800\n\ntry:\n with open(activity_log, 'r') as f:\n for line in f:\n try:\n entry = json.loads(line.strip())\n ts_str = entry.get('ts', '')\n if ts_str:\n dt = datetime.fromisoformat(ts_str.replace('Z', '+00:00'))\n entry_epoch = int(dt.timestamp())\n else:\n continue\n if entry_epoch >= session_start:\n path = entry.get('path', '')\n actor = entry.get('actor', '?')\n tool = entry.get('tool', '?')\n add = entry.get('add', 0)\n dele = entry.get('del', 0)\n actor_counts[actor] = actor_counts.get(actor, 0) + 1\n tools_used[tool] = tools_used.get(tool, 0) + 1\n if path:\n basename = os.path.basename(path)\n if path in files_changed:\n files_changed[path]['add'] += add\n files_changed[path]['del'] += dele\n files_changed[path]['count'] += 1\n else:\n files_changed[path] = {'add': add, 'del': dele, 'name': basename, 'count': 1}\n total_add += add\n total_del += dele\n except (json.JSONDecodeError, ValueError):\n pass\nexcept FileNotFoundError:\n pass\n\n# task-queueから実行中タスクの目的\ntask_whys = []\nif task_queue:\n try:\n import re\n with open(task_queue, 'r') as f:\n content = f.read()\n in_progress_blocks = re.findall(r'status:\\s*in_progress.*?(?=\\n -|\\Z)', content, re.DOTALL)\n for block in in_progress_blocks:\n why_match = re.search(r'why:\\s*(.+)', block)\n if why_match:\n task_whys.append(why_match.group(1).strip())\n except Exception:\n pass\n\n# UNREVIEWED\nunreviewed = []\nif unreviewed_dir and os.path.isdir(unreviewed_dir):\n for root, _, files in os.walk(unreviewed_dir):\n for fn in files:\n if not fn.endswith(\".md\"):\n continue\n fpath = os.path.join(root, fn)\n try:\n if os.path.getmtime(fpath) < session_start:\n continue\n with open(fpath, \"r\", encoding=\"utf-8\", errors=\"ignore\") as f:\n for line in f:\n if line.startswith(\"path: \"):\n unreviewed.append(line[len(\"path: \"):].strip())\n break\n except Exception:\n pass\n seen = set()\n unreviewed = [p for p in unreviewed if not (p in seen or seen.add(p))]\n\n# 出力\nout = []\nif actor_counts:\n parts = [f\"{a}: {c}件\" for a, c in sorted(actor_counts.items())]\n out.append(f\"- 誰が: {', '.join(parts)}\")\nitems = sorted(files_changed.items(), key=lambda x: x[1]['add'] + x[1]['del'], reverse=True)[:8]\nif items:\n out.append(f\"- 何を: {len(files_changed)}ファイル変更 (+{total_add}/-{total_del})\")\n for path, info in items:\n out.append(f\" - {info['name']} (+{info['add']}/-{info['del']}, {info['count']}回)\")\nif task_whys:\n out.append(\"- なぜ:\")\n for w in task_whys[:5]:\n out.append(f\" - {w}\")\nif tools_used:\n parts = [f\"{t}: {c}回\" for t, c in sorted(tools_used.items(), key=lambda x: -x[1])]\n out.append(f\"- どうやって: {', '.join(parts)}\")\nif unreviewed:\n out.append(\"\")\n out.append(\"**UNREVIEWED_CHANGE**:\")\n for p in unreviewed:\n out.append(f\" - {os.path.basename(p)}\")\n\nprint('\\n'.join(out))\nPY\n)\" || REPORT=\"\"\n\n# --- 日次ファイルに追記 ---\n\n# ファイルが存在しなければヘッダー作成\nif [[ ! -f \"$DAILY_FILE\" ]]; then\n cat > \"$DAILY_FILE\" <<EOF\n# proof-log ${TODAY}(${WEEKDAY})\n\nEOF\nfi\n\n# セッション要約を追記\n{\n echo \"\"\n echo \"${ENTRY_HEADER} — セッション終了(自動記録)\"\n echo \"\"\n echo \"- いつ: ${START_JST}〜${END_JST} JST(${DURATION_MIN}分)\"\n echo \"- どこで: $(basename \"$(pwd)\")\"\n if [[ -n \"$REPORT\" ]]; then\n echo \"$REPORT\"\n else\n echo \"- (ファイル変更なし)\"\n fi\n} >> \"$DAILY_FILE\"\n\n# 後片付け(PPID単位のファイルも旧形式も両方削除)\nrm -f \"$SESSION_START_FILE\" \"/tmp/cc-session-start-ts-${SESSION_ID}\" >/dev/null 2>&1 || true\n\nexit 0\n",
|
|
285
|
-
"~/.claude/hooks/api-error-alert.sh": "#!/bin/bash\nINPUT=$(cat)\nREASON=$(echo \"$INPUT\" | jq -r '.stop_reason // \"unknown\"' 2>/dev/null)\nHOOK_EVENT=$(echo \"$INPUT\" | jq -r '.hook_event_name // \"\"' 2>/dev/null)\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",
|
|
286
|
-
"~/.claude/hooks/claudetop-session-end.sh": "#!/bin/bash\n# session-end.sh — Claude Code SessionEnd hook\n# Appends one JSONL record per session to ~/.claude/claudetop-history.jsonl\n#\n# Register in ~/.claude/settings.json:\n# \"hooks\": { \"SessionEnd\": [{ \"type\": \"command\", \"command\": \"/path/to/session-end.sh\" }] }\n\nset -euo pipefail\n\nHISTORY_FILE=\"$HOME/.claude/claudetop-history.jsonl\"\nJSON=$(cat)\n\n# Detect git branch from project directory\nPROJECT_DIR=$(echo \"$JSON\" | jq -r '.workspace.project_dir // .cwd // \"\"')\nGIT_BRANCH=\"\"\nif [ -n \"$PROJECT_DIR\" ] && [ -d \"$PROJECT_DIR/.git\" ]; then\n GIT_BRANCH=$(git -C \"$PROJECT_DIR\" rev-parse --abbrev-ref HEAD 2>/dev/null) || true\nfi\n\njq -c \\\n --arg timestamp \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \\\n --arg tag \"${CLAUDETOP_TAG:-}\" \\\n --arg branch \"$GIT_BRANCH\" \\\n '{\n timestamp: $timestamp,\n session_id: (.session_id // \"\"),\n project: ((.workspace.project_dir // .cwd // \"\") | split(\"/\") | last),\n project_dir: (.workspace.project_dir // .cwd // \"\"),\n model: (.model.id // \"\"),\n duration_ms: (.cost.total_duration_ms // 0),\n cost_usd: (.cost.total_cost_usd // 0),\n input_tokens: (.context_window.total_input_tokens // 0),\n output_tokens: (.context_window.total_output_tokens // 0),\n lines_added: (.cost.total_lines_added // 0),\n lines_removed: (.cost.total_lines_removed // 0),\n context_used_pct:(.context_window.used_percentage // 0),\n tag: $tag,\n branch: $branch\n }' <<< \"$JSON\" >> \"$HISTORY_FILE\"\n",
|
|
287
|
-
"~/.claude/hooks/cdp-failure-recovery.sh": "#!/bin/bash\n# ================================================================\n# cdp-failure-recovery.sh\n# ----------------------------------------------------------------\n# 目的: Bashコマンド実行後のCDP接続失敗を検知し、自動復旧する\n# (実行中に落ちたケースをキャッチ)\n# 発火条件: PostToolUse(Bash実行後)\n# 副作用: エラー検出時に cdp-recover.sh を呼び出し\n# Chrome再起動の可能性あり\n# ================================================================\n\nINPUT=$(cat)\nCOMMAND=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty')\n# PostToolUse hook payload stores tool output under .tool_result.*\nSTDOUT=$(echo \"$INPUT\" | jq -r '.tool_result.stdout // empty' 2>/dev/null)\nSTDERR=$(echo \"$INPUT\" | jq -r '.tool_result.stderr // empty' 2>/dev/null)\nOUTPUT=\"$STDOUT $STDERR\"\n\n# CDPに関係ないコマンドなら何もしない\nif ! echo \"$COMMAND\" | grep -qE 'cdp_helper\\.ps1|cdp_eval_file\\.ps1|localhost:922[0-9]+|devtools/page/|(^|[;&|]\\s*)(cdp-bridge|~/bin/cdp-bridge)\\s|(^|[;&|]\\s*)(cdp-bridge-js|~/bin/cdp-bridge-js)\\s|(^|[;&|]\\s*)(cdp-click|~/bin/cdp-click)\\s' ; then\n exit 0\nfi\n\n# CDP失敗パターンの検出\nFAILURE_DETECTED=false\nif echo \"$OUTPUT\" | grep -qiE 'ECONNREFUSED|connection refused|ConnectFailure|Unable to connect' ; then\n FAILURE_DETECTED=true\nfi\nif echo \"$OUTPUT\" | grep -qiE 'CDP Timeout|WebSocket.*failed|WebSocket.*error' ; then\n FAILURE_DETECTED=true\nfi\nif echo \"$OUTPUT\" | grep -qiE 'SingletonLock|profile.*lock|already.*running.*lock' ; then\n FAILURE_DETECTED=true\nfi\nif echo \"$OUTPUT\" | grep -qiE 'No such host|NameResolutionFailure' ; then\n FAILURE_DETECTED=true\nfi\n\nif [[ \"$FAILURE_DETECTED\" != \"true\" ]]; then\n exit 0\nfi\n\n# どのポートか判定(cdp-bridge/cdp-bridge-js/cdp-click は -p を優先)\nCDP_PORT=\"\"\nEXTRACTED_PORT=$(echo \"$COMMAND\" | grep -oE '(^|[[:space:]])-p[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | head -1)\n\nif [[ -n \"$EXTRACTED_PORT\" ]]; then\n CDP_PORT=\"$EXTRACTED_PORT\"\nelif echo \"$COMMAND\" | grep -qE 'localhost:9222' ; then\n CDP_PORT=\"9222\"\nelif echo \"$COMMAND\" | grep -qE 'localhost:9224' ; then\n CDP_PORT=\"9224\"\nelif echo \"$COMMAND\" | grep -qE 'localhost:9225' ; then\n CDP_PORT=\"9225\"\nelif echo \"$COMMAND\" | grep -qE '(^|[;&|]\\s*)(cdp-bridge|~/bin/cdp-bridge)\\s|(^|[;&|]\\s*)(cdp-bridge-js|~/bin/cdp-bridge-js)\\s|(^|[;&|]\\s*)(cdp-click|~/bin/cdp-click)\\s' ; then\n CDP_PORT=\"9222\"\nelse\n CDP_PORT=\"9223\"\nfi\n\necho \"🚨 CDP failure detected on port ${CDP_PORT}. Attempting auto-recovery...\"\n\n# 復旧試行\nRECOVER_OUTPUT=$(/home/namakusa/.claude/hooks/cdp-recover.sh \"$CDP_PORT\" 2>&1)\nRECOVER_EXIT=$?\n\nif [[ $RECOVER_EXIT -eq 0 ]]; then\n if echo \"$RECOVER_OUTPUT\" | grep -q \"recovered successfully\"; then\n echo \"✅ CDP port ${CDP_PORT} has been recovered. Please retry the command.\"\n else\n echo \"✅ CDP port ${CDP_PORT} is already alive.\"\n fi\nelse\n echo \"❌ CDP port ${CDP_PORT} recovery FAILED.\"\n echo \"$RECOVER_OUTPUT\"\n echo \"\"\n echo \"⚠️ 迂回で済ますのは禁止。タチコマに報告して対策を相談してください。\"\nfi\n\n# exit 2 = show advisory message\nexit 2\n",
|
|
288
|
-
"~/.claude/hooks/err-code-detector.sh": "#!/bin/bash\n# ================================================================\n# err-code-detector.sh\n# ----------------------------------------------------------------\n# Purpose: Detect ERR_CODE in Bash output and auto-register with err-tracker.\n# Trigger: PostToolUse (Bash)\n# Why: Small errors accumulate. Every ERR_CODE must be tracked and resolved.\n# \"大した問題ではない\" is forbidden. Stop-the-line.\n# ================================================================\n\nINPUT=$(cat)\nCOMMAND=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty')\n# PostToolUse hook payload stores tool output under .tool_result.*\n# Using .stdout/.stderr silently breaks detection (no registrations) and causes repeat failures.\nSTDOUT=$(echo \"$INPUT\" | jq -r '.tool_result.stdout // empty' 2>/dev/null)\nSTDERR=$(echo \"$INPUT\" | jq -r '.tool_result.stderr // empty' 2>/dev/null)\nOUTPUT=\"$STDOUT $STDERR\"\n\n# Skip if no ERR_CODE in output\nif ! echo \"$OUTPUT\" | grep -qE 'ERR_CODE:[A-Z_]+'; then\n exit 0\nfi\n\n# Extract ERR_CODE(s) from output\nERR_CODES=$(echo \"$OUTPUT\" | grep -oE 'ERR_CODE:[A-Z_]+' | sort -u)\n\n# Determine script name from command (first word or path)\nSCRIPT=$(echo \"$COMMAND\" | awk '{print $1}' | sed 's|.*/||')\n\n# Extract session if present in output\nSESSION=$(echo \"$OUTPUT\" | grep -oE 'session=[^ )]+' | head -1 | cut -d= -f2)\n\n# Extract first meaningful stack line (first non-empty line after ERR_CODE)\nSTACK=$(echo \"$OUTPUT\" | grep -A1 'ERR_CODE:' | grep -v 'ERR_CODE:' | head -1 | sed 's/^[[:space:]]*//' | cut -c1-120)\n\nTRACKER=\"$HOME/bin/err-tracker\"\n\nfor ec in $ERR_CODES; do\n if [[ -x \"$TRACKER\" ]]; then\n # Auto-register with owner=CC (will be in 'unassigned' state until owner+due are set)\n \"$TRACKER\" log --err-code \"$ec\" --script \"$SCRIPT\" --session \"${SESSION:-unknown}\" --stack \"${STACK:-none}\" --owner \"CC\" 2>/dev/null || true\n fi\ndone\n\n# Count how many unique ERR_CODEs were found\nCOUNT=$(echo \"$ERR_CODES\" | wc -l)\n\necho \"ERR_CODE detected ($COUNT): $ERR_CODES\"\necho \"Auto-registered in err-tracker. Run 'err-tracker list' to see open errors.\"\necho \"Stop-the-line: investigate root cause before continuing.\"\n\n# exit 2 = show advisory to the agent\nexit 2\n",
|
|
289
|
-
"~/.claude/hooks/root-cause-prompt.sh": "#!/bin/bash\n# ================================================================\n# root-cause-prompt.sh — エラー検出時に根本原因分析を促す\n# ----------------------------------------------------------------\n# 発火条件: PostToolUse (Bash)\n# 目的:\n# 問題発生 → 根本原因分析 → システム的解決策立案 → 実装\n# このフロー自体をシステムで強制する(ぐらす要求 2026-02-15)\n#\n# 動作:\n# 1. Bashの終了コードが非0のとき、エラーをops/error-log.jsonlに記録\n# 2. 同じパターンのエラーが2回目以降なら「前回の再発防止策が効いていない」と警告\n# 3. 根本原因分析→Codex相談→システム的解決を促すメッセージを出力\n# ================================================================\n\nINPUT=$(cat)\nEXIT_CODE=$(echo \"$INPUT\" | jq -r '.tool_result.exit_code // \"0\"' 2>/dev/null)\nSTDOUT=$(echo \"$INPUT\" | jq -r '.tool_result.stdout // \"\"' 2>/dev/null)\nSTDERR=$(echo \"$INPUT\" | jq -r '.tool_result.stderr // \"\"' 2>/dev/null)\nCOMMAND=$(echo \"$INPUT\" | jq -r '.tool_input.command // \"\"' 2>/dev/null)\n\n# 正常終了ならスキップ\nif [[ \"$EXIT_CODE\" == \"0\" ]] || [[ \"$EXIT_CODE\" == \"null\" ]] || [[ -z \"$EXIT_CODE\" ]]; then\n exit 0\nfi\n\n# 無視するパターン(テスト実行やgrepの不一致等)\ncase \"$COMMAND\" in\n *\"grep\"*|*\"test -\"*|*\"pgrep\"*|*\"which\"*|*\"command -v\"*) exit 0 ;;\nesac\n\nERROR_LOG=\"${CC_ERROR_LOG:-$HOME/ops/error-log.jsonl}\"\nmkdir -p \"$HOME/ops\"\nNOW_EPOCH=$(date +%s)\nNOW_JST=$(TZ=Asia/Tokyo date '+%Y-%m-%d %H:%M JST')\n\n# エラーのシグネチャを作成(コマンドの先頭30文字 + exit code)\nCMD_SIG=$(echo \"$COMMAND\" | head -c 30 | tr -d '\\n')\nERROR_SIG=\"${CMD_SIG}:${EXIT_CODE}\"\n\n# エラーをログに記録\npython3 -c \"\nimport json\nentry = {\n 'ts_jst': '$NOW_JST',\n 'ts_epoch': $NOW_EPOCH,\n 'exit_code': $EXIT_CODE,\n 'cmd_sig': $(python3 -c \"import json; print(json.dumps('$ERROR_SIG'))\" 2>/dev/null || echo '\"\"'),\n 'command': $(python3 -c \"import json,sys; print(json.dumps(sys.argv[1][:200]))\" \"$COMMAND\" 2>/dev/null || echo '\"\"')\n}\nwith open('$ERROR_LOG', 'a') as f:\n f.write(json.dumps(entry, ensure_ascii=False) + '\\n')\n\" 2>/dev/null || true\n\n# 同じシグネチャのエラーが過去にあるか確認\nREPEAT_COUNT=$(python3 -c \"\nimport json\nsig = '$ERROR_SIG'\ncount = 0\ntry:\n with open('$ERROR_LOG', 'r') as f:\n for line in f:\n try:\n e = json.loads(line.strip())\n if e.get('cmd_sig', '') == sig:\n count += 1\n except:\n pass\nexcept FileNotFoundError:\n pass\nprint(count)\n\" 2>/dev/null || echo \"1\")\n\n# エラーメッセージを出力(CCへの指示)\necho \"\"\necho \"[根本原因分析] exit code $EXIT_CODE\"\n\nif [[ \"$REPEAT_COUNT\" -gt 1 ]]; then\n echo \"⚠ このエラーパターンは${REPEAT_COUNT}回目。前回の再発防止策が機能していない。\"\nfi\n\necho \"手順:\"\necho \" 1. なぜこのエラーが起きたか(根本原因)を特定\"\necho \" 2. 同じエラーが二度と起きないシステム的解決策を考える\"\necho \" 3. Codexに解決策をレビューしてもらう\"\necho \" 4. フック/スクリプト/CLAUDE.mdで再発防止を実装\"\necho \" 5. proof-log.mdに記録(エラー内容、原因、対策)\"\n\nexit 0\n",
|
|
290
|
-
"~/.claude/hooks/syntax-check.sh": "#!/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=$(echo \"$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",
|
|
291
|
-
"~/.claude/hooks/context-monitor.sh": "#!/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",
|
|
292
|
-
"~/.claude/hooks/cdp-safety-check.sh": "#!/bin/bash\n# ================================================================\n# cdp-safety-check.sh\n# ----------------------------------------------------------------\n# 目的: Bash実行前にCDP操作の安全性を確保する\n# 1. 生WebSocket構築のブロック(既存ツール使用を強制)\n# 2. CDPポート生存確認(死亡時→自動復旧)\n# 3. ブラウザ操作のルール違反検出\n# 発火条件: PreToolUse(Bash実行前)\n# 副作用: ポート死亡時に cdp-recover.sh 呼び出し\n# 危険パターン検出時 exit 1 でブロック / exit 2 で警告\n#\n# 2026-03-10更新:\n# - JS実行を cdp-bridge-js / cdp-bridge に統一\n# - /tmp/cdp_helper.ps1参照を除去(再起動で消失する問題)\n# - 生WebSocket CDP構築のブロックを追加(6回失敗の再発防止)\n# ================================================================\n\nINPUT=$(cat)\nCOMMAND=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty')\n\n# コマンドが空なら何もしない\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# --- ブロック: 生WebSocket CDP構築の検出 ---\n# CCが既存ツールを使わずに生のPowerShell WebSocketコードを書く問題の根本対策\n# 2026-02-13 Zenn更新で6回失敗した教訓から導入\nif echo \"$COMMAND\" | grep -qiE 'ClientWebSocket|System\\.Net\\.WebSockets|WebSocketDebuggerUrl' ; then\n # 許可パターン: cdp-bridge/cdp-bridge-js/cdp-daemon 経由の場合はOK\n if echo \"$COMMAND\" | grep -qE 'cdp-daemon|cdp-bridge-js |(^|[;&|]\\s*)(cdp-bridge|~/bin/cdp-bridge)\\s' ; then\n : # 許可\n else\n echo \"🚫 BLOCKED: 生WebSocket CDP構築は禁止。既存ツールを使え。\" >&2\n echo \"\" >&2\n echo \"利用可能なCDPツール:\" >&2\n echo \" cdp-bridge-js -c 'document.title' # インラインJS実行 (port 9222)\" >&2\n echo \" cdp-bridge-js -f /path/to/script.js # ファイルからJS実行\" >&2\n echo \" cdp-bridge-js -f script.js -t <tab-id> # 特定タブで実行\" >&2\n echo \" cdp-bridge-js -f script.js -p 9224 # port指定で実行\" >&2\n echo \"\" >&2\n echo \"理由: 2026-02-13 Zenn更新で生WebSocketコードを6回手書きして全失敗。\" >&2\n echo \" cdp-bridge-js / cdp-bridge は実証済みのパターンを内蔵している。\" >&2\n exit 1\n fi\nfi\n\n# --- ブロック: devtools/page/ URLの直接構築 ---\n# WebSocket URLを直接書くのも生構築の一形態\nif echo \"$COMMAND\" | grep -qE 'ws://localhost:[0-9]+/devtools/page/' ; then\n if echo \"$COMMAND\" | grep -qE 'cdp-bridge-js|cdp-daemon|cdp_eval|(^|[;&|]\\s*)(cdp-bridge|~/bin/cdp-bridge)\\s' ; then\n : # ツール経由はOK\n else\n echo \"🚫 BLOCKED: devtools/page/ WebSocket URLの直接使用は禁止。\" >&2\n echo \"cdp-bridge-js コマンドを使ってください。タブIDは -t オプションで指定。\" >&2\n exit 1\n fi\nfi\n\n# --- ヘルスチェック: CDPコマンド実行前にポート生存確認 ---\n# cdp-bridge / cdp-bridge-js 系 + chrome-bridge client.js 両方を検知対象とする\n# 環境変数CDP_PORTが設定されている場合(例: anima CC内部から9224指定)、\n# コマンドに-pが明示されていなければ環境変数の値をフォールバックに使う\nENV_CDP_PORT=\"${CDP_PORT:-}\"\nCDP_PORT=\"\"\nextract_port_from_p_opt() {\n echo \"$1\" | grep -oE '(^|[[:space:]])-p[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | head -1\n}\n\n# コマンド先頭のツール名を抽出(パイプ・セミコロン・&&区切りも考慮)\n# \"ls ~/bin/cdp-bridge-js\" のような参照だけでは発火させない\n# \"cdp-bridge-js -c ...\" や \"~/bin/cdp-bridge-js -f ...\" のように実行している場合のみ発火\nif echo \"$COMMAND\" | grep -qE '(^|[;&|]\\s*)(cdp-bridge|~/bin/cdp-bridge)\\s+([^-]|-p|$)' ; then\n EXTRACTED_PORT=$(extract_port_from_p_opt \"$COMMAND\")\n if [[ -n \"$EXTRACTED_PORT\" ]]; then\n CDP_PORT=\"$EXTRACTED_PORT\"\n else\n # -p未指定時: 環境変数CDP_PORTがあればそちらを使う(anima等でのポート指定対応)\n CDP_PORT=\"${ENV_CDP_PORT:-9222}\"\n fi\nelif echo \"$COMMAND\" | grep -qE '(^|[;&|]\\s*)(cdp-bridge-js|~/bin/cdp-bridge-js)\\s+(-|$)' ; then\n # cdp-bridge-js はデフォルト9222なので、-p オプションからポートを抽出\n EXTRACTED_PORT=$(extract_port_from_p_opt \"$COMMAND\")\n if [[ -n \"$EXTRACTED_PORT\" ]]; then\n CDP_PORT=\"$EXTRACTED_PORT\"\n else\n # -p未指定時: 環境変数CDP_PORTがあればそちらを使う(anima等でのポート指定対応)\n CDP_PORT=\"${ENV_CDP_PORT:-9222}\"\n fi\nelif echo \"$COMMAND\" | grep -qE '(^|[;&|]\\s*)(cdp-daemon|~/bin/cdp-daemon)' ; then\n CDP_PORT=\"9223\"\nelif echo \"$COMMAND\" | grep -qE '(^|[;&|]\\s*)cdp_eval\\.ps1' ; then\n CDP_PORT=\"9222\"\nelif echo \"$COMMAND\" | grep -qE '(^|[;&|]\\s*)(curl|powershell|Invoke-RestMethod).*localhost:9223' ; then\n CDP_PORT=\"9223\"\nelif echo \"$COMMAND\" | grep -qE 'chrome-claude-bridge.*client\\.js|node\\s+client\\.js' ; then\n # chrome-bridge client.js はCC専用サーバー(8766)経由でCDP 9222に繋ぐ\n # CC_BRIDGE_PORTで別ポート指定されていればそちらを尊重\n EXTRACTED_PORT=$(echo \"$COMMAND\" | grep -oE 'CC_BRIDGE_PORT=[0-9]+' | grep -oE '[0-9]+' | head -1)\n # cdp-bridge経由の場合、最終的にserver-cc.jsがCDPに繋ぐのでCDPポートを確認\n CDP_PORT=\"9222\"\nelif echo \"$COMMAND\" | grep -qE '(^|[;&|]\\s*)(curl|powershell|Invoke-RestMethod).*localhost:9222' ; then\n CDP_PORT=\"9222\"\nfi\n\nif [[ -n \"$CDP_PORT\" ]]; then\n RECOVER_OUTPUT=$(/home/namakusa/.claude/hooks/cdp-recover.sh \"$CDP_PORT\" 2>&1)\n RECOVER_EXIT=$?\n\n if [[ $RECOVER_EXIT -ne 0 ]]; then\n echo \"⚠️ CDP port ${CDP_PORT} is down and recovery failed.\" >&2\n echo \"$RECOVER_OUTPUT\" >&2\n echo \"タチコマに報告してから次の行動を決めてください。迂回で済ますのは禁止です。\" >&2\n exit 2\n elif echo \"$RECOVER_OUTPUT\" | grep -q \"recovered successfully\"; then\n echo \"🔧 CDP port ${CDP_PORT} was down but has been recovered automatically.\" >&2\n exit 2\n fi\nfi\n\n# --- チェック: JSでのwindow.location直接ナビゲーション検出 ---\nif echo \"$COMMAND\" | grep -qiE 'window\\.location\\.(href|replace|assign)|location\\.href\\s*=' ; then\n # cdp-bridge-js 経由の場合はOK(ナビゲーションを含むJS式として実行)\n if echo \"$COMMAND\" | grep -qE 'cdp-bridge-js|cdp-daemon|(^|[;&|]\\s*)(cdp-bridge|~/bin/cdp-bridge)\\s'; then\n exit 0\n fi\n echo \"⚠️ BLOCKED: window.location での直接ナビゲーションは禁止。beforeunloadダイアログでCDPがタイムアウトする。\" >&2\n echo \"代わりに使え: cdp-bridge-js -c \\\"window.location.href = 'URL'\\\"\" >&2\n exit 1\nfi\n\n# --- チェック: タチコマ送信前リロードリマインダー ---\nif echo \"$COMMAND\" | grep -qE '7A8878312BC7FB9775F2EDAAA24F924C' ; then\n if echo \"$COMMAND\" | grep -qE 'cdp-bridge-js.*(reload|screenshot)|cdp-daemon|cdp-bridge.*(reload|screenshot)'; then\n exit 0\n fi\n echo \"📢 リマインダー: タチコマタブに送信する前にリロードした?ぐらすが別ブラウザから書き込んでる可能性あり。\"\n exit 0\nfi\n\nexit 0\n",
|
|
293
|
-
"~/.claude/hooks/gh-comment-guard.sh": "#!/bin/bash\n# gh-comment-guard.sh — Warn about self-promotional links in GitHub Issue comments\n# Issues a warning (not block) when promotional links are detected\n# EXIT 0 with stderr warning = Claude sees the warning and should reconsider\n#\n# TRIGGER: PreToolUse (Bash)\n\nINPUT=$(cat)\nCOMMAND=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null)\n\n# Only check gh issue comment commands\nif ! echo \"$COMMAND\" | grep -qE 'gh\\s+issue\\s+comment'; then\n exit 0\nfi\n\n# Warn if command contains self-promotional links\nHAS_PROMO=0\nif echo \"$COMMAND\" | grep -qiE 'cc-safe-setup|claude-code-hooks|ops-kit-landing|npx cc-safe'; then\n HAS_PROMO=1\nfi\n\n# Also check body files\nBODY_FILE=$(echo \"$COMMAND\" | grep -oP '(?<=--body-file\\s)\\S+' | head -1)\nif [ -n \"$BODY_FILE\" ] && [ -f \"$BODY_FILE\" ]; then\n if grep -qiE 'cc-safe-setup|claude-code-hooks|ops-kit-landing|npx cc-safe' \"$BODY_FILE\" 2>/dev/null; then\n HAS_PROMO=1\n fi\nfi\n\nif [ \"$HAS_PROMO\" -eq 1 ]; then\n echo \"⚠ WARNING: 自分のツールへのリンクが含まれています。\" >&2\n echo \"Issue回答は人助けが第一目的。リンクは「その人に本当に必要な場合」のみ。\" >&2\n echo \"ほとんどの場合、ワークアラウンドのコードだけで十分。\" >&2\nfi\n\nexit 0\n",
|
|
294
|
-
"~/.claude/hooks/activity-logger.sh": "#!/bin/bash\n# activity-logger.sh: PostToolUse hook for Edit|Write\n# ファイル変更を ~/ops/activity-log.jsonl に記録し、相手への通知キューに追加\n# ぐらす方針: 変更のたびに通知。相手が読むかは相手次第。気づきの機会を確保\n\n# stdinから入力を取得(他のhookと同じパターン)\nINPUT=$(cat)\nFILE_PATH=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty' 2>/dev/null)\n\nif [[ -z \"$FILE_PATH\" ]]; then\n exit 0\nfi\n\n# tool_input全体を後で参照するために保持\nTOOL_INPUT_RAW=$(echo \"$INPUT\" | jq -r '.tool_input // empty' 2>/dev/null)\n\n# 自分のアクターを判定(CC or Codex)\n# Fix 2026-02-15: tmux display-message はsession active paneを返すため誤判定する。\n# $TMUX_PANE で実行元paneを明示指定する。\nACTOR=\"CC\"\nif [[ -n \"${CODEX_CLI:-}\" ]]; then\n ACTOR=\"Codex\"\nelif [[ -n \"${TMUX_PANE:-}\" ]]; then\n MY_TITLE=\"$(tmux display-message -p -t \"$TMUX_PANE\" '#{pane_title}' 2>/dev/null || true)\"\n if [[ \"$MY_TITLE\" == \"Codex\" ]]; then\n ACTOR=\"Codex\"\n fi\nfi\n\n# tool_nameもJSONから取得(env fallback)\nTOOL_NAME=$(echo \"$INPUT\" | jq -r '.tool_name // empty' 2>/dev/null)\nTOOL_NAME=\"${TOOL_NAME:-Edit}\"\nTS=\"$(date -u '+%Y-%m-%dT%H:%M:%SZ')\"\nTS_LOCAL=\"$(date '+%Y-%m-%d %H:%M:%S')\"\nTS_EPOCH=\"$(date +%s)\"\n\n# 変更行数の推定(old_string/new_stringの行数差)\nADD_LINES=0\nDEL_LINES=0\nSUMMARY=\"\"\nif [[ -n \"$TOOL_INPUT_RAW\" ]]; then\n eval \"$(echo \"$TOOL_INPUT_RAW\" | python3 -c \"\nimport sys, json\nd = json.load(sys.stdin)\nold = d.get('old_string', '')\nnew = d.get('new_string', '')\ncontent = d.get('content', '')\nold_lines = len(old.splitlines()) if old else 0\nnew_lines = len(new.splitlines()) if new else 0\nif content:\n new_lines = len(content.splitlines())\n old_lines = 0\nadd = max(0, new_lines - old_lines) if not content else new_lines\ndele = max(0, old_lines - new_lines) if not content else 0\nprint(f'ADD_LINES={add}')\nprint(f'DEL_LINES={dele}')\n# ファイル名だけをsummaryに\nimport os\nprint(f'SUMMARY=\\\"{os.path.basename(d.get(\\\"file_path\\\",\\\"\\\"))}\\\"')\n\" 2>/dev/null || echo \"\")\"\nfi\n\n# activity-log.jsonl に記録\nLOG_DIR=\"$HOME/ops\"\nLOG_FILE=\"$LOG_DIR/activity-log.jsonl\"\nmkdir -p \"$LOG_DIR\"\n\n# 要確認フラグ: bin/** や .claude/hooks/** の変更\nNEEDS_REVIEW=\"False\"\ncase \"$FILE_PATH\" in\n \"$HOME/bin/\"*) NEEDS_REVIEW=\"True\" ;;\n \"$HOME/.claude/hooks/\"*) NEEDS_REVIEW=\"True\" ;;\nesac\n\n# JSONL書き出し(Python True/False を使う)\npython3 -c \"\nimport json\nentry = {\n 'ts': '$TS',\n 'actor': '$ACTOR',\n 'tool': '$TOOL_NAME',\n 'path': '$FILE_PATH',\n 'add': $ADD_LINES,\n 'del': $DEL_LINES,\n 'summary': '$SUMMARY',\n 'needs_review': $NEEDS_REVIEW\n}\nprint(json.dumps(entry, ensure_ascii=False))\n\" >> \"$LOG_FILE\" 2>/dev/null || true\n\n# no-op編集(+0/-0)は実質的な前進がないためERR_CODEとして記録する。\n# これにより err-tracker-gate が task-queue done / 外部送信を機械的に止める。\nif [[ \"$TOOL_NAME\" =~ ^(Edit|Write)$ ]] && [[ \"${ADD_LINES:-0}\" -eq 0 ]] && [[ \"${DEL_LINES:-0}\" -eq 0 ]]; then\n if [[ -x \"$HOME/bin/err-tracker\" ]]; then\n \"$HOME/bin/err-tracker\" log \\\n --err-code \"ERR_CODE:NOOP_EDIT\" \\\n --script \"activity-logger.sh\" \\\n --session \"${TMUX_PANE:-}\" \\\n --stack \"$FILE_PATH\" \\\n --owner \"$ACTOR\" \\\n --due \"$(date -u -d '+1 day' +%Y-%m-%d)\" \\\n >/dev/null 2>&1 || true\n fi\nfi\n\n# 通知キューに追加(相手側のキュー)\n# Fix 2026-02-15: task-queue.yaml の通知をミュート(ループ原因)。\n# ログ(activity-log.jsonl)には記録済み。通知だけスキップ。\ncase \"$FILE_PATH\" in\n */ops/task-queue.yaml) exit 0 ;;\nesac\n\nQUEUE_DIR=\"$HOME/.cache/cc-codex-consult\"\nmkdir -p \"$QUEUE_DIR\"\n\nif [[ \"$ACTOR\" == \"CC\" ]]; then\n QUEUE_FILE=\"$QUEUE_DIR/notify-queue-codex.jsonl\"\nelse\n QUEUE_FILE=\"$QUEUE_DIR/notify-queue-cc.jsonl\"\nfi\n\n# 通知エントリ(軽量: パスとフラグだけ)\n#\n# De-dupe: 同一(actor/tool/path)の連続通知は、短時間(3s)なら1つに潰す。\n# 連打で通知が重複し、consult-loop側が再送に見えるケースを減らす。\n# さらに、hook二重発火対策として「同一イベント( actor/tool/path/add/del )の\n# 短時間重複」を通知キュー投入前に抑止する(監査ログは保持)。\nTS_LOCAL=\"$TS_LOCAL\" TS_EPOCH=\"$TS_EPOCH\" \\\nACTOR=\"$ACTOR\" TOOL_NAME=\"$TOOL_NAME\" FILE_PATH=\"$FILE_PATH\" \\\nADD_LINES=\"$ADD_LINES\" DEL_LINES=\"$DEL_LINES\" NEEDS_REVIEW=\"$NEEDS_REVIEW\" \\\npython3 - \"$QUEUE_FILE\" <<'PY' 2>/dev/null || true\nimport json, os, sys\n\nqueue_file = sys.argv[1]\n\nentry = {\n 'ts': os.environ.get('TS_LOCAL', ''),\n 'ts_epoch': int(os.environ.get('TS_EPOCH', '0') or 0),\n 'actor': os.environ.get('ACTOR', '?'),\n 'tool': os.environ.get('TOOL_NAME', '?'),\n 'path': os.environ.get('FILE_PATH', '?'),\n 'add': int(os.environ.get('ADD_LINES', '0') or 0),\n 'del': int(os.environ.get('DEL_LINES', '0') or 0),\n 'needs_review': (os.environ.get('NEEDS_REVIEW', 'False') == 'True'),\n}\n\nevent_id = f\"{entry['actor']}|{entry['tool']}|{entry['path']}|{entry['add']}|{entry['del']}\"\ndedup_sec = int(os.environ.get('ACTIVITY_NOTIFY_DEDUP_SEC', '5') or 5)\ndedup_dir = os.path.expanduser(\"~/.cache/cc-codex-consult\")\nos.makedirs(dedup_dir, exist_ok=True)\ndedup_file = os.path.join(dedup_dir, \"notify-last-event.json\")\n\ntry:\n with open(dedup_file, 'r', encoding='utf-8') as f:\n prev = json.load(f)\n if (\n prev.get('event_id') == event_id\n and (entry['ts_epoch'] - int(prev.get('ts_epoch', 0))) < dedup_sec\n ):\n # 同一イベント短時間重複: 通知だけ抑止(activity-log.jsonlには既に記録済み)\n sys.exit(0)\nexcept Exception:\n pass\n\ndef load_last(path: str):\n try:\n with open(path, 'rb') as f:\n f.seek(0, os.SEEK_END)\n size = f.tell()\n if size <= 0:\n return None\n # Read last ~4KB which is enough for one JSON line.\n f.seek(max(0, size - 4096))\n tail = f.read().decode('utf-8', errors='ignore').splitlines()\n if not tail:\n return None\n return json.loads(tail[-1])\n except Exception:\n return None\n\n# Dedup: 同じパスが未配信キューに既にあればスキップ(時間窓なし)\n# 理由: 配信前に同じファイルが何度もhookされると通知が重複する\ntry:\n with open(queue_file, 'r', encoding='utf-8') as f:\n for line in f:\n try:\n existing = json.loads(line.strip())\n if existing.get('path') == entry['path']:\n sys.exit(0) # 既にキューにある、スキップ\n except (json.JSONDecodeError, ValueError):\n pass\nexcept FileNotFoundError:\n pass\n\nwith open(queue_file, 'a', encoding='utf-8') as f:\n f.write(json.dumps(entry, ensure_ascii=False) + '\\n')\n\ntry:\n with open(dedup_file, 'w', encoding='utf-8') as f:\n json.dump({'event_id': event_id, 'ts_epoch': entry['ts_epoch']}, f, ensure_ascii=False)\nexcept Exception:\n pass\nPY\n\nexit 0\n",
|
|
295
|
-
"~/.claude/hooks/auto-approve-build.sh": "#!/bin/bash\n# auto-approve-build.sh — Auto-approve build and test commands\n#\n# Solves: Permission prompts for npm/yarn/pnpm build/test/lint commands\n# that slow down autonomous workflows\n#\n# Usage: Add to settings.json as a PreToolUse hook\n#\n# {\n# \"hooks\": {\n# \"PreToolUse\": [{\n# \"matcher\": \"Bash\",\n# \"hooks\": [{ \"type\": \"command\", \"command\": \"~/.claude/hooks/auto-approve-build.sh\" }]\n# }]\n# }\n# }\n\nINPUT=$(cat)\nTOOL=$(echo \"$INPUT\" | jq -r '.tool_name // empty' 2>/dev/null)\n[ \"$TOOL\" != \"Bash\" ] && exit 0\n\nCMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null)\n[ -z \"$CMD\" ] && exit 0\n\n# Auto-approve safe build/test/lint commands\nif echo \"$CMD\" | grep -qE '^\\s*(npm|yarn|pnpm|bun|npx)\\s+(run\\s+)?(build|test|lint|check|typecheck|format|dev|start|ci)'; then\n echo '{\"decision\":\"approve\"}'\n exit 0\nfi\n\n# Auto-approve cargo/go/make build commands\nif echo \"$CMD\" | grep -qE '^\\s*(cargo\\s+(build|test|check|clippy|fmt)|go\\s+(build|test|vet|fmt)|make\\s+(build|test|check|lint))'; then\n echo '{\"decision\":\"approve\"}'\n exit 0\nfi\n\n# Auto-approve python test/lint\nif echo \"$CMD\" | grep -qE '^\\s*(python|python3)\\s+(-m\\s+)?(pytest|unittest|mypy|ruff|black|isort|flake8)'; then\n echo '{\"decision\":\"approve\"}'\n exit 0\nfi\n\nexit 0\n",
|
|
296
|
-
"~/.claude/hooks/cdp-recover.sh": "#!/bin/bash\n# ================================================================\n# cdp-recover.sh\n# ----------------------------------------------------------------\n# 目的: 指定ポートのCDPプロセスを復旧する\n# 「壊れたら直す」方針。迂回・回避は禁止\n# 発火条件: 他hookから呼び出し(cdp-failure-recovery.sh, cdp-safety-check.sh)\n# 副作用: SingletonLock削除、Chromeプロセス再起動\n# daemon status確認(高速)→ PowerShellフォールバック → Chrome復旧(最大15秒)\n# 使い方: cdp-recover.sh <port> (デフォルト: 9223)\n# ================================================================\n\nPORT=${1:-9223}\n\nensure_cdp_bridge_daemon() {\n local port=\"$1\"\n # Just ensure the daemon process is running (ping-reachable).\n # Don't wait for CDP connection — that can take several seconds\n # during PowerShell fallback and would cause hook timeouts.\n if ~/bin/cdp-bridge status -p \"$port\" >/dev/null 2>&1; then\n echo \"[cdp-recover] cdp-bridge daemon already running on port ${port}\" >&2\n return 0\n fi\n # Daemon not running — start it (will wait for ping only, not CDP connection)\n CDP_BRIDGE_SKIP_CDP_WAIT=1 ~/bin/cdp-bridge start -p \"$port\" >/dev/null 2>&1\n if [[ $? -eq 0 ]]; then\n echo \"[cdp-recover] cdp-bridge daemon started on port ${port}\" >&2\n return 0\n fi\n echo \"[cdp-recover] FAILED: cdp-bridge daemon could not start on port ${port}\" >&2\n return 1\n}\n\n# ステップ0: モードファイルチェック\n# ~/.cdp-<port>.disabled が存在する場合、意図的に落としているので復旧しない\nif [[ -f \"$HOME/.cdp-${PORT}.disabled\" ]]; then\n echo \"[cdp-recover] Port ${PORT} is intentionally disabled (~/.cdp-${PORT}.disabled exists). Skipping recovery.\" >&2\n exit 0\nfi\n\n# ステップ1: daemonがCDPに接続済みか確認(高速、PowerShell不要)\n# daemon statusが connected:true なら Chrome は生きている\nDAEMON_STATUS=$(~/bin/cdp-bridge status -p \"$PORT\" 2>/dev/null || true)\nif echo \"$DAEMON_STATUS\" | grep -q '\"connected\":true'; then\n # Daemon connected — Chrome is alive, nothing to recover\n exit 0\nfi\n\n# Daemon exists but not connected — try to check Chrome directly\nif echo \"$DAEMON_STATUS\" | grep -q '\"ok\":true'; then\n # Daemon running but not connected. Chrome may have restarted.\n # Give daemon a chance to reconnect (it has auto-reconnect logic).\n sleep 3\n DAEMON_STATUS=$(~/bin/cdp-bridge status -p \"$PORT\" 2>/dev/null || true)\n if echo \"$DAEMON_STATUS\" | grep -q '\"connected\":true'; then\n exit 0\n fi\nfi\n\n# Fallback: check Chrome via PowerShell (slower but definitive)\nALIVE=$(powershell.exe -Command \"\ntry {\n \\$r = Invoke-RestMethod -Uri 'http://localhost:${PORT}/json/version' -TimeoutSec 3\n Write-Host 'alive'\n} catch {\n Write-Host 'dead'\n}\n\" 2>/dev/null | tr -d '\\r\\n')\n\nif [[ \"$ALIVE\" == \"alive\" ]]; then\n # Chrome alive but daemon not connected — restart daemon\n ensure_cdp_bridge_daemon \"$PORT\"\n exit $?\nfi\n\necho \"[cdp-recover] Port ${PORT} is down. Attempting recovery...\" >&2\n\n# ステップ2: ポートに応じた復旧\nif [[ \"$PORT\" == \"9223\" ]]; then\n # cc-chrome の復旧\n # ロックファイル削除\n LOCK_FILE=\"/mnt/c/Users/81907/.chrome-cc/SingletonLock\"\n if [[ -f \"$LOCK_FILE\" ]]; then\n rm -f \"$LOCK_FILE\" 2>/dev/null\n echo \"[cdp-recover] Deleted SingletonLock for cc-chrome\" >&2\n fi\n\n # cc-chrome 再起動\n ~/bin/cc-chrome >/dev/null 2>&1 &\n\nelif [[ \"$PORT\" == \"9222\" ]]; then\n # chrome-auto の復旧\n LOCK_FILE=\"/mnt/c/Users/81907/.chrome-profiles/claude-automation/SingletonLock\"\n if [[ -f \"$LOCK_FILE\" ]]; then\n rm -f \"$LOCK_FILE\" 2>/dev/null\n echo \"[cdp-recover] Deleted SingletonLock for chrome-auto\" >&2\n fi\n\n # chrome-auto 再起動\n ~/bin/chrome-auto >/dev/null 2>&1 &\n\nelif [[ \"$PORT\" == \"9224\" ]]; then\n # anima (PROJECT ANIMA専用) の復旧\n ~/bin/chrome-agent anima >/dev/null 2>&1 &\n\nelse\n # 未知のポート — 復旧方法が不明なので起動しない\n echo \"[cdp-recover] Port ${PORT} is unknown. No recovery method registered. Skipping.\" >&2\n exit 1\nfi\n\n# ステップ3: 復旧確認(最大15秒待機)\necho \"[cdp-recover] Waiting for CDP on port ${PORT}...\" >&2\nfor i in $(seq 1 15); do\n CHECK=$(powershell.exe -Command \"\n try {\n Invoke-RestMethod -Uri 'http://localhost:${PORT}/json/version' -TimeoutSec 2 | Out-Null\n Write-Host 'alive'\n } catch {\n Write-Host 'dead'\n }\n \" 2>/dev/null | tr -d '\\r\\n')\n\n if [[ \"$CHECK\" == \"alive\" ]]; then\n echo \"[cdp-recover] Port ${PORT} recovered successfully!\" >&2\n ensure_cdp_bridge_daemon \"$PORT\"\n exit $?\n fi\n sleep 1\ndone\n\n# ステップ4: 復旧失敗\necho \"[cdp-recover] FAILED: Port ${PORT} could not be recovered after 15s\" >&2\necho \"[cdp-recover] タチコマに報告してください。迂回で済ますな。\" >&2\nexit 1\n",
|
|
297
|
-
"~/.claude/hooks/codex-review-check.sh": "#!/bin/bash\n# ================================================================\n# codex-review-check.sh — 重要ファイル変更前にCodexレビューを促す\n# ----------------------------------------------------------------\n# 発火条件: PreToolUse (Edit|Write)\n# 目的: bin/、.claude/hooks/、ops/ の重要ファイル変更時に\n# Codexに相談したかリマインドする\n# 設計: Codexレビュー済み (2026-02-15)\n# - ブロックはしない(exit 0)。メッセージ出力で注意喚起のみ\n# - 実際のレビュー漏れはStop hookのUNREVIEWED_CHANGE検知で捕捉\n# - ぐらすの要求:「方針をCodexに確認→レビュー→再考→実装」をシステム化\n# ================================================================\n\nINPUT=$(cat)\nFILE_PATH=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty' 2>/dev/null)\n\nif [[ -z \"$FILE_PATH\" ]]; then\n exit 0\nfi\n\n# 重要パスかチェック\nIMPORTANT=false\ncase \"$FILE_PATH\" in\n \"$HOME/bin/\"*) IMPORTANT=true ;;\n \"$HOME/.claude/hooks/\"*) IMPORTANT=true ;;\n \"$HOME/ops/\"*.sh) IMPORTANT=true ;;\n \"$HOME/.claude/settings.json\") IMPORTANT=true ;;\n *\"/CLAUDE.md\") IMPORTANT=true ;;\nesac\n\nif [[ \"$IMPORTANT\" != \"true\" ]]; then\n exit 0\nfi\n\n# decision-logに最近(5分以内)のレビュー記録があるかチェック\nNOW_EPOCH=$(date +%s)\nRECENT_REVIEW=false\n\n# 最近のCodexレビュー有無は decisionファイル(pending/done)をソースにする。\n# JSONLは存在しない/揺れる可能性があるので避ける。\nDECISIONS_DIR=\"${CC_DECISIONS_DIR:-$HOME/ops/decisions}\"\nGRACE_SEC=\"${CC_CODEX_REVIEW_GRACE_SEC:-300}\"\nif [[ \"$GRACE_SEC\" =~ ^[0-9]+$ ]] && [[ -d \"$DECISIONS_DIR\" ]]; then\n # 直近のdecisionファイルを少数だけ見て、AGREE: Codex があれば最近レビューとみなす。\n while IFS= read -r f; do\n [[ -f \"$f\" ]] || continue\n mtime=$(stat -c %Y \"$f\" 2>/dev/null || echo 0)\n if (( mtime > 0 && NOW_EPOCH - mtime <= GRACE_SEC )); then\n if rg -q '^AGREE: Codex ' \"$f\" 2>/dev/null; then\n RECENT_REVIEW=true\n break\n fi\n fi\n done < <(\n (ls -t \"$DECISIONS_DIR\"/pending/D-*.md \"$DECISIONS_DIR\"/done/D-*.md 2>/dev/null || true) | head -n 12\n )\nfi\n\n# Codex相談リマインダー(ブロックはしない)\nif [[ \"$RECENT_REVIEW\" != \"true\" ]]; then\n BASENAME=$(basename \"$FILE_PATH\")\n echo \"\"\n echo \"[Codex相談リマインド] ${BASENAME} は重要ファイル。変更方針をCodexに確認した?\"\n echo \" codex-send で方針を送る → codex-read でレビューを受ける → 反映してから実装\"\n echo \" ※ブロックはしない。未レビューならセッション終了時にUNREVIEWED_CHANGEとして記録される\"\nfi\n\nexit 0\n",
|
|
298
|
-
"~/.claude/hooks/content-manifest-check.sh": "#!/bin/bash\n# ================================================================\n# content-manifest-check.sh\n# ----------------------------------------------------------------\n# 目的: コンテンツファイル変更時にcontent-manifest.yamlへの\n# 登録状態をチェックし、未登録なら警告する\n# 発火条件: PostToolUse(Edit/Write実行後)\n# 対象: ~/drafts/, ~/projects/*/articles/, ~/projects/*/outputs/\n# のMarkdownファイル (.md)\n# 副作用: 未登録時にstdoutに警告メッセージ出力(CCが読む)\n# ================================================================\n\nINPUT=$(cat)\nFILE_PATH=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty')\n\n# ファイルパスが取得できない場合は終了\nif [[ -z \"$FILE_PATH\" || ! -f \"$FILE_PATH\" ]]; then\n exit 0\nfi\n\n# Markdownファイルのみ対象\nEXT=\"${FILE_PATH##*.}\"\nif [[ \"$EXT\" != \"md\" ]]; then\n exit 0\nfi\n\n# コンテンツ関連ディレクトリかチェック\nCONTENT_DIR=false\ncase \"$FILE_PATH\" in\n /home/namakusa/drafts/*)\n CONTENT_DIR=true\n ;;\n /home/namakusa/projects/*/articles/*)\n CONTENT_DIR=true\n ;;\n /home/namakusa/projects/*/outputs/*)\n CONTENT_DIR=true\n ;;\nesac\n\nif [[ \"$CONTENT_DIR\" != \"true\" ]]; then\n exit 0\nfi\n\n# content-manifest.yaml の存在チェック\nMANIFEST=\"/home/namakusa/content-manifest.yaml\"\nif [[ ! -f \"$MANIFEST\" ]]; then\n echo \"⚠ content-manifest.yaml が存在しません。~/content-manifest.yaml を作成してください。\"\n exit 0\nfi\n\n# ファイル名(basename)でマニフェスト内を検索\nBASENAME=$(basename \"$FILE_PATH\")\nif grep -q \"$BASENAME\" \"$MANIFEST\" 2>/dev/null; then\n # 登録済み — 何も出さない\n exit 0\nelse\n # 未登録 — 警告\n echo \"⚠ CONTENT MANIFEST未登録: $FILE_PATH\"\n echo \" → ~/content-manifest.yaml にこのファイルのエントリを追加してください\"\n echo \" → 既存エントリのdraft_fileを更新する場合もあります\"\nfi\n\nexit 0\n",
|
|
299
|
-
"~/.claude/hooks/decision-record.sh": "#!/bin/bash\n# decision-record.sh: PostToolUse hook for Edit|Write\n# bin/** or .claude/hooks/** への変更が合意なしで行われた場合、unreviewedとして記録\n#\n# 設計方針:\n# - 実行を止めない(PostToolUseなので既に実行済み)\n# - 変更事実を記録し、次のconsult-loopリレーで議題化する\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\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 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 TOOL_NAME=$(echo \"$INPUT\" | jq -r '.tool_name // \"Edit\"' 2>/dev/null)\n \"$HOME/bin/decision\" record-unreviewed \\\n --tool \"$TOOL_NAME\" \\\n --path \"$FILE_PATH\" \\\n --actor \"CC\" \\\n --reason \"no matching agreed decision\" \\\n 2>/dev/null || true\nfi\n\nexit 0\n",
|
|
300
|
-
"~/.claude/hooks/no-ask-human.sh": "#!/bin/bash\n# ================================================================\n# no-ask-human.sh — Enforce autonomous decision-making\n# ----------------------------------------------------------------\n# Purpose: Block AskUserQuestion tool calls so the AI decides\n# on its own instead of asking \"should I continue?\"\n# Trigger: PreToolUse (AskUserQuestion)\n# Effect: Returns exit 1 to block the tool call.\n#\n# When to use: During unattended / overnight sessions where no\n# human is available to answer questions.\n#\n# To disable temporarily: export CC_ALLOW_QUESTIONS=1\n# ================================================================\n\n# Allow override via environment variable\nif [ \"${CC_ALLOW_QUESTIONS:-0}\" = \"1\" ]; then\n exit 0\nfi\n\necho \"BLOCKED: Do not ask the human.\" >&2\necho \"Decision order: 1) Decide yourself 2) Log uncertainty to ~/pending_for_human.md 3) Move to the next task\" >&2\nexit 1\n",
|
|
301
|
-
"~/.claude/hooks/no-sudo-guard.sh": "COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)\n[ -z \"$COMMAND\" ] && exit 0\nif echo \"$COMMAND\" | grep -qE '^\\s*sudo\\s'; then\n echo \"BLOCKED: sudo command detected.\" >&2\n echo \"Command: $COMMAND\" >&2\n exit 2\nfi\nexit 0\n",
|
|
302
|
-
"~/.claude/hooks/on-stop.sh": "#!/bin/bash\n# ================================================================\n# on-stop.sh\n# ----------------------------------------------------------------\n# 目的: セッション終了時の状態保存とログ記録\n# 発火条件: Stop(セッション終了時)\n# 副作用: ~/.claude/memory/auto-extracted/YYYY-MM-DD.md に追記\n# ~/bin/claude-code-notifier.sh \"Stop\" を呼び出し(存在時)\n# ================================================================\n\nMEMORY_DIR=\"$HOME/.claude/memory\"\nAUTO_DIR=\"$MEMORY_DIR/auto-extracted\"\nTODAY=$(date +%Y-%m-%d)\nTIMESTAMP=$(date +%H:%M:%S)\n\nmkdir -p \"$AUTO_DIR\"\n\necho \"---\" >> \"$AUTO_DIR/$TODAY.md\"\necho \"## $TIMESTAMP - Session End\" >> \"$AUTO_DIR/$TODAY.md\"\necho \"\" >> \"$AUTO_DIR/$TODAY.md\"\n\n# 通知(既存)\n~/bin/claude-code-notifier.sh Stop \"Task completed\" 2>/dev/null || true\n",
|
|
303
|
-
"~/.claude/hooks/protect-claudemd.sh": "#!/bin/bash\n# ================================================================\n# protect-claudemd.sh — Block edits to CLAUDE.md and settings files\n# ================================================================\n# PURPOSE:\n# Claude Code sometimes modifies CLAUDE.md, settings.json, or\n# other configuration files without permission. This hook blocks\n# Edit/Write to these critical files.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Edit|Write\"\n# ================================================================\n\nINPUT=$(cat)\nTOOL=$(echo \"$INPUT\" | jq -r '.tool_name // empty' 2>/dev/null)\nFILE=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty' 2>/dev/null)\n\nif [[ \"$TOOL\" != \"Edit\" && \"$TOOL\" != \"Write\" ]]; then\n exit 0\nfi\n\nif [[ -z \"$FILE\" ]]; then\n exit 0\nfi\n\nBASENAME=$(basename \"$FILE\")\n\n# Protected files\ncase \"$BASENAME\" in\n CLAUDE.md|.claude.json|settings.json|settings.local.json)\n echo \"BLOCKED: Cannot modify configuration file: $BASENAME\" >&2\n echo \"File: $FILE\" >&2\n echo \"\" >&2\n echo \"Configuration files should be edited manually, not by Claude.\" >&2\n exit 2\n ;;\nesac\n\n# Protected directories\nif echo \"$FILE\" | grep -qE '\\.claude/(hooks|settings|plugins)/'; then\n echo \"BLOCKED: Cannot modify .claude system directory: $FILE\" >&2\n exit 2\nfi\n\nexit 0\n",
|
|
304
|
-
"~/.claude/hooks/session-start-marker.sh": "#!/bin/bash\n# ================================================================\n# session-start-marker.sh — セッション開始時刻を記録\n# ----------------------------------------------------------------\n# 発火条件: PostToolUse(全ツール)\n# 目的: 初回実行時のみセッション開始時刻を記録\n# proof-log-session.sh(Stop hook)がセッション所要時間を計算するために使う\n#\n# 修正 (2026-02-28): PPID単位でファイルを分離。\n# cc-loopと通常セッションが同時に走っても干渉しない。\n# ================================================================\n\n# PPIDでセッションを分離(hookの親プロセス = Claude Code本体)\nSESSION_ID=\"${PPID:-$$}\"\nSTART_FILE=\"${CC_SESSION_START_FILE:-/tmp/cc-session-start-ts-${SESSION_ID}}\"\n\n# 後続hookが同じファイルを参照できるよう環境変数を書き出す\n# (Stop hookはsubprocessなのでenvは引き継がれないが、ファイル名で発見する)\n\n# 既にファイルがあればスキップ(セッション中は1回だけ)\nif [[ -f \"$START_FILE\" ]]; then\n exit 0\nfi\n\ndate +%s > \"$START_FILE\"\n\n# セッション開始時に必須フックの登録を検証\nGUARD_SCRIPT=\"$HOME/.claude/hooks/settings-guard.sh\"\nif [[ -x \"$GUARD_SCRIPT\" ]]; then\n \"$GUARD_SCRIPT\" || true\nfi\n\nexit 0\n",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
312
|
-
"~/.claude/hooks/no-claude-sprawl.sh": "#!/bin/bash\n# no-claude-sprawl.sh: PreToolUse hook for Edit|Write\n# 目的: $HOME/.claude/ をCCランタイム専用に保つ。運用資産の混入を機械的にブロックする。\n# 方針: ホワイトリスト(Step1残留一覧と同期)に該当 → 通す、非該当 → exit 2\n# plans/ はCCのplanモードが自動生成するため一時許可(案内出力のみ)\n#\n# Override (emergency only):\n# CC_ALLOW_CLAUDE_SPRAWL=\"理由を記述\" で一時的にブロック解除(監査ログに記録)\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\nCLAUDE_DIR=\"$HOME/.claude\"\n\n# $HOME/.claude/ 配下でなければ対象外\ncase \"$FILE_PATH\" in\n ${CLAUDE_DIR}/*)\n ;;\n *)\n exit 0\n ;;\nesac\n\n# バイパス環境変数(理由文字列が必須)\nif [[ -n \"${CC_ALLOW_CLAUDE_SPRAWL:-}\" ]]; then\n if [[ \"$CC_ALLOW_CLAUDE_SPRAWL\" == \"1\" ]]; then\n echo \"BLOCKED: CC_ALLOW_CLAUDE_SPRAWL には理由文字列が必須です\"\n echo \"例: CC_ALLOW_CLAUDE_SPRAWL='hotfix: emergency skill deploy'\"\n exit 2\n fi\n mkdir -p \"$HOME/ops/logs\"\n echo \"$(date -Iseconds) BYPASS: $FILE_PATH — 理由: $CC_ALLOW_CLAUDE_SPRAWL\" >> \"$HOME/ops/logs/sprawl-bypass.log\"\n exit 0\nfi\n\n# $HOME/.claude/ からの相対パス\nREL=\"${FILE_PATH#${CLAUDE_DIR}/}\"\n\n# ホワイトリスト判定(Step1残留一覧と1:1同期)\ncase \"$REL\" in\n # 既存mdファイル\n CLAUDE.md|COST_CONTROL.md|SECURITY_POLICY.md|dod-checklists.md)\n exit 0 ;;\n SKILL_INVENTORY.md|SKILLS_CATALOG.md|SKILL_EXPANSION.md)\n exit 0 ;;\n # 設定\n settings.json|settings.local.json|settings.json.got-theme-backup|design-system.yaml)\n exit 0 ;;\n # ディレクトリ配下\n skills/*|skills-pending/*|hooks/*|commands/*|memory/*|projects/*)\n exit 0 ;;\n # CC内部管理\n tasks/*|todos/*|plugins/*|mcp-servers/*|backups/*)\n exit 0 ;;\n # 機械管理キャッシュ\n cache/*|paste-cache/*|file-history/*|debug/*|session-env/*|shell-snapshots/*)\n exit 0 ;;\n # テレメトリ\n statsig/*|telemetry/*|usage-data/*)\n exit 0 ;;\n # 履歴\n history.jsonl|stats-cache.json)\n exit 0 ;;\n # dotfiles\n .git/*|.credentials.json|.mcp.json|.gitignore)\n exit 0 ;;\n # plans/ — CCのplanモードが自動生成。ブロックしないが案内を出す\n plans/*)\n echo \"INFO: \\$HOME/.claude/plans/ はplanモード用の一時領域です。\"\n echo \"恒久保存: ops-put plan <filename> で \\$HOME/ops/session-plans/ にコピーしてください。\"\n exit 0 ;;\nesac\n\n# ホワイトリスト非該当 → ブロック\necho \"BLOCKED: \\$HOME/.claude/ への運用資産の追加は禁止(ランタイム専用ディレクトリ)。\"\necho \"検出パス: $FILE_PATH\"\necho \"\"\necho \"正しい置き場所:\"\necho \" - セッション計画 → ops-put plan <file>\"\necho \" - インフラ → ops-put infra <file>\"\necho \" - ログ → ops-put note <file>\"\necho \" - 参考資料 → ops-put doc <file>\"\necho \" - マーケティング → ops-put marketing <file>\"\necho \" - 正本マップ → \\$HOME/ops/docs/source-of-truth.md\"\necho \"\"\necho \"例外(緊急): CC_ALLOW_CLAUDE_SPRAWL=\\\"理由を記述\\\" (監査ログに記録される)\"\nexit 2\n",
|
|
313
|
-
"~/.claude/hooks/error-gate.sh": "#!/bin/bash\n# ================================================================\n# error-gate.sh — Unresolved Error Blocker\n# ================================================================\n# PURPOSE:\n# Detects unresolved errors in error log and blocks external\n# publishing actions until errors are cleared.\n#\n# Allows local-only operations (read, edit, test, syntax check)\n# but blocks high-risk external actions that could propagate\n# errors to users (git push, npm publish, curl POST, etc).\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# WHAT IT BLOCKS (exit 2) when errors exist:\n# - git push\n# - npm publish\n# - curl POST/PUT/DELETE\n# - Any external API calls\n#\n# WHAT IT ALLOWS (exit 0) even with errors:\n# - Local reads (cat, ls, grep, etc)\n# - Local edits and test runs\n# - Syntax checks\n# - All non-blocking operations\n#\n# CONFIGURATION:\n# CC_ERROR_LOG — path to error log file\n# default: \"$HOME/.claude/error-tracker.log\"\n#\n# CC_ERROR_THRESHOLD — minimum severity to block external actions\n# default: \"WARNING\" (block on WARNING and ERROR)\n# Options: \"ERROR\", \"WARNING\", \"INFO\"\n#\n# NOTE: Error log format: each line = \"TIMESTAMP|SEVERITY|MESSAGE\"\n# ================================================================\n\nINPUT=$(cat)\nCOMMAND=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null)\n\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# Get error log path from env\nERROR_LOG=\"${CC_ERROR_LOG:-$HOME/.claude/error-tracker.log}\"\nTHRESHOLD=\"${CC_ERROR_THRESHOLD:-WARNING}\"\n\n# If no error log exists, allow everything\nif [[ ! -f \"$ERROR_LOG\" ]]; then\n exit 0\nfi\n\n# Check if log has unresolved errors (non-empty and contains recent errors)\nif [[ ! -s \"$ERROR_LOG\" ]]; then\n exit 0 # Empty file = no errors\nfi\n\n# Count errors matching threshold\nERROR_COUNT=0\nif [[ \"$THRESHOLD\" == \"ERROR\" ]]; then\n ERROR_COUNT=$(grep -c \"^[^|]*|ERROR|\" \"$ERROR_LOG\" 2>/dev/null || echo 0)\nelse # WARNING or INFO\n ERROR_COUNT=$(grep -cE \"^[^|]*(|WARNING||ERROR|)\" \"$ERROR_LOG\" 2>/dev/null || echo 0)\nfi\n\nif (( ERROR_COUNT == 0 )); then\n exit 0 # No matching errors\nfi\n\n# Check if this is an external action that should be blocked\nBLOCKS_EXTERNAL=0\n\nif echo \"$COMMAND\" | grep -qE '^\\s*git\\s+push'; then\n BLOCKS_EXTERNAL=1\nelif echo \"$COMMAND\" | grep -qE '^\\s*npm\\s+publish'; then\n BLOCKS_EXTERNAL=1\nelif echo \"$COMMAND\" | grep -qE 'curl\\s+-(X(POST|PUT|DELETE)|d)'; then\n BLOCKS_EXTERNAL=1\nelif echo \"$COMMAND\" | grep -qE 'curl\\s+.*(-d|-X(POST|PUT|DELETE))'; then\n BLOCKS_EXTERNAL=1\nfi\n\n# If not an external action, allow it\nif (( BLOCKS_EXTERNAL == 0 )); then\n exit 0\nfi\n\n# Block external action due to unresolved errors\necho \"BLOCKED: Unresolved errors in error log.\" >&2\necho \"\" >&2\necho \"Error log: $ERROR_LOG\" >&2\necho \"Unresolved errors: $ERROR_COUNT\" >&2\necho \"\" >&2\necho \"External actions are blocked until errors are resolved.\" >&2\necho \"Review the error log and fix issues before publishing.\" >&2\nexit 2\n",
|
|
314
|
-
"~/.claude/hooks/destructive-guard.sh": "#!/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=$(echo \"$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 echo \"$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 echo \"$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 echo \"$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=$(echo \"$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 echo \"$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 echo \"$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 echo \"$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 echo \"$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 echo \"$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 echo \"$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 echo \"$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 echo \"$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\nexit 0\n",
|
|
315
|
-
"~/.claude/hooks/branch-guard.sh": "#!/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)\nCOMMAND=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null)\n\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# Only check git push commands\nif ! echo \"$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 echo \"$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 echo \"$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",
|
|
316
|
-
"~/.claude/hooks/comment-strip.sh": "#!/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)\nCOMMAND=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null)\n\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# Strip leading comment lines and empty lines\nCLEAN=$(echo \"$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",
|
|
317
|
-
"~/.claude/hooks/cd-git-allow.sh": "#!/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)\nCOMMAND=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null)\n\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# Only handle cd + git compounds\nif ! echo \"$COMMAND\" | grep -qE '^\\s*cd\\s+.*&&\\s*git\\s'; then\n exit 0\nfi\n\n# Extract the git subcommand\nGIT_CMD=$(echo \"$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",
|
|
318
|
-
"~/.claude/hooks/secret-guard.sh": "#!/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=$(echo \"$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 echo \"$COMMAND\" | grep -qE '^\\s*git\\s+add'; then\n # Direct .env file staging\n if echo \"$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 echo \"$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 echo \"$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",
|
|
319
|
-
"~/.claude/hooks/scope-guard.sh": "#!/bin/bash\n# scope-guard.sh — Block file operations outside the project directory\n#\n# Solves: Claude Code deleting files on Desktop, in ~/Applications,\n# or anywhere outside the working directory (#36233, #36339)\n#\n# Usage: Add to settings.json as a PreToolUse hook\n#\n# {\n# \"hooks\": {\n# \"PreToolUse\": [{\n# \"matcher\": \"Bash\",\n# \"hooks\": [{ \"type\": \"command\", \"command\": \"~/.claude/hooks/scope-guard.sh\" }]\n# }]\n# }\n# }\n\nINPUT=$(cat)\nTOOL=$(echo \"$INPUT\" | jq -r '.tool_name // empty' 2>/dev/null)\nCMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null)\n\n[[ \"$TOOL\" != \"Bash\" ]] && exit 0\n[[ -z \"$CMD\" ]] && exit 0\n\n# Skip string output commands\nif echo \"$CMD\" | grep -qE '^\\s*(echo|printf|cat\\s*<<)'; then\n exit 0\nfi\n\n# Check for destructive commands with paths outside project\nif echo \"$CMD\" | grep -qE '\\brm\\b.*(-[a-zA-Z]*[rf]|--(recursive|force))'; then\n # Block absolute paths\n if echo \"$CMD\" | grep -qE '\\brm\\b[^|;]*\\s+/[a-zA-Z]'; then\n echo \"BLOCKED: rm with absolute path\" >&2\n echo \"Command: $CMD\" >&2\n exit 2\n fi\n # Block home directory paths\n if echo \"$CMD\" | grep -qE '\\brm\\b[^|;]*\\s+~/'; then\n echo \"BLOCKED: rm targeting home directory\" >&2\n exit 2\n fi\n # Block parent directory escapes\n if echo \"$CMD\" | grep -qE '\\brm\\b[^|;]*\\s+\\.\\./'; then\n echo \"BLOCKED: rm escaping project directory\" >&2\n exit 2\n fi\nfi\n\n# Block targeting well-known user/system directories\nif echo \"$CMD\" | grep -qiE '\\b(rm|del|Remove-Item)\\b.*(Desktop|Applications|Documents|Downloads|Library|Keychain|\\.aws|\\.ssh)'; then\n echo \"BLOCKED: targeting system/user directory\" >&2\n exit 2\nfi\n\nexit 0\n"
|
|
320
|
-
}
|
|
321
|
-
}
|