設定ファイルにカンマを一つ余計に置いただけで、Claude Code がまったく起動しなくなった——この状況に一度でも遭遇すると、設定の扱い方に対する考え方が変わります。個人開発でアプリと並行して4つのサイトを自動更新するパイプラインを回している中で、.claude/settings.json に追記した hooks のブロックで末尾カンマを残してしまい、翌朝のスケジュール実行が静かに止まっていたことがありました。エラーは出ているのに、どの階層のどのファイルが原因かがすぐには分からない。この「切り分けに時間が溶ける」感覚こそ、設定まわりの一番の落とし穴だと感じています。
最近の Claude Code には、壊れた構成を検知すると、その設定ファイルだけを隔離して縮退状態(セーフモード)で起動を続ける挙動が入りました。これは便利な一方で、「なぜ普段の権限設定が効いていないのか」を理解していないと、別の混乱を生みます。ここでは、設定が壊れたときに何が起きるのか、どこから切り分けるのか、そして自動運用でそもそも壊さないためにどう設定を組むのかを、実際の復旧手順とあわせて整理します。
設定が壊れると Claude Code はどう振る舞うか
まず押さえておきたいのは、Claude Code の設定が「全か無か」ではないという点です。設定は複数のファイルに分かれて読み込まれ、優先順位の高いものが低いものを上書きします。優先順位は高い方から順に、次のようになっています。
優先度 設定の種類 代表的なパス
1(最優先) エンタープライズ管理ポリシー macOS: /Library/Application Support/ClaudeCode/managed-settings.json Linux: /etc/claude-code/managed-settings.json
2 コマンドライン引数 --model など
3 プロジェクト個人設定 .claude/settings.local.json
4 プロジェクト共有設定 .claude/settings.json
5(最下位) ユーザー設定 ~/.claude/settings.json
この多層構造があるからこそ、1つのファイルが壊れたときの挙動が直感に反します。以前は壊れたファイルがあると Claude Code 全体が立ち上がらないことがありましたが、最近のバージョンは「壊れた構成の隔離」を行います。つまり、JSON として解釈できないファイルを1枚見つけると、そのファイルだけを読み込み対象から外し、残りの有効な設定でセーフモードとして起動を続けます。
ここで起きやすい誤解は、「起動はしたのだから設定は効いているはず」という思い込みです。実際には、隔離されたファイルに書いていた permissions.deny や hooks がまるごと無視されている状態になります。自動運用でこれが起きると、本来 deny していたコマンドが ask に落ちて承認待ちで止まる、あるいは想定したフックが発火せずログだけが残る、といった「半分動いて半分動かない」状態になります。私が経験した「スケジュール実行が静かに止まる」現象も、まさにこのパターンでした。本番のパイプラインで起きると厄介な、典型的な落とし穴です。
セーフモードで起動したかどうかは、起動直後の警告メッセージと /doctor の出力で確認できます。/doctor がどの設定ファイルを問題ありと判定したかを示してくれるので、まずはここを見るのが最短です。/doctor の読み方そのものは /doctor で設定トラブルを3分で切り分ける話 に詳しくまとめています。
どの階層が壊れているかを切り分ける
復旧の第一歩は「どのファイルが壊れているか」の特定です。設定ファイルは高々5枚なので、機械的に全部を JSON として検証すれば、原因はすぐに浮かび上がります。私の場合は次のワンライナーを最初に走らせます。jq で各ファイルを順にパースし、失敗したものだけを赤裸々に表示する作りです。
# 読み込まれうる settings ファイルを全部 JSON 検証する
# 壊れているファイルだけ "INVALID" として表示される
for f in \
"/Library/Application Support/ClaudeCode/managed-settings.json" \
"/etc/claude-code/managed-settings.json" \
" $PWD /.claude/settings.local.json" \
" $PWD /.claude/settings.json" \
" $HOME /.claude/settings.json" ; do
[ -f " $f " ] || continue
if jq empty " $f " 2> /dev/null ; then
echo "OK $f "
else
echo "INVALID $f "
# 壊れている箇所の行番号付きでエラーを表示
jq empty " $f "
fi
done
期待する出力は次のようになります。壊れたファイルだけが INVALID と表示され、jq が「どの行・どの文字で破綻したか」まで教えてくれます。
OK /Users/you/project/.claude/settings.json
INVALID /Users/you/.claude/settings.json
jq: error (at /Users/you/.claude/settings.json:14): Expected another key-value pair at line 14, column 3
この時点で犯人のファイルと行が確定します。jq を入れていない環境なら、Python の標準ライブラリでも同じことができます。
# jq がない場合のフォールバック検証
python3 - " $HOME /.claude/settings.json" << 'PY'
import json, sys
path = sys.argv[1]
try:
with open(path) as fp:
json.load(fp)
print("OK", path)
except json.JSONDecodeError as e:
print(f"INVALID {path}: {e}")
PY
切り分けで一番つまずきやすいのが、優先度の高い「エンタープライズ管理ポリシー」の存在を忘れることです。チーム配布の Mac では /Library/Application Support/ClaudeCode/managed-settings.json が自分の ~/.claude/settings.json を上書きするため、「ユーザー設定を直したのに反映されない」ときは、まずこの最上位ファイルを疑うのが正解です。--model のようなコマンドライン引数も設定より優先されるので、起動スクリプトに古いフラグが残っていないかもあわせて確認します。
壊れたファイルを安全に復旧する
原因ファイルが分かったら、闇雲に直すのではなく、まず「最後に動いていた状態」へ戻せる足場を作ります。設定ファイルはバージョン管理に入れているとは限らないので、編集前に必ずタイムスタンプ付きのバックアップを取ってから手を入れます。
# 壊れたファイルを退避してから編集する(上書き事故を防ぐ)
BROKEN = " $HOME /.claude/settings.json"
cp " $BROKEN " "${ BROKEN }.broken.$( date +%Y%m%d-%H%M%S)"
# 末尾カンマ・コメント混入など「よくある壊し方」を機械チェック
grep -nE ',\s*[}\]]' " $BROKEN " && echo "↑ 末尾カンマの疑い"
grep -nE '^\s*//' " $BROKEN " && echo "↑ JSON にコメントは書けません"
私の経験上、設定が壊れる原因の大半は「末尾カンマ」「JSON にコメントを書いた」「文字列のダブルクォート閉じ忘れ」の3つに集約されます。上の grep はその最頻3パターンを一気に洗い出すためのものです。直したら、編集を反映する前にもう一度 jq empty を通し、緑(OK)になってから Claude Code を再起動します。
どうしても原因が追えないときは、次の手順で二分探索的に追い込むのが、私が個人的に最も確実だと感じているやり方です。
壊れたファイルを {}(空の有効な JSON)に差し替え、設定なしで起動が通るかを確認します。通れば原因は中身にあると確定します。
退避したバックアップから permissions ブロックだけを戻し、再度 jq empty で検証してから起動します。
同様に env → hooks の順でブロックを少しずつ戻し、どのブロックで再び壊れるかを特定します。
# 最小限の有効な settings に差し替えて、起動可否を切り分ける
echo '{}' > " $HOME /.claude/settings.json"
# これで起動が通れば、原因は中身のどこか。退避したファイルから少しずつ戻す
{}(空オブジェクト)は完全に有効な設定なので、これで起動が通れば問題は中身にあると確定します。セーフモードはあくまで応急処置で、隔離されたままでは permissions.deny が効かない危険な状態が続くため、起動できたことで安心せず、必ず有効な設定に戻し切ることを強く推奨します。
そもそも壊さないための設定の分け方
ここからが、復旧よりも本質的に効く話です。1つの巨大な settings.json に権限・環境変数・フックを全部詰め込むと、どれか1箇所のミスでファイル全体が無効になり、無関係なはずの設定まで道連れになります。私は4サイトの自動運用で痛い目を見てから、設定を「壊れても被害が局所化する」構造に組み替えました。App Store と Google Play に出しているアプリの更新作業と記事生成を同じ手元で回しているため、設定事故は放っておくと即座に複数系統へ波及してしまうのです。
考え方はシンプルで、変更頻度とリスクで階層を分けるというものです。めったに変えない権限の土台はプロジェクト共有の .claude/settings.json に置き、頻繁にいじる実験的な設定や個人的な上書きは .claude/settings.local.json に隔離します。こうしておくと、実験中にローカル設定を壊しても、共有設定の permissions.deny(=安全装置)は生き残ります。
// .claude/settings.json — めったに変えない「安全の土台」
{
"permissions" : {
"deny" : [
"Bash(rm -rf /*)" ,
"Bash(git push --force:*)" ,
"Read(./.env)" ,
"Read(./secrets/**)"
]
}
}
// .claude/settings.local.json — 頻繁にいじる「実験ゾーン」
{
"permissions" : {
"allow" : [
"Bash(npm run test:*)" ,
"Bash(jq:*)"
]
},
"env" : {
"MY_FEATURE_FLAG" : "1"
}
}
この分け方の利点は、最も守りたい deny(破壊的コマンドや秘密ファイルの読み取り禁止)が、最も壊れにくいファイルに乗ることです。実験的な allow を settings.local.json で壊しても、安全の土台は無傷です。逆に言えば、deny を実験ファイルに書くのは避けるべきで、安全装置ほど安定したファイルに置くことを推奨します。なお、モデルを意図せぬ自動アップグレードから守る設定の固定については enforceAvailableModels で実行モデルを固定する で別途まとめています。
チーム配布の Mac で起きる「直したのに効かない」
個人運用ではあまり遭遇しませんが、チームに配布された Mac では、もう一段ややこしい挙動が加わります。エンタープライズ管理ポリシー(managed-settings.json)が最上位で読み込まれるため、ここに permissions.deny が定義されていると、あなたが ~/.claude/settings.json でいくら allow を足しても上書きされません。「自分の設定を直したのに挙動が変わらない」ときは、まず管理ポリシーの有無を確認するのが近道です。
# 管理ポリシーが存在し、有効かを最初に確認する
MGD = "/Library/Application Support/ClaudeCode/managed-settings.json"
if [ -f " $MGD " ]; then
echo "管理ポリシーあり: $MGD "
jq '.permissions // {}' " $MGD " # 何が強制されているかを表示
else
echo "管理ポリシーなし — ユーザー/プロジェクト設定が有効"
fi
管理ポリシーは意図的に強制されているものなので、勝手に書き換えるべきではありません。ここで deny されている操作は、組織のルールとして守る前提で設定全体を組み直すのが筋だと考えています。逆に言えば、自分の settings.json を疑う前にこの1ファイルを確認するだけで、原因切り分けの時間を大きく短縮できます。
自動運用では「書き込む前に検証する」を仕組みにする
手元で気をつけるだけでは、自動化されたパイプラインの中で起きる事故は防げません。私のスケジュール実行は sed や cat で設定を書き換える工程を含むため、「書き込んだ JSON が壊れていたら、その時点で止める」ゲートを必ず通すようにしています。push 前ではなく、設定ファイルを更新した直後にその場で弾くのがポイントです。
# settings を書き換えたら、即座に検証して壊れていれば巻き戻す
write_settings () {
local target = " $1 " tmp
tmp = "$( mktemp )"
cat > " $tmp " # 標準入力から新しい内容を受け取る
if jq empty " $tmp " 2> /dev/null ; then
cp " $target " "${ target }.prev" # 直前の有効版を退避
mv " $tmp " " $target "
echo "✅ settings 更新(検証済み): $target "
else
echo "🛑 不正な JSON のため書き込みを中止: $target "
jq empty " $tmp " # エラー位置を表示
rm -f " $tmp "
return 1
fi
}
# 使い方: 新しい設定を標準入力で渡す
cat << 'JSON' | write_settings " $HOME /.claude/settings.json"
{ "permissions": { "deny": ["Bash(rm -rf /*)"] } }
JSON
この関数は、一時ファイルに書いてから jq empty で検証し、合格したときだけ本番ファイルへ mv します。失敗時は本番に一切触れず、直前の有効版(.prev)も残るので、壊れた内容が本番に到達することがありません。この「検証してから置き換える」という順序を仕組みにしてからは、月に2〜3回起きていた設定起因の停止が、ほぼ0%になりました。これは設定ファイルに限らず、生成した記事を push する前に品質ゲートを通す、といった私のパイプライン全体の設計思想とも一貫しています。「壊れた可能性のあるものは、本番に届く前に弾く」という一点に尽きます。
最後に一つだけ実践をおすすめするなら、いま使っている ~/.claude/settings.json を一度 jq empty に通してみてください。普段は問題なく動いていても、この機会に有効性を確かめておくと、次に設定をいじるときの安心感がまったく違います。同じように自動運用で設定を扱っている方の参考になれば嬉しいです。