Claude Code の設定ファイルの場所は、起動したディレクトリで変わる

目次
  1. 表を写すだけでは「読まれたか」が分からない
  2. わざと壊して doctor に名指しさせる
  3. 3つ置いて2つしか名指しされなかった
  4. HOME を差し替えてもユーザー設定は付いてこない
  5. サブディレクトリから起動すると、共有設定だけが届かない
  6. ~/.claude.json は読まれるが、設定ファイルではない
  7. 自分の環境で「効く場所」を確定させる手順

deny を書いた。モデルを指定した。それでも挙動が変わらない。このとき最初に疑うべきは書いた中身ではなく、そのファイルが読まれたかどうかである。読まれていないファイルは、何を書いても正しく見える。JSON は妥当で、キー名も合っていて、それでも一度も参照されていない。

「claude code 設定ファイル 場所」で出てくる記事はどれも4階層の表を載せている。表は正しい。だが表は「あなたの環境でそのファイルが読まれたか」を答えない。私は読まれたかどうかを判定する器具を作り、置き場所を変えながら測った。結果、場所は固定されていなかった

表を写すだけでは「読まれたか」が分からない

公式ドキュメントの設定ファイルと優先順位は、ユーザー・共有プロジェクト・プロジェクトローカル・管理の4つを表にしている。日本語の解説記事もほぼ同じ表を載せている。どれも間違っていない。

問題は、この表が「どこに置けるか」の一覧であって「いま自分の手元でどこが読まれたか」ではないことだ。設定が効かないときに読者ができるのは、表を見直してもう一度同じ場所に書くことだけになる。切り分けの道具がない。

測った環境はこれだけである。

$ sh env.sh
26.6.1
arm64
2.1.267 (Claude Code)
v22.21.1
git version 2.39.5 (Apple Git-154)
# exit=0 (114 ms)

わざと壊して doctor に名指しさせる

claude doctor は、読み込みに失敗した設定ファイルを絶対パスで名指しする。これを逆に使う。設定ファイルをわざと壊し、名指しされたら「そのファイルは読まれた」と判定する。

壊し方には {"permissions": 1} を使った。JSON としては妥当で、スキーマだけが拒否する形である。構文エラーにしないのは理由がある。パーサが止まる前段ではなく、ファイルを開いて中身のキーまで見た段で落ちたことを確かめたいからだ。

claude doctor を選んだ理由は3つある。API に接続せずに終了するので従量課金が発生しない。--help に「カレントディレクトリの設定ファイルを信頼プロンプトなしで読む」と書かれている。対話セッションを開かないのでスクリプトから回せる。

器具はまず較正した。壊れたファイルを1つも置かない状態で名指しが何件出るかを見る。

$ sh instrument.sh
--- 置いた壊れた設定ファイル ---
placed=0
doctor exit=0
--- doctor 出力の行数 ---
      25
--- doctor が名指しした LAB 配下のパス ---
named=0
done
# exit=0 (582 ms)

placed=0 に対して named=0 である。doctor 自体は25行を出力しているので、黙って何もしなかったわけではない。以後すべての実験で、置いた件数と名指しされた件数を同じログに出すことにした。「名指しされなかった」を「そもそも対象が無かった」と取り違えないためである。

ラボは /tmp/claude/cc-settings-lab に作り、実験ごとに丸ごと作り直している。前の実験の残骸が混ざると、名指しの件数がそのまま壊れる。

3つ置いて2つしか名指しされなかった

ユーザー・共有プロジェクト・プロジェクトローカルの3階層に、同じ壊れた設定を1つずつ置いた。HOME をラボ内の fakehome に差し替えたうえで、プロジェクトディレクトリから claude doctor を走らせる。実環境の ~/.claude/settings.json には触れていない。

