はじめに
「昨日まで問題なく動いていたのに、今日は妙に遅い」
「なぜか出力フォーマットが変わってパースが失敗する」
「再現しない不具合がたまに起きる」
Claude Codeを業務で活用し始めた方なら、こんな経験があるかもしれません。そして厄介なことに、ログを見てもエラーは出ていない。
本記事では、Claude Codeの拡張エコシステムで静かに進行する「競合問題」について解説します。skills、hooks、CLAUDE.mdなどの拡張機能が複数同居したときに何が起きるのか、なぜ従来のデバッグ手法では見つけにくいのか、そしてどう対処すべきかを、実践的な診断ツールとともに紹介します。
1. 背景:拡張が増えるほど「便利」と一緒に「不具合」も増える
Claude Codeでは、以下のような拡張機能が利用できます:
-
スキル(skills): 特定タスク用の行動指針(
~/.claude/skills/) -
スラッシュコマンド: カスタムコマンドの追加(
~/.claude/commands/) - CLAUDE.md: プロジェクト固有の指示を注入
- Hooks: セッション開始時やツール呼び出し時のイベントフック
- 自動化フレームワーク: Claude Codeを実行エンジンとして使うオーケストレーター
これらは、MCPのような「ツール接続の規約」とは異なり、プロンプトレベル・運用レベルでClaudeの振る舞いそのものに介入します。
その結果、拡張同士の衝突(競合)が発生し、以下のような現象が起きます:
| 現象 | 実際に何が起きているか |
|---|---|
| 以前は動いたのに結果が変わる | 新しい拡張がポリシーを上書き |
| たまに固まる | Hooks が重い処理をブロッキング実行 |
| 原因が見つからない | LLM が矛盾を "それっぽく" 整合 |
2. 具体例:複数の拡張を同居させると何が起きるか
2.1 シナリオ設定
以下のような構成を考えてみましょう:
拡張A: 対話支援拡張
└─ CLAUDE.md に「必ずタスク分割してチェックリストで進めろ」と記述
└─ ~/.claude/skills/ にワークフロー定義
拡張B: 開発効率化拡張
└─ hooks で SessionStart 時に環境チェックを実行
└─ 「常にTDD で進めること」というポリシーを注入
自動化ツールC: オーケストレーター
└─ Claude Code CLI を「実行エンジン」として利用
└─ 出力形式の安定性・予測可能性を前提に設計
拡張A/Bは「人間がClaude Codeと対話で使う」前提で設計されています。一方、ツールCは「制御可能で予測可能なClaude」を前提にしています。
この前提の不一致が問題の根本です。
2.2 典型的に観測される症状
| 症状 | 原因 |
|---|---|
| 短いタスクが「計画→分割→検証」の儀式に入り遅延 | 拡張A のワークフロー強制 |
| 出力フォーマットが変わりパースが崩れる | 拡張B のポリシーが形式を変更 |
| ツール選択が偏り、不要な補助ステップが走る | skills が優先順位を変更 |
| 起動直後にログが増え「固まった」ように見える | hooks が重い処理を実行 |
2.3 なぜ気づきにくいのか
最も厄介なのは、Claudeは矛盾する指示を「それっぽく」整合して続行するという点です。
指示A: 必ずタスク分割してから着手せよ
指示B: 余計な手順を入れず、このフォーマットで返せ
指示C: 常にTDDで進めよ
Claudeはこれらを受け取ると、エラーを出すのではなく、すべてを満たそうとして遠回りな行動を取ります。結果として:
- エラーにならない
- 出力がただ "違う"
- 「なんか遅い」「なんか変」という感覚だけが残る
これは**Behavioral Drift(振る舞いのドリフト)**と呼ぶべき現象です。
3. 競合の分類:どこで衝突するのか
競合は大きく3つのレイヤーに分類できます。
3.1 Policy競合(プロンプト合成)— 最も頻出
発生頻度: ★★★★★ / 致命度: ★★★☆☆
拡張が system / injected prompt に方針を注入すると、モデルの最上位の意思決定が変化します。
# 拡張Aの指示
必ず skills を探索してから着手しろ
# 拡張Bの指示
必ずタスク分割してチェックリストで進めろ
# 自動化ツールの期待
余計な手順を入れず、このフォーマットで返せ
観測される症状:
- 以前より遠回り、儀式が増える
- 出力フォーマットが変わる
- ツール選択が偏る
ポイント: 複数の「必ず」「常に」「絶対に」が同居すると、LLMは全部やろうとして動作が変わります。
3.2 State競合(共有状態)— 地味だが厄介
発生頻度: ★★★☆☆ / 致命度: ★★★☆☆
名称が衝突していなくても、同じ場所の状態を複数が読み書きすると破綻します。
典型例:
- グローバル設定とローカル設定の同一ファイルを上書き
- 進行管理ファイルを別ツールが別前提で読む
- worktree/branch 運用が他ツールの前提を壊す
観測される症状:
- 途中から突然やり直す
- 再現性が低い(環境やタイミングで変わる)
3.3 Hooks競合 — 頻度は低めでも「最も致命的」
発生頻度: ★★☆☆☆ / 致命度: ★★★★★
Hooks競合は発生頻度こそ低いものの、起きたときの被害が桁違いに大きいのが特徴です。Policy競合が「出力が変」「手順が増えた」程度の品質劣化で済むのに対し、Hooks競合はシステムが完全に止まることがあります。
なぜHooks競合は致命的なのか
1. 失敗モードが「ハード」になりやすい
Hooks は SessionStart 等で任意のスクリプトを実行でき、以下のような副作用を入れられます:
- 重い処理(依存インストール、更新チェック、巨大リポジトリ走査)
- ネットワーク待ち(外部APIへの問い合わせ)
- 子プロセス起動
- ファイルロック
- stderr への大量出力
これらが入ると、固まる・極端に遅い・永遠に待つが現実に起きます。
2. 再帰(無限ループ)が起きやすい
Hooks で最も危険なのが「再帰パターン」です:
hook が Claude Code CLI を呼ぶ
→ その Claude Code 起動でも hook が走る
→ さらに Claude Code CLI を呼ぶ…(無限再帰)
あるいは:
hook が git 操作やビルドを走らせる
→ その操作が別の監視トリガー(post-checkout等)を誘発
→ 連鎖的に hooks が走る
Policy競合ではここまでの破壊力は出ません。
3. オーケストレーターの I/O を壊す
自動化ツール(オーケストレーター)は「Claude Codeの標準出力」をパースして状態遷移します。Hooksが起動時にログを混ぜたり、TTY制御文字を出したりすると:
解析が壊れる → リトライが走る → リトライが hooks を増幅させる
壊れ方が指数的になり得ます。
4. 原因特定が極めて難しい
Policy競合は設定ファイルのdiffを取れば見えやすいのに対し、Hooks競合は:
- どのタイミングで走ったか
- どのプロセスにぶら下がったか
- 何待ちで固まっているか
がログや環境に依存し、再現性もぶれます。結果、解決まで時間がかかります。
想定される最悪ケース
{
"SessionStart": {
"command": "curl -s https://api.example.com/check && sleep 5"
},
"PreToolCall": {
"command": "claude --print 'Checking...'"
}
}
| 問題 | 結果 |
|---|---|
| ネットワーク待ち + sleep | 起動が極端に遅い |
| CLI の再帰呼び出し | 無限ループでフリーズ |
| stdout にログ出力 | オーケストレーターのパースが壊れる |
競合タイプ別の特性まとめ
┌─────────────┬────────────┬────────────┬──────────────────┐
│ 競合タイプ │ 発生頻度 │ 致命度 │ 壊れ方の特徴 │
├─────────────┼────────────┼────────────┼──────────────────┤
│ Policy │ 高い │ 中程度 │ 出力が変、遅くなる │
│ State │ 中程度 │ 中程度 │ 再現しない不具合 │
│ Hooks │ 低い │ 最大 │ 完全停止、無限ループ│
└─────────────┴────────────┴────────────┴──────────────────┘
結論:
- 発生頻度は Policy 競合が最多
- 致命度は Hooks 競合が圧倒的に最大(起きたら止まる)
- 再現性の悪さは State 競合が最悪
┌─────────────┬────────────┬────────────┐
│ 競合タイプ │ 発生頻度 │ 致命度 │
├─────────────┼────────────┼────────────┤
│ Policy │ 高い │ 中程度 │
│ State │ 中程度 │ 中程度 │
│ Hooks │ 低い │ 最大 │
└─────────────┴────────────┴────────────┘
4. MCP と比べてなぜプロンプト拡張は衝突しやすいのか
4.1 MCP は「道具の接続規約」
MCP(Model Context Protocol)はツールを以下で接続します:
- 明示的な名前
- 入力スキーマ
- 出力の形式
境界が比較的はっきりしているため、衝突が起きても検出可能な形になりやすいです:
✗ 同名ツールの衝突 → エラーで検出可能
✗ スキーマ不一致 → バリデーションで検出可能
✗ 権限・レート制限 → ログで検出可能
4.2 プロンプト拡張は「意思決定そのものに介入」
一方、プロンプト注入・スキル図・強制ワークフローは:
- どの道具をいつ使うか
- どの形式で返すか
- どの手順を優先するか
という最上位の意思決定に触れます。
合成ルールが標準化されていないため:
? 複数の拡張を同時に入れたときの優先順位 → 不定
? どこまで上書きされるか → 不定
その結果、衝突は "エラー" ではなく "挙動のドリフト" として現れ、検出・原因究明が困難になります。
5. なぜ「見つけにくいバグ」になるのか
この問題が従来のデバッグ手法で見つけにくい理由を整理します。
5.1 「壊れ方」がソフトで、エラーにならない
通常のバグ:
入力 → 処理 → ✗ 例外発生 → スタックトレース
LLM競合:
入力 → 処理 → ✓ 正常終了(でも結果が違う)
結果が「間違い」というより「違う」だけなので、テストも通ってしまいます。
5.2 LLM が矛盾を "それっぽく" 整合して続行する
LLMは矛盾する指示を受けても:
- 破綻を例外として出さない
- 自己整合したストーリーで進める
異常が「慎重になった」「賢くなった」ように擬態するため、問題として認識されにくいです。
5.3 非決定性 × 非決定性 の合成
Hooks の非決定性:
└─ タイミング・実行時間が環境依存
LLM の非決定性:
└─ 同じ入力でも確率的に分岐
掛け合わせ:
└─ 再現しない
└─ ログを入れると直る(観測効果)
5.4 原因が「存在しない」
原因は以下にあります:
- 有効な injected prompt の断片
- 有効な hooks の組み合わせ
- セッション内部の意思決定
これらは通常の手段(スタックトレース、return code、APIレスポンス)では可視化されていません。
6. 「競合調査スキル」の設計
6.1 診断対象(最低限)
Claude Codeの設定は階層構造になっており、診断ツールが調べるべき場所は以下の通りです:
~/.claude/ # グローバル設定(全プロジェクト共通)
├─ settings.json # 設定・hooks定義
├─ CLAUDE.md # グローバル注入プロンプト
├─ skills/ # スキル定義
└─ commands/ # カスタムコマンド
./.claude/ # プロジェクトローカル設定
└─ settings.local.json # プロジェクト固有設定
./CLAUDE.md # プロジェクト固有の注入プロンプト(最優先)
これらはマージされて適用されるため、どこで何が有効になっているか把握しづらいのが問題です。
6.2 診断の優先順位
1. Hooks診断(最優先: 致命度が高い)
└─ イベント別に hooks を列挙
└─ 同イベントに複数 hooks があれば警告
└─ 再帰呼び出し・重い処理の検出
2. Policy診断(頻出: ドリフトの主因)
└─ 注入プロンプトの断片一覧を抽出
└─ 強制語(must/always/never)をカウント
└─ 矛盾しそうな組み合わせを警告
3. State診断
└─ 設定ファイルの重複検出
└─ 同一パスへの複数アクセス警告
7. 実践:診断ツールの実装と実行例
以下に、実際に使える診断シェルスクリプトを示します。
7.1 診断スクリプト
#!/bin/bash
# LLM Extension Conflict Diagnostic Tool
# =======================================
# 使用方法: ./diagnose_conflicts.sh [project_path]
set -e
# 色定義
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'
echo ""
echo "======================================"
echo " LLM Extension Conflict Diagnostic"
echo " LLM拡張競合診断ツール v1.0"
echo "======================================"
echo ""
PROJECT_PATH="${1:-.}"
GLOBAL_CONFIG="$HOME/.claude"
LOCAL_CONFIG="$PROJECT_PATH/.claude"
HOOKS_CONFLICT_COUNT=0
POLICY_CONFLICT_COUNT=0
STATE_CONFLICT_COUNT=0
# --- Hooks診断 ---
echo -e "${BLUE}━━━ Section 2: Hooks 診断 [致命度: 最高] ━━━${NC}"
echo ""
check_hooks() {
local config_path="$1"
local scope="$2"
local settings_file="$config_path/settings.json"
if [ -f "$settings_file" ] && grep -q '"hooks"' "$settings_file" 2>/dev/null; then
echo -e "${YELLOW}⚠ [$scope] Hooks検出: $settings_file${NC}"
# イベントタイプの列挙
grep -oE '"(SessionStart|PreMessage|PostMessage|PreToolCall|PostToolCall)"' \
"$settings_file" 2>/dev/null | sort | uniq | while read event; do
echo " - $event"
done
# 危険パターンの検出
if grep -qE 'claude|llm|ai' "$settings_file" 2>/dev/null; then
echo -e " ${RED}✗ 警告: Hooks内でLLM/CLIを再帰呼び出しの可能性${NC}"
((HOOKS_CONFLICT_COUNT++)) || true
fi
if grep -qE 'sleep|curl|wget|timeout' "$settings_file" 2>/dev/null; then
echo -e " ${YELLOW}⚠ 注意: 重い処理・ネットワーク待ちの可能性${NC}"
((HOOKS_CONFLICT_COUNT++)) || true
fi
else
echo -e "${GREEN}✓ [$scope] Hooksは設定されていません${NC}"
fi
}
check_hooks "$GLOBAL_CONFIG" "Global"
check_hooks "$LOCAL_CONFIG" "Local"
# --- Policy診断 ---
echo ""
echo -e "${BLUE}━━━ Section 3: Policy 診断 [発生頻度: 最高] ━━━${NC}"
echo ""
for file in "$PROJECT_PATH/CLAUDE.md" "$GLOBAL_CONFIG/CLAUDE.md"; do
if [ -f "$file" ]; then
echo -e "${YELLOW}ℹ 注入プロンプト検出: $file${NC}"
must_count=$(grep -ci '\bmust\b\|必ず\|絶対' "$file" 2>/dev/null || echo "0")
always_count=$(grep -ci '\balways\b\|常に\|毎回' "$file" 2>/dev/null || echo "0")
never_count=$(grep -ci '\bnever\b\|決して\|禁止' "$file" 2>/dev/null || echo "0")
if [ "$must_count" -gt 0 ] || [ "$always_count" -gt 0 ] || [ "$never_count" -gt 0 ]; then
echo " 強制語検出:"
echo " - must/必ず: $must_count 件"
echo " - always/常に: $always_count 件"
echo " - never/禁止: $never_count 件"
((POLICY_CONFLICT_COUNT++)) || true
fi
fi
done
# --- 総合結果 ---
echo ""
echo "┌────────────────────────────────────┐"
echo "│ 競合リスクサマリー │"
echo "├────────────────────────────────────┤"
printf "│ Hooks競合(致命度:高) : %2d 件 │\n" $HOOKS_CONFLICT_COUNT
printf "│ Policy競合(頻度:高) : %2d 件 │\n" $POLICY_CONFLICT_COUNT
echo "└────────────────────────────────────┘"
7.2 実行例①:クリーンな環境
$ ./diagnose_conflicts.sh /path/to/clean-project
======================================
LLM Extension Conflict Diagnostic
LLM拡張競合診断ツール v1.0
======================================
━━━ Section 2: Hooks 診断 [致命度: 最高] ━━━
✓ [Global] Hooksは設定されていません
✓ [Local] Hooksは設定されていません
Hooks診断結果: 問題なし
━━━ Section 3: Policy 診断 [発生頻度: 最高] ━━━
Policy診断結果: 強制ポリシーなし
━━━ Section 5: 総合診断結果 ━━━
┌────────────────────────────────────┐
│ 競合リスクサマリー │
├────────────────────────────────────┤
│ Hooks競合(致命度:高) : 0 件 │
│ Policy競合(頻度:高) : 0 件 │
│ State競合(再現性:低) : 0 件 │
├────────────────────────────────────┤
│ 合計 : 0 件 │
└────────────────────────────────────┘
★ 診断結果: 良好
競合リスクは検出されませんでした。
7.3 実行例②:競合が検出される環境
以下のような設定がある環境で実行してみます:
.claude/settings.json:
{
"hooks": {
"SessionStart": {
"command": "curl -s https://example.com/notify && sleep 2"
},
"PreToolCall": {
"command": "claude --print 'Checking permissions...'"
}
}
}
CLAUDE.md:
## 必須ワークフロー
- 必ずタスクを分割してチェックリストを作成すること
- 常に計画フェーズを経てから実装に入ること
- 絶対に直接コードを書き始めてはいけない
- 必ずTypeScriptを使用すること
- 常にテストを先に書くこと
診断結果:
======================================
LLM Extension Conflict Diagnostic
LLM拡張競合診断ツール v1.0
======================================
━━━ Section 2: Hooks 診断 [致命度: 最高] ━━━
✓ [Global] Hooksは設定されていません
⚠ [Local] Hooks検出: .claude/settings.json
登録されているイベント:
- "PreToolCall"
- "SessionStart"
✗ 警告: Hooks内でLLM/CLIを再帰呼び出しの可能性
⚠ 注意: 重い処理・ネットワーク待ちの可能性
Hooks診断結果: 2 件の潜在的問題を検出
━━━ Section 3: Policy 診断 [発生頻度: 最高] ━━━
ℹ [Project Root] 注入プロンプト検出: CLAUDE.md
行数: 20
強制語検出:
- must/必ず: 5 件
- always/常に: 3 件
- never/禁止: 1 件
Policy診断結果: 1 件の強制ポリシーを検出
→ 複数の拡張が同居している場合、振る舞いのドリフトに注意
━━━ Section 5: 総合診断結果 ━━━
┌────────────────────────────────────┐
│ 競合リスクサマリー │
├────────────────────────────────────┤
│ Hooks競合(致命度:高) : 2 件 │
│ Policy競合(頻度:高) : 1 件 │
│ State競合(再現性:低) : 0 件 │
├────────────────────────────────────┤
│ 合計 : 3 件 │
└────────────────────────────────────┘
★ 診断結果: 要対策
複数の競合リスクが検出されました。
プロファイル分離を強く推奨します。
━━━ 推奨アクション ━━━
【Hooks】
1. 再入防止フラグを追加(例: CLAUDE_HOOK_RUNNING=1)
2. タイムアウトを設定(30秒以内推奨)
3. stdout汚染を防ぐ(出力はstderrかファイルへ)
【Policy】
1. 自律実行ツール用に別プロファイルを作成
2. 強制ワークフローを条件付きに変更
3. CLAUDE.md の指示を緩和(must→should等)
8. 対策:設計・運用で最も効くのは「分離」と「観測」
8.1 Hooksは「原則オフ」をデフォルトに(最重要)
最も効果的な対策は、Hooksを原則オフにすることです。
特に自動化ツール(オーケストレーター)を使う場合:
✓ 自動化実行時は hooks を無効化
✓ 人間との対話時だけ hooks を許可
これだけで「固まる系」の事故が大幅に減ります。
8.2 プロファイル分離
用途別にプロファイルを分離することで、競合を根本から防げます:
profile-interactive/ # 対話拡張用
└─ CLAUDE.md # ワークフロー強制OK
└─ skills/ # 便利スキル全部入り
└─ hooks/ # 通知等あり
profile-automation/ # 自律実行用
└─ CLAUDE.md # 最小限の指示のみ
└─ (skills なし)
└─ (hooks なし) ← これが重要
実行時に設定ディレクトリを切り替える運用が特に効きます。
8.3 Hooks のガードレール(入れるなら必須)
どうしてもHooksを使う場合は、以下のガードレールを必ず設けましょう:
必須チェックリスト
| ガードレール | 目的 | 実装例 |
|---|---|---|
| 再入防止 | 無限ループを防ぐ |
CLAUDE_HOOK_RUNNING=1 なら即 exit |
| タイムアウト | 永遠に待たない | 30秒以内で強制終了 |
| 重い処理禁止 | 起動を遅くしない | ビルド・npm install・巨大走査は禁止 |
| 出力制御 | パースを壊さない | stdout ではなく stderr に出す |
実装例
#!/bin/bash
# 安全な hooks のテンプレート
# 1. 再入防止(最重要)
if [ -n "$CLAUDE_HOOK_RUNNING" ]; then
exit 0
fi
export CLAUDE_HOOK_RUNNING=1
# 2. タイムアウト(30秒以内推奨)
timeout 30 your_command
# 3. 出力制御(stdoutを汚さない)
your_command >&2 # or > /dev/null
絶対にやってはいけないこと
# ❌ hooks 内で Claude Code を呼ぶ(無限再帰)
claude --print "checking..."
# ❌ 重い処理を同期実行(固まる)
npm install
pip install -r requirements.txt
find / -name "*.log"
# ❌ ネットワーク待ちをタイムアウトなしで実行
curl https://slow-api.example.com/check
# ❌ stdout に直接出力(パースが壊れる)
echo "Hook running..."
8.4 起動時の「有効介入一覧」の強制ダンプ
診断のためのもう一つの実践的アプローチは、起動時に有効な設定を必ずダンプすることです:
# 起動時診断(hooks内で実行、stderrへ出力)
{
echo "[DIAG] $(date '+%Y-%m-%d %H:%M:%S')"
echo "[DIAG] Active hooks: $(grep -c '"command"' ~/.claude/settings.json 2>/dev/null || echo 0)"
echo "[DIAG] CLAUDE.md lines: $(wc -l < CLAUDE.md 2>/dev/null || echo 0)"
echo "[DIAG] Skills count: $(ls ~/.claude/skills/*.md 2>/dev/null | wc -l)"
} >&2
これを毎回出せるだけでも、「いつから遅くなったか」「何が変わったか」の原因究明が大幅に楽になります。
8.5 強制語の緩和
CLAUDE.md の指示は、状況に応じて柔軟性を持たせましょう:
# Before(硬い)
必ずタスク分割してチェックリストで進めること
# After(柔軟)
複雑なタスクの場合は、タスク分割してチェックリストで進めることを推奨
(単純なタスクは直接実行可)
9. まとめ:今後増える「壊れているのに動いている」問題
拡張が増える世界では:
- 便利さのために介入が強くなる
- ユーザーは複数導入する
- LLMは破綻を隠蔽して動き続ける
結果として、壊れているのに動いているClaude Code環境が増えます。
このクラスの問題は:
- 例外で止まらない
- ログにも出ない
- 再現性が低い
ため、従来のソフトウェア工学のデバッグ手法だけでは対処が難しいです。
解決の鍵は3点に集約されます:
| 対策 | 効果 |
|---|---|
| 分離(プロファイル) | 用途別に環境を分けて競合を防ぐ |
| 観測(有効介入一覧、トレース) | 何が有効か常に把握できる状態にする |
| ガードレール(hooks) | 致命的な副作用を防ぐ仕組みを入れる |
付録:用語定義
| 用語 | 説明 |
|---|---|
| skills | Claude Codeに特定タスクの行動指針を与えるMarkdownファイル群(~/.claude/skills/) |
| commands | スラッシュコマンドとして呼び出せるカスタム命令(~/.claude/commands/) |
| CLAUDE.md | システムプロンプトに追加される指示。プロジェクトルートに配置 |
| hooks | SessionStart等のイベントで任意スクリプトを実行する仕組み(settings.jsonで定義) |
| orchestrator | Claude Code CLIを実行エンジンとして利用する自動化フレームワーク |
| MCP | Model Context Protocol。ツール接続の標準規約 |
| Behavioral Drift | エラーではなく「振る舞いの変化」として現れる競合現象 |