同じリポジトリを、昼はエディタ内のターミナルから、夜は別のシェルから開いています。ある晩、夜のセッションだけが数世代前のモデル名で始まっていることに気づきました。
心当たりを探すと、原因は数か月前の自分でした。モデルを一時的に固定したくて ~/.zshrc に書いた export ANTHROPIC_MODEL=... の一行が、消し忘れたまま残っていたのです。設定ファイルではなくシェル設定に書いたものは、画面のどこにも出てきません。開くたびに黙って注入されるので、気づく手がかりが「なんとなく応答の質が違う」しかありませんでした。
v2.1.236 で ANTHROPIC_DEFAULT_MODEL が追加されたと知ったとき、最初は「これに置き換えれば解決する」と考えました。実際には違いました。二つは役割が別で、片方が残っている限りもう片方は表に出てきません。
二つの環境変数は、置き換えの関係にありません
ANTHROPIC_MODEL は以前からある変数で、設定されている間はセッションのモデルをそれに固定します。ANTHROPIC_DEFAULT_MODEL は v2.1.236 で追加されたもので、新規セッションが開始するモデルを決めます。セッション中に /model で別のモデルを選べば、そちらが優先され、その選択は再起動をまたいで保持されます。
つまり前者は「動かせない指定」、後者は「動かしてよい初期値」です。両方が設定されていれば、固定の側が勝ちます。ここを取り違えたまま新しい変数だけを足しても、始まるモデルは一つも変わりません。
| 指定の場所 | 効く範囲 | セッション中に /model で変えられるか | 忘れやすさ |
|---|---|---|---|
ANTHROPIC_MODEL | 設定されている間の全セッション | 意図した固定なので基本的に変えない | 高い(シェル設定に書くと画面に出ない) |
ANTHROPIC_DEFAULT_MODEL | 新規セッションの開始時のみ | 変えられる。選択は再起動をまたいで残る | 中 |
settings.json の model | そのユーザー/プロジェクト | 変えられる | 低い(ファイルとして見える) |
settings.json の env 内の指定 | そのプロジェクトの全セッション | 固定として振る舞う | 高い(model 欄しか見ない人には気づけない) |
厄介なのは右の2列です。効き方が違うものが同じ「モデル名の文字列」として4か所以上に散らばり、そのうち二つは目に入りにくい場所にあります。私自身、原因を探すのに /status の表示と設定ファイルを何度も往復しました。
いま何がモデルを決めているかを一覧する
必要だったのは「どれが勝つか」を推測する前に、「そもそも何が設定されているか」を一画面で見ることでした。環境変数・ユーザー設定・プロジェクト設定・ローカル設定・シェル設定を横断して拾い、固定と初期値を区別して並べるだけの小さなスクリプトを書きました。
#!/usr/bin/env python3
"""Claude Code の実行モデルを決めうる設定を一覧し、食い違いを報告する。
使い方:
python3 model_sources.py [プロジェクトのルート]
引数を省略するとカレントディレクトリをプロジェクトルートとみなす。
"""
import json
import os
import re
import sys
import unicodedata
# テストしやすいようにホームを差し替えられるようにしておく
HOME = os.environ.get("MODEL_SOURCES_HOME") or os.path.expanduser("~")
# 環境変数は「効き方」が違う。ここを混同すると再起動後の挙動を読み違える。
ENV_VARS = [
("ANTHROPIC_MODEL", "pin", "設定されている間は全セッションでこのモデルを使う"),
("ANTHROPIC_DEFAULT_MODEL", "default", "新規セッションの開始モデル。/model の選択が優先される"),
("ANTHROPIC_SMALL_FAST_MODEL", "aux", "補助処理用。メインの応答モデルは変えない"),
]
# 後ろにあるものほど近い設定(プロジェクト固有 > ユーザー全体)
SETTINGS_FILES = [
("ユーザー設定", os.path.join(HOME, ".claude", "settings.json")),
("プロジェクト設定", os.path.join("{root}", ".claude", "settings.json")),
("ローカル設定", os.path.join("{root}", ".claude", "settings.local.json")),
]
# 「昔書いて忘れている」定番の置き場所
PROFILES = [".zshrc", ".zshenv", ".bashrc", ".bash_profile", ".profile"]
JSONC_COMMENT = re.compile(r"^\s*//")
def load_jsonc(path):
"""// 行コメントを許容して JSON を読む。壊れていても落とさない。"""
try:
with open(path, encoding="utf-8") as fh:
body = "".join(l for l in fh if not JSONC_COMMENT.match(l))
return json.loads(body), None
except FileNotFoundError:
return None, None
except (json.JSONDecodeError, OSError) as exc:
return None, f"読み取り失敗: {exc}"
def collect(root):
found = []
for name, kind, note in ENV_VARS:
value = os.environ.get(name)
if value:
found.append(("環境変数", name, value, kind, note))
for label, template in SETTINGS_FILES:
path = template.format(root=root)
data, error = load_jsonc(path)
if error:
# 壊れた JSON を黙って無視すると「設定したのに効かない」の原因を見失う
found.append((label, path, "-", "error", error))
continue
if not data:
continue
if data.get("model"):
found.append((label, path, data["model"], "config", "設定ファイルの model"))
nested = (data.get("env") or {}).get("ANTHROPIC_MODEL")
if nested:
found.append((label, path, nested, "pin", "settings の env が ANTHROPIC_MODEL を注入"))
for name in PROFILES:
path = os.path.join(HOME, name)
try:
with open(path, encoding="utf-8") as fh:
lines = fh.readlines()
except OSError:
continue
for lineno, line in enumerate(lines, 1):
if "ANTHROPIC_MODEL" in line or "ANTHROPIC_DEFAULT_MODEL" in line:
found.append((
"シェル設定", f"{path}:{lineno}", line.strip(), "shell",
"新しいシェルを開くたびに再注入される",
))
return found
def display_width(text):
"""全角を2桁として数える。日本語ラベルの表を崩さないため。"""
return sum(2 if unicodedata.east_asian_width(ch) in "WF" else 1 for ch in text)
def pad(text, width):
return text + " " * max(0, width - display_width(text))
def main():
root = sys.argv[1] if len(sys.argv) > 1 else os.getcwd()
rows = collect(root)
if not rows:
print("モデルを決めている設定は見つかりませんでした(CLI の既定に従います)")
return 0
width = max(display_width(r[0]) for r in rows)
print(f"{pad('区分', width)} 種別 値 / 場所")
print("-" * 72)
for origin, where, value, kind, note in rows:
print(f"{pad(origin, width)} {kind.ljust(8)} {value}")
print(f"{' ' * width} └ {where} — {note}")
pins = {r[2] for r in rows if r[3] == "pin"}
defaults = {r[2] for r in rows if r[3] in ("default", "config")}
print()
if len(pins) > 1:
print(f"⚠ 強制指定が {len(pins)} 種類あります: {', '.join(sorted(pins))}")
if pins and defaults - pins:
print(f"⚠ 強制指定 {sorted(pins)} があるため、既定値 {sorted(defaults - pins)} は効きません")
if not pins and len(defaults) > 1:
print(f"ℹ 既定値の候補が {len(defaults)} 種類あります(より近い設定が勝ちます)")
return 1 if len(pins) > 1 else 0
if __name__ == "__main__":
sys.exit(main())意図的にそうした点が三つあります。
一つ目は、settings.json の env の中まで見ていることです。model 欄だけを確認して「プロジェクト設定は空だから関係ない」と判断すると、いちばん見つけにくい固定を見落とします。
二つ目は、壊れた JSON を握りつぶさず error として行に出していることです。設定ファイルが構文エラーで読み飛ばされているとき、症状は「設定したのに効かない」になります。原因と症状が離れているので、ここは沈黙させたくありませんでした。
三つ目は、固定が複数見つかったときに終了コード 1 を返すことです。目視で眺めるだけなら不要ですが、個人開発では設定を見直す担当が自分ひとりしかいません。この手のものは、眺めるのをやめた頃に静かに壊れます。
手元で走らせた結果
私の設定を模した一式(ユーザー設定・プロジェクト設定・ローカル設定・シェル設定・環境変数)に対して実行した出力です。
区分 種別 値 / 場所
------------------------------------------------------------------------
環境変数 pin claude-opus-4-1-20250805
└ ANTHROPIC_MODEL — 設定されている間は全セッションでこのモデルを使う
環境変数 default claude-sonnet-5
└ ANTHROPIC_DEFAULT_MODEL — 新規セッションの開始モデル。/model の選択が優先される
ユーザー設定 config claude-sonnet-5
└ /home/me/.claude/settings.json — 設定ファイルの model
プロジェクト設定 config claude-opus-5
└ /work/proj/.claude/settings.json — 設定ファイルの model
プロジェクト設定 pin claude-opus-5
└ /work/proj/.claude/settings.json — settings の env が ANTHROPIC_MODEL を注入
ローカル設定 config claude-haiku-4-5-20251001
└ /work/proj/.claude/settings.local.json — 設定ファイルの model
シェル設定 shell export ANTHROPIC_MODEL="claude-opus-4-1-20250805"
└ /home/me/.zshrc:2 — 新しいシェルを開くたびに再注入される
シェル設定 shell export ANTHROPIC_DEFAULT_MODEL="claude-sonnet-5"
└ /home/me/.bash_profile:1 — 新しいシェルを開くたびに再注入される
⚠ 強制指定が 2 種類あります: claude-opus-4-1-20250805, claude-opus-5
⚠ 強制指定 ['claude-opus-4-1-20250805', 'claude-opus-5'] があるため、既定値 ['claude-haiku-4-5-20251001', 'claude-sonnet-5'] は効きません読み方は単純です。8行のうち、実際に始まるモデルを決めているのは上から1行目だけで、残り7行のうち4行は書いた本人が「効いているはず」と思っている指定です。ANTHROPIC_DEFAULT_MODEL も、ユーザー設定の model も、ローカル設定の model も、この状態では一度も参照されません。
シェル設定の行から ANTHROPIC_MODEL を消し、プロジェクト設定の env からも外すと、同じスクリプトの出力はこうなります。
区分 種別 値 / 場所
------------------------------------------------------------------------
環境変数 default claude-sonnet-5
└ ANTHROPIC_DEFAULT_MODEL — 新規セッションの開始モデル。/model の選択が優先される
ユーザー設定 config claude-sonnet-5
└ /home/me/.claude/settings.json — 設定ファイルの model
プロジェクト設定 config claude-opus-5
└ /work/proj/.claude/settings.json — 設定ファイルの model
ローカル設定 config claude-haiku-4-5-20251001
└ /work/proj/.claude/settings.local.json — 設定ファイルの model
シェル設定 shell export ANTHROPIC_DEFAULT_MODEL="claude-sonnet-5"
└ /home/me/.bash_profile:1 — 新しいシェルを開くたびに再注入される
ℹ 既定値の候補が 3 種類あります(より近い設定が勝ちます)警告が「食い違い」から「候補が複数ある」に変わりました。この状態なら、どれが選ばれても /model で上書きできます。固定が消えた時点で、設定は読み手にとって理解可能なものに戻ります。
なお、このスクリプトが答えるのは「何が設定されているか」までです。「結局どれが勝つのか」は、claude を起動して /status の表示で確かめるのが確実です。バージョンによって優先順位の細部が変わる可能性がある以上、一覧と実測は分けておいたほうが安全だと考えています。
私の設定をどこに落ち着かせたか
個人開発で iOS と Android のアプリを並行して触っていると、リポジトリごとに求めるモデルが違ってきます。既存コードの読み解きが中心のプロジェクトと、まっさらな画面をゼロから組むプロジェクトでは、同じモデルが最適とは限りません。それでも指定の置き場所は減らしたほうがよい、というのが今回の結論でした。
何度か入れ替えてみて、私はこの形に落ち着きました。
- シェル設定からは、モデル指定を完全に消す。 見えない場所で効くものを一つも残さない、というのが最優先でした
- 常用モデルは
ANTHROPIC_DEFAULT_MODELに一本化する。 初期値として置き、その日の作業に応じて/modelで動かします - プロジェクトごとの違いは
.claude/settings.jsonのmodelに書く。 ファイルとして残るので、半年後の自分にも見えます - 固定したい場面では、そのコマンドの前だけに付ける。 恒久設定にせず
ANTHROPIC_MODEL=claude-haiku-4-5-20251001 claude -p "..."の形で寿命をコマンド1回に閉じ込めます
4番目が今回いちばん効きました。固定は必要な道具ですが、寿命を長くした瞬間に「忘れられる設定」に変わります。プロファイルに書くのではなく実行するコマンドに添えるだけで、後から原因を追う必要がほぼなくなりました。
モデルの選び分けそのものについては、Claude Code のモデル選択戦略 に用途別の考え方をまとめています。無人で走らせるジョブがどのモデルで実行されたかを記録として残す方法は、昨夜の無人セッションはどのモデルで走ったのか で扱いました。
変更したあとに確かめる三つのこと
設定を触ったら、その場で次の三つを通しておくと、後から悩む時間がなくなります。
まず、新しいシェルを開いて env | grep ANTHROPIC を実行します。編集中のシェルには古い値が残っているので、必ず開き直してから確認します。ここで出てくるものが、次のセッションに持ち込まれるものです。
次に、claude を起動して /status で実行中のモデルを見ます。一覧と実測が一致していれば、その設定は自分の理解どおりに効いています。
最後に、スクリプトを終了コードごと使う場所に置きます。私は無人で走らせるジョブの前処理に入れて、固定が二つ以上見つかったらジョブを止めるようにしました。クレジットの上限管理まで含めて固定を仕組みとして扱いたい場合は、enforceAvailableModels で実行モデルを固定する のほうがより踏み込んだ設計を書いています。
今日できる最初の一歩としては、grep -rn ANTHROPIC_MODEL ~/.zshrc ~/.bashrc ~/.profile を一度走らせてみてください。何も出なければ、少なくとも見えない固定は存在しません。何か出てきたら、それがいま実際にモデルを決めています。
お読みいただきありがとうございました。設定の整理は地味な作業ですが、原因の分からない挙動が一つ減るだけで、翌日の集中の質がずいぶん変わると感じています。