$ sh tiers2.sh
--- 壊した中身 ---
{"permissions": 1}
--- 置いた壊れた設定ファイル ---
/tmp/claude/cc-settings-lab/fakehome/.claude/settings.json
/tmp/claude/cc-settings-lab/proj/.claude/settings.json
/tmp/claude/cc-settings-lab/proj/.claude/settings.local.json
placed=3
doctor exit=0
--- doctor が名指しした LAB 配下のパス ---
named=2
Invalid settings
- /private/tmp/claude/cc-settings-lab/proj/.claude/settings.json › permissions: Expected object, but received undefined
- /private/tmp/claude/cc-settings-lab/proj/.claude/settings.local.json › permissions: Expected object, but received undefined
done
# exit=0 (1453 ms)

3つ置いて、名指しは2つだった。欠けたのは fakehome/.claude/settings.json、つまりユーザー階層である。

ここで「ユーザー設定は読まれない」と書きたくなる。書いたら誤りになる。このログが支えているのは「差し替えた HOME の下に置いたファイルは名指しされなかった」までだ。原因の候補が2つ残っている。HOME の差し替えが Claude Code に効いていないのか、doctor がユーザー階層をそもそも検査しないのか。どちらなのかは、このログでは分からない。

なお管理設定(managed-settings.json)はシステム領域にあり、この検証では書き込み権限がないため測っていない。以降の件数はすべて管理階層を除いた数である。

HOME を差し替えてもユーザー設定は付いてこない

2つの候補を分けるために、同じ壊れた設定を2箇所に置いて条件を変えた。片方は差し替えた HOME の下、もう片方は CLAUDE_CONFIG_DIR が指すディレクトリである。あわせて、子プロセスに HOME が実際に届いているかも同じログに出した。

$ sh config-dir2.sh
--- 置いた壊れた設定ファイル ---
/tmp/claude/cc-settings-lab/cfg/settings.json
/tmp/claude/cc-settings-lab/fakehome/.claude/settings.json
placed=2
--- 子プロセスに HOME が届いているかの確認 ---
child_HOME=/tmp/claude/cc-settings-lab/fakehome
=== ケース1: HOME だけ差し替え(CLAUDE_CONFIG_DIR なし) ===
doctor exit=0
named=0
=== ケース2: CLAUDE_CONFIG_DIR を LAB/cfg に向ける ===
doctor exit=0
named=1
- /tmp/claude/cc-settings-lab/cfg/settings.json › permissions: Expected object, but received undefined
done
# exit=0 (1200 ms)

child_HOME が差し替え先を指しているので、環境変数は子プロセスまで届いている。それでもケース1の名指しは0件だった。ケース2では1件名指しされた。

つまり doctor はユーザー階層を検査している。検査しているのに名指しされなかったのだから、ユーザー設定の場所を決めているのは HOME ではない。動かしたいなら CLAUDE_CONFIG_DIR を使う。

ここで断っておく。ケース1で実環境側の ~/.claude/settings.json が代わりに読まれたのかどうかは測っていない。実環境のファイルを壊さない方針にしたので、名指しさせる手段がない。分かっているのは「差し替え先は読まれなかった」までである。

実務への影響は地味だが効く。env HOME=/tmp/x claude ... のような書き方でユーザー設定を切り離したつもりになっても、切り離せていない。テスト用の設定でCIを回すときはここを踏む。

サブディレクトリから起動すると、共有設定だけが届かない

ここからが本題である。同じ木を2つ作った。片方は git init 済み、もう片方は git 管理外。どちらもルートと sub/ の両方に settings.jsonsettings.local.json を置き、合計8ファイルを壊した。そのうえで**sub/ から** claude doctor を走らせる。

