深夜、レビュー待ちのPRがなかなか終わらない。ログを覗いたら、ただの typo 修正のためだけに走らせたはずのサブエージェントが、涼しい顔で Opus の xhigh を使っていた。「え、これだけの作業にそんな気合いらないのに」と思わず声が出る(正直、悪いのはサブエージェントではなくこちらの設定なのだが)。
Claude Code(Pro $20/月、Max $100〜$200/月)の effortLevel は、モデルにどれだけ考えさせるかを low / medium / high / xhigh / max の5段階で決める設定だ。「重い判断には高いeffortを、軽い作業には低いeffortを」という原則自体はシンプルで、たいていの人がすぐ理解する。ところが Workflow やサブエージェントを組み始めた瞬間、この原則は静かに壊れる。effort を指定し忘れたサブエージェントは、勝手に軽くなってくれるわけではなく、親セッションの設定をそのまま引き継ぐからだ。
この記事では、settings.json の effortLevel フィールド自体の基本は別記事に譲り、Workflow/サブエージェント運用で実際に踏んだ「継承の罠」と、その対処として組んでいる仕組みを掘り下げる。
この記事で学べること
- サブエージェントに
model/effortを明示しないと何が起きるか(継承の実態) - skill や agent の定義に書く
effortの値を、書き込み時点で機械的に弾くガードの作り方 - 実際に
effortLevelの既定値を上げ下げした判断と、それを読み返せる形で残す方法 - Claude Code に限らず、他のエージェント型コーディングツールにも同じ設計軸があるという話
前提条件
- Claude Code で Skill や Task ツール経由のサブエージェント呼び出しをすでに使っている
settings.jsonのeffortLevelを一度は触ったことがある(触ったことがなければ上記の別記事を先に読むとつながりやすい)
settings.json の該当箇所はこういう見た目をしている。
{
"effortLevel": "high"
}
トップレベルに置くだけの短い設定項目だが、これが「オーケストレーションの重さの既定値」を決めている、という自覚があるかどうかで運用の安定度がかなり変わる。
継承の罠 — 指定しないサブエージェントは軽くならない
Workflow や Skill からサブエージェントを呼ぶとき、model と effort を毎回明示するのは正直めんどうだ。省略すれば動くし、省略しても一見エラーにならない。だから省略しがちになる。
問題は、省略したときの挙動が「ちょうどいい中間値になる」ではなく「親セッションの設定をそのまま引き継ぐ」ことだ。自分たちのオーケストレーション運用ルールでは、これを次のように名付けている。
Inherit ≠ Default-Heavy:
agent()は明示しない限りセッションモデルを継承する。軽ステージに指定をサボると全部 opus xhigh になる
つまり、親セッションが opus の xhigh で動いているときに、フォーマット変換やログ集計のような軽い処理を「とりあえずサブエージェントに投げる」と、そのサブエージェントも opus の xhigh で動く。中身は jq で数行のJSONを整形するだけの処理だとしても、だ(爪切りを頼んだらチェーンソーを渡されるようなものだが、渡された側は律儀にチェーンソーで爪を切りにいく)。
図にするとこうなる。
対処は単純で、呼び出す側で model と effort を明示するだけだ。
// Bad: 何も指定しない → 親セッションの opus/xhigh をそのまま継承する
{ "subagent_type": "general-purpose", "prompt": "このJSONをキー順に整形して" }
// Good: 軽量・機械的な作業だと分かっているなら明示する
{ "subagent_type": "general-purpose", "model": "haiku", "effort": "low",
"prompt": "このJSONをキー順に整形して" }
自分たちのソフトウェア原則では、これを一段抽象化して次のように書いている。
Right-Sized Compute: 軽量・機械的(web search, grep集約, mechanical edit)→
model: haiku, effort: low/ 重い判断のみ(verify/judge/synthesize)→ opus + 上位 effort
原則自体は当たり前に聞こえる。だが「継承がデフォルトの安全側ではなく重い側に倒れる」という挙動を知らないと、この原則は宣言しただけで実践されない。呼び出し側が毎回、軽い作業を見つけるたびに model / effort を書く手間を惜しまない、という運用でしか守れない原則だからだ。
壊れた値を機械的に弾くガード
effort は skill や agent の定義(フロントマター)にも書ける。ここで踏みやすいのが typo だ。hihg と打っても、xtrahigh と打っても、目視レビューでは案外気づかない(人間の目は「だいたい合ってる文字列」を勝手に補完して読んでしまう、無駄に優秀な機能を持っている)。
これを人間の注意力に頼らず機械的に弾くために、フロントマターを書き込む直前に検証する PreToolUse フックを運用している。中身はシンプルな enum チェックだ。
# effort フィールドの値を閉じた enum で検証する(PreToolUseフックの中核部分)
EFFORT=$(echo "$FRONTMATTER" | grep -E '^effort:\s*' | sed 's/^effort:\s*//' | xargs)
if [[ -n $EFFORT ]]; then
case "$EFFORT" in
low | medium | high | xhigh | max) ;;
*) ERRORS+=("Invalid effort: '${EFFORT}'. Must be one of: low, medium, high, xhigh, max") ;;
esac
fi
同じフックで model(haiku|sonnet|opus)や context(fork)のような他の frontmatter フィールドも同じ形で検証している。ポイントは、「正しく書く」ことを執筆者の注意力に委ねるのではなく、「間違った値は保存できない」という層を1枚挟むこと。これは前段の継承の話とワンセットで効いてくる。継承は「明示しないと重くなる」問題で、こちらは「明示したつもりが typo で無効化される」問題だ。どちらも運用の緊張が緩んだ瞬間に起きる。
実際に effortLevel の既定値を書き換えた判断
effortLevel の値そのものは、一度決めたら固定するものではない。実際に自分たちの settings.json でも、この数ヶ月で両方向の変更をしている。
1つ目は、JSONファイルをアルファベット順キーソートで自動整形するフォーマッタを settings.json にも適用したときの出来事だ。Claude Code 本体の書き戻しでキー順が崩れて diff が全行差分になる問題を解消するのが目的だったが、その整形作業に混ぜる形で、実質的な意味変更として effortLevel を xhigh から high に、既定モデルを未指定から sonnet に変えている。
これは、フォーマット変更と意味変更を同じコミットに混ぜると、後から「結局どこが本質的な変更だったのか」を diff から読み取れなくなる、という失敗の実例でもある。全行差分の中から「2箇所だけ意味のある変更」を見つけるのは、レビューする側にとって針を探すような作業だ。設定ファイルの整形を自動化するときは、意味変更を混ぜず別コミットに分離しておくべきだった、というのが振り返っての教訓になる。
2つ目は逆方向で、既定モデルを sonnet から opus に上げた変更だ。こちらはシンプルな1点だけの変更として独立してコミットしている。
| 変更 | 内容 | コミットの粒度 |
|---|---|---|
| 整形と同時変更 | effortLevel: xhigh→high、既定modelをsonnetに | 整形との混在で意味変更が埋もれた |
| 単独変更 | 既定modelをsonnet→opusに | 独立した1コミットで意図が読める |
同じ設定項目でも、「なぜ変えたか」を後から diff 1箇所で説明できる粒度と、フォーマット変更の海に埋もれて説明できない粒度がある。effort や model の既定値は触る頻度がそう高くないぶん、変えるときこそ独立したコミットにしておく価値がある。
余談だが、同じ時期に別の設定でも「揃っているはずの値が実はズレていた」という不具合が見つかっている。ブランチ削除の自動化コマンドが2つあり、片方は保護対象ブランチのパターンに dev を含めていたが、もう片方は含めておらず、dev ブランチが削除対象に紛れ込むバグになっていた。原因は単純な設定漏れで、両者を揃えることで直っている。effort の既定値も同じ構造の事故が起きやすい。複数の設定ファイルやレイヤーに同じ意図の値がバラバラに散らばっていると、どこか1箇所だけ古いまま取り残される(「揃えたはず」は、設定ファイルの世界で一番信用してはいけない自己申告だ)。
Claude Code だけの話じゃない
effort という名前の設定はClaude Code固有だが、「モデルにどれだけ考えさせるか」というダイヤル自体は、エージェント型のコーディングツール全般に共通する設計軸だ。たとえば Codex CLI にも同じ発想の設定がある。
# Codex の設定例(本記事の趣旨に沿って一部抜粋)
model_reasoning_effort = "xhigh"
呼び方が違うだけで、やっていることは同じ「推論の深さと引き換えに時間とコストを使う」というトレードオフだ。
| ツール | 設定名 | 値の例 |
|---|---|---|
| Claude Code(settings.json) | effortLevel | low / medium / high / xhigh / max |
| Claude Code(skill/agent frontmatter) | effort | 同上 |
| Codex CLI(config) | model_reasoning_effort | xhigh など |
複数のエージェント型ツールを併用している場合、この対応関係を頭に入れておくと、「片方では effort を細かく使い分けているのに、もう片方はデフォルトのまま」という設定ドリフトに気づきやすくなる(気づいたときには、もう半年くらい放置していた、というのがこの手の話のお決まりのオチだ)。
「上げれば安全」への反証 — 実務チェックリスト
effort を上げるほど品質が上がるなら、迷ったら全部 max にすればいい理屈になる。実際にはそう単純ではない。サブスクの5時間ローリングウィンドウ(Anthropic公式のプラン仕様)は有限で、max を多用すると軽い調査でも消費が早まり、肝心の重い判断をしたい場面で枠が残っていない、という本末転倒が起きる(軽いタスクでクレジットを使い切って、本番前に財布が空になっている、というのはサブスク運用あるあるの中でもわりと洒落にならない部類だ)。
もう一つ見落としがちなのが、「effort を上げること」と「機械的な処理を確実にすること」は別の解決策だという点だ。ある時期、AIエージェントがファイル全読み・テキストgrepに頼らず、専用CLIでコンテキストの圧迫を減らせるようにする、というルールを整備したことがある。構文認識の一括置換にはast-grep、コードベースの規模把握にはtokei、未知のJSON構造の探索にはgronとripgrepの組み合わせ、という具合に、用途ごとに決定論的なツールへ誘導するルールだ。これは「機械的な作業を、より高いeffortのモデル推論で頑張らせる」のではなく、「そもそも推論に頼らず確定的に処理する」という別解になっている。effortを上げる前に、「これは本当に考えさせる必要がある処理か、決定論的なツールに置き換えられないか」を先に疑う価値がある。
もう一つ、「安全策を入れたつもりが、実際には機能していなかった」という事例もある。dev-flowの運用を段階的に信頼性の高いモードへ昇格させる仕組みで、実際の稼働ログを送る配線が一部抜けていたため、直近30日分の該当する実行すべてで実測データが1件も届いていなかった、ということがあった。「shadowモードで様子を見てから昇格する」という設計自体は正しくても、配線が切れていれば判定材料そのものが存在しない(「見てから決める」つもりが、実は何も見えていなかった、というのは笑えないオチのほうだ)。effort や model のような重要度の判断も同じで、「上げておけば安心」という感覚ではなく、実際にログや実測値で裏取りできているかを確認する癖が要る。
ここまでを実務チェックリストにするとこうなる。
- サブエージェントを呼ぶコードで
model/effortを明示しているか。指定漏れは親セッションの重い設定を継承する前提で洗い出す - 機械的な処理はまず専用CLIやスクリプトへの置き換えを検討し、それでも足りない部分にだけ高いeffortを充てる
- skillやagentのfrontmatterに
effortを書くなら、typoや不正値を保存前に弾く検証を用意する effortLevelのような既定値を変えるときは、フォーマット変更や他の修正と混ぜず、意味変更だけのコミットにする- 「安全になったはず」の変更は、実際にログや実測値で確認できているかを確認する。確認できないなら、それはまだ安全になっていない
maxを使う場所は「考えの浅さが品質を直接壊す」箇所に絞り、ルーティン処理には使わない
動作確認
自分の環境で、まずは既定値がどこにどう置かれているかを確認できる。
# settings.json のトップレベルに effortLevel があるか確認する
jq '.effortLevel' ~/.claude/settings.json
次に、サブエージェントを呼んでいる自分のコードやワークフロー定義から、model / effort の指定漏れを洗い出す。呼び出し方はプロジェクトごとに違うので、パターンは自分の実装に合わせて調整してほしい。
# エージェント呼び出しを検索し、model/effort を指定していない行を抽出する例
grep -rn "subagent_type" your-workflow-definitions/ | grep -v "model|effort"
最後に、frontmatterのenumガードは、自分のプロジェクトでもすぐ試せる。次のスクリプトを保存して、xhigh と super-high の2つの入力でそれぞれ実行してみると、閉じたenumでない値だけが弾かれる挙動を確認できる。
#!/usr/bin/env bash
# validate-effort.sh: 引数で受け取ったeffortの値を閉じたenumで検証する
echo "$1" | grep -qE '^(low|medium|high|xhigh|max)$' && echo "OK: $1" || { echo "NG: '$1' is not a valid effort tier"; exit 1; }
bash validate-effort.sh xhigh # OK: xhigh
bash validate-effort.sh super-high # NG: 'super-high' is not a valid effort tier
これをそのままフックの入り口に置けば、typoの混入をレビュー前に止められる。
まとめ
effort は「賢さと引き換えにコストと時間を使うダイヤル」というシンプルな設計だが、エージェントに仕事を委任する構成では話がもう一段複雑になる。指定しなければ軽くなるのではなく、呼び出し元の重さをそのまま受け継ぐ。だからこそ、呼び出す側が明示する運用と、書いた値が壊れていないか機械的に確かめる層の両方が要る。
これはClaude Codeに限った話ではなく、Codexのようなツールにも同じ設計軸がある。あなたが使っているエージェント基盤でも、サブエージェントやワークフローの呼び出しでモデル・推論深度をどう継承しているか、一度確認してみる価値はあるはずだ。設定ファイルにダイヤルが1つあるだけなら見落としやすいが、それが積み重なった先で、深夜のちょっとした整形作業がなぜか一番重いモデルで動いている、という状況になる。



