TL;DR
- Claude Codeの
settings.jsonに書く Hooks をフル活用して、会話メモの自動保存・Obsidian Vault同期・機密ファイル保護・自己リマインダーを実装した - 構成は 3層(global / project / project-local)× 5カテゴリ(事故防止 / 会話保存 / Wiki連携 / 観測 / バッチ)
- 便利だが自動pushフックで
.env.bakのAPIキーをリポジトリに混入させた事故もあったので、注意点も併記する - 全フック設定の構造、トリガー、jqでのJSON判定、
systemMessage返却の実装例を掲載
想定読者
- Claude Code を業務で本格的に使い始めた方
- フックの仕組みを「何ができるか」レベルから一段踏み込みたい方
- 大規模な会話セッションをこなしてコンテキスト管理に悩んでいる方
- Obsidian や個人ナレッジ管理と Claude Code を連携させたい方
1. Claude Code Hooks の前提知識
Claude Code のフックは、settings.json 内の hooks キーで定義します。発火タイミングは以下:
| イベント | タイミング |
|---|---|
SessionStart |
セッション開始時 |
Stop |
セッション終了時(強制終了直前も発火) |
PreToolUse |
ツール実行直前 |
PostToolUse |
ツール実行直後 |
SubagentStop |
サブエージェント終了時 |
Notification |
通知発生時 |
matcher でツール名フィルタが可能(例: "matcher": "Read" で Read ツール時のみ発火)。
フックの実体は bash コマンド または bash スクリプト。標準出力に JSON を返すと Claude にメッセージを伝えられます(例: systemMessage フィールド)。
2. 全体構成(10本超・3層・5カテゴリ)
私の構成を整理するとこうなります。
3層構造
| 層 | パス | 用途 | gitignore |
|---|---|---|---|
| Global | ~/.claude/settings.json |
全プロジェクト共通 | - |
| Project | <repo>/.claude/settings.json |
プロジェクトチーム共有 | コミット対象 |
| Project Local | <repo>/.claude/settings.local.json |
個人最適化(許可リスト等) | gitignore |
5カテゴリのフック一覧
| カテゴリ | フックファイル | 発火イベント |
|---|---|---|
| 事故防止 | デニーリスト(permissions.deny) | (パーミッション層) |
| 事故防止 | protect-sensitive-files.sh |
PreToolUse(Edit|Write|MultiEdit) |
| 会話保存 | session-end-to-vault.sh |
Stop |
| 会話保存 | session-start-vault-pending.sh |
SessionStart |
| 会話保存 | post-tool-vault-sync.sh |
PostToolUse |
| Wiki連携 | note-post-to-wiki.sh |
PostToolUse(Write|Edit) |
| Wiki連携 | vault-auto-ingest.sh |
(Stop後段) |
| Wiki連携 | hot-md-updater.sh |
Stop |
| Wiki連携 |
sync-pull.sh / sync-push.sh
|
SessionStart / Stop |
| 観測 | PreToolUse(Read) リマインダー | PreToolUse(Read) |
| バッチ | daily-vault-batch.sh |
launchd 定期実行 |
| バッチ | weekly-wiki-lint.sh |
launchd 定期実行 |
| プロジェクト固有 | typecheck-vscode-ext.sh |
PostToolUse(Edit|Write|MultiEdit) |
3. 実装例:個人的に推せるフック3つ
3-1. Stopフックで会話を自動保存(session-end-to-vault.sh)
動機
セッション中に強制フリーズ・強制終了が稀に発生し、会話履歴が消えることがあった。最悪のケースでも要点だけは残したい。
副次的に、noteブログのネタ収集にも使えると判明したので積極利用に切り替えた。
設定(settings.json)
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash $HOME/.claude/hooks/session-end-to-vault.sh"
}
]
}
]
}
}
スクリプト要点(10行程度の抜粋)
#!/bin/bash
# session-end-to-vault.sh - セッション終了時に会話を Vault に保存
VAULT_DIR="$HOME/Documents/notes/MyVault/conversations"
DATE=$(date +%Y-%m-%d)
HASH=$(echo "$CLAUDE_SESSION_ID" | cut -c1-8)
OUTPUT="$VAULT_DIR/$DATE/会話メモ_${HASH}.md"
mkdir -p "$(dirname "$OUTPUT")"
# ここで Claude のセッションログを取得して整形(実装は環境依存)
extract_session_summary > "$OUTPUT"
結果として得られるディレクトリ構造
notes/MyVault/conversations/
├── 2026-05-21/
│ ├── 会話メモ_fcce9751.md
│ ├── 会話メモ_5f958391.md
│ └── 会話メモ_c830936c.md
└── 2026-05-22/
└── ...
1日複数セッション × 数週間で50〜100ファイル蓄積される。Obsidian側で検索・分類しているので、後から「あのときの議論どこだっけ」を解決しやすい。
3-2. PreToolUse(Read) で自分自身にリマインダー
動機
巨大ファイルを Read ツールで丸ごと読むと、コンテキストが急激に膨張して autocompact(自動要約)が走り、会話が壊れることがある。
CLAUDE.md(プロジェクト指示書)に「Readは offset/limit を指定する」と書いていても、Claude は時折これを忘れる。フックで毎回強制リマインドする仕組みにした。
設定
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "jq -e '.tool_input | select(.offset == null or .limit == null)' >/dev/null 2>&1 && echo '{\"systemMessage\": \"⚠️ トークン節約ルール: Readツールが offset/limit パラメータなしで呼び出されました。\\n推奨: offset/limit指定、grep事前絞り込み、並列Read。\"}' || true"
}
]
}
]
}
}
仕組み
-
PreToolUseイベントで Claude がツールを呼ぶ直前に発火 -
matcher: "Read"で Read ツール限定 - 標準入力に渡される JSON を
jqでパース -
.tool_input.offsetまたは.tool_input.limitがnullか判定 - 該当なら
systemMessageを含む JSON を標準出力に返す → Claude が受け取って警告を意識する
効果
- 巨大ファイル全文 Read のミスを大幅に減らせる
- CLAUDE.md に書くだけでは抑止できない「うっかり」をフックで物理的にブロック相当の効果
注意
- 本当にファイル全体が必要なケースもある(小さい設定ファイル等)。警告は出るが処理はブロックしない(
|| trueで常に成功扱い) - Postの方にも
Extensive reading (N files)警告を別途仕込んでおくと、Read使いすぎ自体も検知できる
3-3. 3層構造での設定分離
動機
最初は全部 ~/.claude/settings.json に突っ込んでいたが、案件が増えてきて以下の問題が出た:
- 業務案件にだけ走らせたいフックが他プロジェクトでも動いてしまう
- チームに共有したい設定と個人の許可リストが混在
- gitignore したい個人設定がコミット対象に混ざる
構成
~/.claude/settings.json # Global
└── 全プロジェクト共通フック・デニーリスト・Wiki同期
<project>/.claude/settings.json # Project (commit対象)
└── プロジェクト固有フック (typecheck, 機密保護)
<project>/.claude/settings.local.json # Project Local (gitignore)
└── 個人の許可リスト・MCP有効化
Project層の例(settings.json)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "/abs/path/.claude/hooks/protect-sensitive-files.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "/abs/path/.claude/hooks/typecheck-vscode-ext.sh"
}
]
}
]
}
}
効果
- Global は全プロジェクトで共有される定常運用フック
- Project はチーム展開時の挙動を統一
- Local は個人の許可リストで gitignore 対象、コミット衝突を避ける
- 各層は加算的にマージされるので、Project で追加して Global を上書きしない使い方ができる
4. ⚠️ 自分に起こったアクシデント3件
便利にしたぶん、踏んだ落とし穴も正直に共有します。
4-1. 自動pushフックで .env.bak のAPIキーをリポジトリに混入
事象
Stopフックに git add -A && git commit && git push を組み込んでいた。会話メモを自動保存・自動同期させるためだったが、 .gitignore 漏れの .env.bak(過去にバックアップで作成・APIキー入り)が毎セッションpushされていた。
気づいたきっかけ
gitleaks を別目的で導入したところ、past commits を走査して .env.bak が検出された。それまで数週間気づかなかった。
対応
-
git filter-repoで履歴から.env.bakを完全削除 - 該当APIキーを即時ローテーション
-
.gitignoreに「禁則パターン全部入り」を再構築 - Stopフックに
gitleaks pre-commit相当の事前チェックを追加 -
protect-sensitive-files.shを PreToolUse(Edit|Write|MultiEdit) に追加
教訓
自動コミット系フックを入れる前に:
-
.env*/*.bak/*.key/*.pem/secrets//credentials/を先にgitignoreする - 可能ならPrivateリポでも
gitleaksを pre-commit で回す - Stopフックの自動pushは「便利と引き換えに監視できなくなる」ことを認識して導入する
4-2. launchdサンドボックスで「動いてるのにファイルがない」
事象
daily-vault-batch.sh を launchd で毎朝5時に走らせていた。ログには completed successfully と出ているのに、想定の場所に出力ファイルが生成されていない状態が3日続いた。
原因
macOSのlaunchdジョブはサンドボックス制限がかかる場合があり、スクリプトが書き込んでいたパスが仮想ディレクトリにリダイレクトされていた。実体ファイルシステムには到達していなかった。
対応
-
~/Library/LaunchAgents/<plist>のWorkingDirectoryを明示 - 出力先を
~/Documents/notes/...の絶対パスに統一 - スクリプト末尾に
[ -f "$EXPECTED_OUTPUT" ] || exit 1で実体検証を追加 - ログ監視を「ジョブ成功」だけでなく「ファイル存在」まで拡張
教訓
バックグラウンドジョブは「ログ成功」だけでは不十分。実体ファイル存在の検証まで仕込まないと、静かに壊れている期間を見逃す。
4-3. フック過多でセッション開始が体感的に重い
事象
SessionStart で2フック・PostToolUse で複数フック・MCP初期化が同時並行で走り、Claude Code を起動してから初回応答が来るまでに数秒の待ち時間が発生するようになった。
原因
フック数の累積による起動オーバーヘッド。特に SessionStart の sync-pull.sh(git fetch + pull)は async にしていても初回応答に影響する。
対応
- 不要フックの定期棚卸し(半年に1回)
-
async: trueを活用して並列起動可能なものは並列化 - 重い処理は
daily-vault-batch.sh側に逃がしてSessionStartから外す
教訓
「とりあえず入れておこう」のフックは時間とともにコストになる。「絶対欲しい」と判断したものだけ残す。
4-4. 「動いていると思っていた」ゾンビフックの発見
事象
記事執筆中に既存フックを棚卸ししたところ、settings.json に登録されないままスクリプトファイルだけ存在している ゾンビフックが3本見つかった。設計はしたが、登録漏れで一度も発火していなかった。
具体的には:
-
skill-observer.sh: スキル使用ログを JSONL で記録するつもりだったが、PostToolUse の matcher にSkill用の登録がされておらず未稼働 -
skill-inspector.py: 上記ログを集計してレポート生成する設計だったが、元データが空なので未稼働 -
post-write-review.sh: コード編集記録を取るつもりだったが、settings.json への登録漏れで未稼働
気づいたきっかけ
各フックの出力先ログを確認したところ、いずれもログファイルが0件 or ディレクトリ自体未生成だった。
対応
- settings.json の
hooksセクションと、~/.claude/hooks/配下のスクリプトを突き合わせ - 登録されていないスクリプトは archive ディレクトリに退避
- 出力ログを生成しないフックは「稼働している証拠なし」とみなして整理対象に
教訓
「設定したつもり」と「実際に稼働している」は別。実出力ログの存在で稼働確認する習慣を持たないと、ゾンビが溜まる。手元のスクリプトを書いただけで満足せず、settings.json への正しい登録と、実ログの確認まで含めて初めて「稼働した」と言える。
5. その他のフック(参考)
詳細は割愛しますが、以下も運用中です:
-
daily-vault-batch.sh/weekly-wiki-lint.sh: launchd定期実行でVault整理 -
note-post-to-wiki.sh: 投稿済み記事の元ネタを自動でWikiに同期 -
post-tool-vault-sync.sh: ツール実行のたびに会話を差分でVaultに保存 -
hot-md-updater.sh: 次セッション開始時に読み込まれる構造化サマリを自動生成(後述)
おまけ: 次セッションの文脈引き継ぎ(hot-md-updater.sh)
最近追加して気に入っている仕組みが、hot-md-updater.sh です。
Stop フックで Claude Haiku を呼び出し、当日の作業メモを構造化サマリにして ~/.claude/hot.md に書き出します。生成される構造はこんな感じ:
# Hot Context
Updated: 2026-06-03
## 現在の状況
- ...
## 今日のアウトプット
- ...
## 次セッションで把握すべきこと
- ...
## 未解決の論点
- ...
そして ~/CLAUDE.md(グローバル)に「セッション開始時は必ず ~/.claude/hot.md を読み込む」という指示を入れています。これで新しいセッションを開いた瞬間に、前回どこまでやったかを Claude が把握してくれます。
「続きから」と言うだけで、未完了タスクから再開できる運用になりました。
6. まとめ
- Claude Code のフックは
SessionStartStopPreToolUsePostToolUseなどを組み合わせて個人運用OSレベルの自動化が可能 -
jqで標準入力のJSONを判定してsystemMessageを返すことで、Claude自身に自己リマインドを促すフックも書ける - 3層構造(global / project / project-local)で運用すると、共有設定と個人設定がきれいに分離できる
-
ただし自動コミット・自動push系は機密漏洩リスクが高い。
.gitignore設計とgitleaks併用が前提 - 「便利だから」と増やしすぎると起動コストになる。定期棚卸しが必要
- 「設定したつもり」と「実稼働」は別物。実ログでの稼働確認まで含めて完了
おまけ: トークン節約と層分けの誤解
3層に分けるとトークン消費が減ると思いがちですが、設定ファイルは全層がマージされてロードされるので、消費する設定の総量は変わりません。3層分けの効用は「整理整頓・git管理しやすさ・共有/個人の分離」であって、トークン節約とは別の話です。
本当にトークンを節約したいなら、重複する個別許可を削除してワイルドカードに寄せるほうが効きます。たとえばグローバルで Bash(*) が許可済みなら、プロジェクトローカルの Bash(git status) Bash(grep *) 等の個別許可は冗長です。私は同様の冗長を削除して settings.local.json を 38% 縮められました。
フックは設定次第でプログラマブルな自動化レイヤーとして活用できます。同じ方向性の運用をしている方がいたらコメントで教えてほしいです。
参考リンク
タグ案: ClaudeCode Anthropic 自動化 Obsidian セキュリティ bash jq