$ sh subdir.sh
git リポジトリか: repo=yes plain=no
...
placed=8
=== ケース1: git リポジトリの sub/ から起動 ===
doctor exit=0
named=3
- /private/tmp/claude/cc-settings-lab/repo/sub/.claude/settings.json › permissions: Expected object, but received undefined
- /private/tmp/claude/cc-settings-lab/repo/sub/.claude/settings.local.json › permissions: Expected object, but received undefined
- /private/tmp/claude/cc-settings-lab/repo/.claude/settings.local.json › permissions: Expected object, but received undefined
=== ケース2: git 管理外の sub/ から起動 ===
doctor exit=0
named=2
- /private/tmp/claude/cc-settings-lab/plain/sub/.claude/settings.json › permissions: Expected object, but received undefined
- /private/tmp/claude/cc-settings-lab/plain/sub/.claude/settings.local.json › permissions: Expected object, but received undefined
done
# exit=0 (897 ms)

git リポジトリの sub/ から起動したとき、読まれたのは3つ。sub/ に置いた2つと、リポジトリルートの settings.local.json である。ルートの settings.json は名指しされていない。

同じ木を git 管理外にすると、読まれたのは sub/ の2つだけになった。ルート側は settings.local.json も含めて名指しされない。差は git リポジトリかどうかだけなので、ルートまで遡る挙動は git 由来である。

この結果が意味するところは、チームで運用していると重い。

  • .claude/settings.json起動したディレクトリのものしか読まれない
  • .claude/settings.local.json は起動ディレクトリとリポジトリルートの両方が読まれる
  • 同じ .claude/ フォルダに入っている2ファイルなのに、探索の起点が違う

リポジトリルートに .claude/settings.json を commit してチーム共有の権限を配ったとする。packages/api/ に降りてから claude を起動する人には、そのファイルは届かない。届かないことは画面に出ない。権限プロンプトが増えたか減ったかでしか気づけない。

一方、自分だけの settings.local.json はルートまで遡って読まれる。共有したいファイルが届かず、共有しないファイルが広く届くという、直感と逆の組み合わせになっている。

~/.claude.json は読まれるが、設定ファイルではない

もう1つ、名前が紛らわしいファイルがある。~/.claude.json である。「設定ファイル」として紹介されることがあるが、ここに permissions を書いても効かない。効かないことを、同じ壊し方の対照で確かめた。

$ sh claude-json.sh
=== ケース1: 対照。settings.json に {"permissions": 1} ===
placed=1
/tmp/claude/cc-settings-lab/cfg/settings.json
doctor exit=0
named=1
- /tmp/claude/cc-settings-lab/cfg/settings.json › permissions: Expected object, but received undefined
error_lines=2
Invalid settings
- Remote Control availability could not be verified (no server response this session)
=== ケース2: 同じ中身を .claude.json に入れる ===
placed=1
/tmp/claude/cc-settings-lab/cfg/.claude.json
doctor exit=0
named=0
error_lines=1
- Remote Control availability could not be verified (no server response this session)
...
# exit=0 (1129 ms)

まったく同じ中身である。settings.json に入れれば名指しされ、.claude.json に入れれば名指しされない。

ただしこれだけでは「ファイルが読まれていない」のか「読まれているが設定として検査されない」のかが分かれない。そこで3つ目のケースで、同じ .claude.json を JSON として壊した。

$ sh claude-json.sh
...
=== ケース3: .claude.json を JSON として壊す ===
placed=1
/tmp/claude/cc-settings-lab/cfg/.claude.json
doctor exit=1
named=4
Claude configuration file at /tmp/claude/cc-settings-lab/cfg/.claude.json is corrupted: JSON Parse error: Expected '}'
The corrupted file has been backed up to: /tmp/claude/cc-settings-lab/cfg/backups/.claude.json.corrupted.1789617103146
Configuration error in /tmp/claude/cc-settings-lab/cfg/.claude.json: JSON Parse error: Expected '}'
Claude configuration file at /tmp/claude/cc-settings-lab/cfg/.claude.json is corrupted: JSON Parse error: Expected '}'
error_lines=5
...
# exit=0 (1129 ms)

読まれている。それどころか、破損を検出して backups/ に退避までしている。doctor の終了コードもここだけ 1 になった。ケース1とケース2はどちらも 0 だったので、この違いは破損そのものに由来する。

なお Remote Control availability could not be verified は、検証環境が外部ネットワークに出られないために出ている行である。3ケースすべてに共通する条件なので、ケース間の差の説明にはならない。設定ファイルの検査自体はオフラインで完結している。

まとめると、.claude.json読まれるが設定ファイルではない。役割はサインイン状態や MCP サーバー構成、プロジェクトごとの信頼判断の保持であって、permissionshooks の置き場ではない。ここに書いた設定は、エラーも警告も出さずに無視される。

自分の環境で「効く場所」を確定させる手順

最後に、同じ木をリポジトリルートから起動して読まれる集合がどう変わるかを見た。

$ sh from-root.sh
--- 置いた壊れた設定ファイル ---
/tmp/claude/cc-settings-lab/repo/.claude/settings.json
/tmp/claude/cc-settings-lab/repo/.claude/settings.local.json
/tmp/claude/cc-settings-lab/repo/sub/.claude/settings.json
/tmp/claude/cc-settings-lab/repo/sub/.claude/settings.local.json
placed=4
=== リポジトリルートから起動 ===
doctor exit=0
named=2
- /private/tmp/claude/cc-settings-lab/repo/.claude/settings.json › permissions: Expected object, but received undefined
- /private/tmp/claude/cc-settings-lab/repo/.claude/settings.local.json › permissions: Expected object, but received undefined
done
# exit=0 (809 ms)

4つ置いて2つ。ルートの2ファイルが読まれ、sub/ のものは読まれない。ファイルは1つも動かしていない。変えたのは cd した先だけである。

ここまでの実測を1枚にまとめる。縦が起動位置、横がファイル名で、中身は「そのファイルとして何が読まれたか」である。

起動位置 .claude/settings.json .claude/settings.local.json
リポジトリルート ルートのもの ルートのもの
リポジトリの sub/ sub/ のものだけ sub/ とルートの両方
git 管理外の sub/ sub/ のものだけ sub/ のものだけ

表の右上のマスだけが、他と挙動が違う。共有設定は起動ディレクトリに固定され、個人設定だけがリポジトリルートまで遡る。この非対称が「チームに配ったはずの設定が届かない」の正体だった。

ユーザー階層はこの表に入れていない。起動位置ではなく CLAUDE_CONFIG_DIR で決まるので、縦軸が別物になるからである。

2件リポジトリルート3件リポジトリのsub2件git管理外のsub
起動位置ごとに読まれた設定ファイル数

冒頭の問いに戻る。「書いたつもりのファイルが読まれていない」状態は、次の順で判定できる。

  1. 設定が効かないと感じたら、中身ではなく起動ディレクトリを先に疑う。 場所は固定されていない
  2. 疑っている設定ファイルに {"permissions": 1} を一時的に入れて claude doctor を走らせる。 名指しされれば読まれている。されなければ読まれていない
  3. 名指しされなかったら、リポジトリルートに cd して同じことをやる。 そこで名指しされるなら、原因はファイルの場所ではなく起動位置である

判定が終わったら壊した設定を戻すこと。{"permissions": 1} を入れている間は、そのファイルの permissions が丸ごと落ちる。

この器具で分かることには限界がある。分かるのは「そのファイルが読み込み対象になったか」までで、複数のファイルが同じキーを設定したときにどちらの値が採用されたかは測っていない。優先順位は別の実験がいる。管理設定の階層も、書き込み権限がないので触っていない。claude doctor の名指しはエラーがある行に限られるので、正しく書かれた設定については「読まれた」を直接は示せない。壊して初めて見える。

この方法が向かないのは、設定を一時的にも壊せない環境である。共有マシンや、他の人のセッションが同時に走っている環境では、名指しさせるための破損が相手のセッションにも届く。その場合は自分の手元に同じ階層構造のラボを作り、そちらで測ったほうが速い。本記事の実験はすべて /tmp 以下だけで完結していて、実環境の設定には一度も触れていない。

関連書籍