Claude CodeでHydraの設定合成エラーを直す — --cfg jobで差分を確かめる
Hydraのdefaults listとoverrideが食い違ったとき、--cfg jobと--resolveで合成後の設定を出してClaude Codeに読ませる手順と、conf/の編集直後に合成だけ走らせるhookの書き方です。
Hydraの設定エラーは、conf/のYAMLを読んでもどこが悪いのか分からないことが多いエラーです。原因は個々のファイルではなく、defaults listとコマンドラインのoverrideが組み合わさった合成後の設定にあるからです。Claude Codeに直させるなら、ファイルを読ませる前に--cfg jobで合成結果を出させるのが最短です。
以下の出力は、Hydra 1.3.8とOmegaConf 2.3.1を空の仮想環境に入れた小さなアプリで、エラーを意図的に起こして得たものです。ネットワーク上のサービスやモデル呼び出しは使っていません。
合成後の設定を先に出させる
Hydraは--cfgで、関数を実行せずに設定だけを表示します。引数はjob(自分の設定)、hydra(Hydra自身の設定)、all(両方)です。補間(${...})は既定では展開されず、--resolveを足すと展開済みの値が出ます。
検証に使った構成は次のとおりです。
conf/
config.yaml
db/mysql.yaml # host: localhost / port: 3306
db/postgres.yaml # host: pg.local / port: 5432# conf/config.yaml
defaults:
- db: mysql
db:
host: backup
timeout: ${db.port}python my_app.py --cfg job --resolve出力は次のようになりました。
db:
host: backup
port: 3306
timeout: 3306db/mysql.yamlにはhost: localhostがあるのに、結果はbackupです。公式のdefaults listの説明では、_self_を書かないと自分自身は末尾に追加されるので、config.yamlの値がdb/mysqlを上書きします。この版では、_self_が無いことを知らせる警告もstderrに出ました。
defaultsの先頭に_self_を置くと順序が逆転します。同じコマンドで確かめた結果は次のとおりです。
db:
host: localhost
timeout: 3306
port: 3306hostはlocalhostに戻り、portの並びも変わりました。キーの並びが変わっただけで、値の意味は変わりません。
Claude Codeへの渡し方
エラー文だけ貼るより、次の3点を一緒に渡すほうが原因に近づけます。
- 失敗したコマンドの全文(overrideを含む)
- 同じコマンドに
--cfg job --resolveを足した出力、もしくはその失敗出力 --info defaultsの出力(合成順序を表で見られる)
--cfgの引数はjobが基本です。出力先ディレクトリやjob名など、Hydra自身の設定が疑わしいときだけ--cfg hydraを使い、両方を並べて見たいときは--cfg allにします。自分の設定が原因のときにhydra側まで渡すと、読ませる量だけが増えます。出力が長いときは--package(短縮形-p)で一部だけを表示できます。たとえば--cfg hydra --package hydra.jobなら、hydra.jobの下だけが出ます。
--info defaultsは、設定ファイルごとに「Config path / Package / _self_ / Parent」を表で返します。db/mysqlがconfigより先に並んでいれば、後ろのconfigが勝つ、と読めます。同じ値を複数の設定ファイルが定義しているとき、最後のものが勝つというのが公式の説明です。
CLAUDE.mdには、手順を次のように書いておきます。
## Hydra設定のデバッグ
- 設定エラーでは、まず `python my_app.py <同じoverride> --cfg job --resolve` を実行する
- 値の出所が分からないときは `--info defaults` で合成順序を見る
- YAMLを推測で書き換える前に、合成後の値と期待値の差分を文章で示す
- 例外名やスタックトレースが必要なときは、同じコマンドの前に `HYDRA_FULL_ERROR=1` を付けて再実行する3行目が効くのは、差分を言葉にさせると、_self_の位置やoverrideの食い違いのような「順序の問題」に気づきやすいためです。
エラーの型ごとに出るもの
手元で出した3種類を、何を見ればよいかと一緒に並べます。
| 起こしたこと | 出たメッセージ | 見る場所 |
|---|---|---|
db=oracle(存在しない選択肢) | 出たメッセージIn 'config': Could not find 'db/oracle' と、dbの選択肢(mysql、postgres)の一覧 | 見る場所選択肢の一覧とconfig search path |
db.hots=x(キーの打ち間違い) | 出たメッセージCould not override 'db.hots'. に続き To append to your config use +db.hots=x | 見る場所キー名。新規追加なら+を付ける |
補間先のキー名の誤り(${db.prot}) | 出たメッセージ--cfg jobは成功、--cfg job --resolveだけがInterpolation key 'db.prot' not foundで失敗 | 見る場所--resolveを付けたか |
1行目の例外名はHYDRA_FULL_ERROR=1を付けると表示され、この版ではMissingConfigExceptionでした。メッセージの最後にも、スタックトレース全体を見るにはこの環境変数を設定するよう案内が出ます。
3行目は見落としやすい点です。--resolveを付けない--cfg jobは、補間先のキーが存在しなくても終了コード0で通りました。補間まで検査したいなら--resolveが必須です。
defaults listが食い違う3つの形
選択肢の名前を間違えたわけではないのに合成が落ちるときは、defaults listの書き方かoverrideの付け方が食い違っています。手元で再現できた3つを、直し方と一緒に並べます。
同じグループを2回選んでいる
defaultsにdb: mysqlがあるのに、コマンドラインで+db=postgresと追加すると、次のエラーで止まります。
Multiple values for db. To override a value use 'override db: postgres'defaultsの中にdb: mysqlとdb: postgresを並べた場合も同じメッセージでした。選択肢を差し替えたいときは、コマンドラインならdb=postgres、YAMLの中ならoverride db: postgresを使います。+は「defaults listに無いものを足す」記法なので、すでにあるグループには使えません。公式のoverride文法の説明でも、db=mysqlが選択肢の上書き、+db=mysqlがdefaults listへの追加と区別されています。
同じ文法表には、ほかの操作も載っています。
| 書き方 | 意味 |
|---|---|
db=mysql | 意味defaults listの選択肢を上書き |
+db=mysql | 意味defaults listに追加 |
~db | 意味defaults listからdbを削除 |
++db.host=x | 意味設定値を、無ければ追加・あれば上書き |
db.host=xが「キーが無い」と落ち、+db.host=xが「すでにある」と落ちるときは、++なら存在の有無を気にせず通ります。Claude Codeに「とりあえず++にして」と直させると原因を隠すので、まず--cfg jobで値の有無を確かめてから選ぶ形にします。~dbは、グループごと外してdbの設定を持たない構成を試すときに使えます。
入れ子のdefaultsが親のグループ名を引き継ぐ
conf/server/apache.yamlの中で- db: mysqlと書くと、探されるのはdb/mysqlではなくserver/db/mysqlです。実際に出たエラーは次のとおりです。
In 'server/apache': Could not find 'server/db/mysql'- /db: mysqlのように先頭にスラッシュを付けて絶対パスにすると解決しました。親ディレクトリをたどる../dbのような書き方は、defaults listのパスでは使えません。公式の構文表にも、..は対応しないと書かれています。このとき--cfg jobの結果はserver.dbの下に入ります。公式のdefaults listの説明に、server/db/mysqlをserver.dbパッケージとして並べた表があり、同じ構造です。
オプションの上書きが別のパッケージに向いている
上の構成でdb=postgresを渡すと、dbはserver/dbではないので、次のように案内されます。
Could not override 'db'.
Did you mean to override db@server.db?
To append to your default list use +db=postgresHydraがdb@server.dbと候補を出してくれるので、この行はそのままClaude Codeに読ませられます。--info defaults-treeで「どのグループがどの親の下にあるか」を見ると、候補の意味が確認できます。
3つとも、YAMLの文法は正しいのに起きるエラーです。ファイルだけでは原因が見えないので、--info defaults-treeと--cfg jobの出力を一緒に渡します。
エラーが出ないことが原因になるoptional
逆に、期待した設定が入らないのにエラーも出ない場合は、defaultsにoptionalが付いていないか確かめます。公式の構文表では、既定では存在しない選択肢はエラーになり、optionalを付けるとそのエラーが抑えられます。
defaults:
- optional db: mysqlこの書き方だと、db/mysql.yamlの置き場所や名前が違っていてもエラーにならず、グループのファイルが読み込まれないまま合成が進みます。dbの値をグループのファイルだけで定義している構成では、dbがまるごと欠けます。上の例のようにconfig.yamlにもdbを書いていると、そちらの値だけが残ります。Could not findが出ない代わりに、--cfg jobの出力でグループ由来のキーが揃っているかを、Claude Codeには最初に見させます。
似た挙動にnullがあります。- db: nullは後からoverrideで埋めるための場所取りで、埋められなければそのエントリは無視されます。db=mysqlを付け忘れても落ちず、グループ由来の値が入らない設定が出てきます。dbをグループのファイルだけで定義している構成なら、dbが欠けた設定になります。再現コマンドにoverrideを含めるよう頼むのは、この型を見分けるためでもあります。
conf/の編集直後に合成だけ走らせるhook
設定を書き換えさせる運用では、書いた直後に合成が通るかを返すと、壊れたYAMLを次の手に持ち越さずに済みます。ここではPostToolUseを使います。公式のhooksリファレンスでは、PostToolUseはツール成功の直後に動き、終了コード2のときはstderrがClaudeに渡されます。ツールは実行済みなので、書き込み自体は止められません。
.claude/settings.jsonには次のように登録します。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/hydra-compose-check.sh"
}
]
}
]
}
}スクリプト側では、標準入力のJSONからtool_input.file_pathを取り、conf/配下のYAMLだけを対象にします。
#!/bin/bash
# .claude/hooks/hydra-compose-check.sh
input=$(cat)
file=$(jq -r '.tool_input.file_path // empty' <<<"$input")
case "$file" in
*/conf/*.yaml|*/conf/*.yml) ;;
*) exit 0 ;;
esac
cd "$CLAUDE_PROJECT_DIR" || exit 0
if ! out=$(HYDRA_FULL_ERROR=1 python my_app.py --cfg job --resolve 2>&1); then
echo "$out" | tail -n 15 >&2
exit 2
fi
exit 0壊れたYAML(mysql.yamlの2行目をインデントずれにした状態)でこのコマンドを走らせると、終了コードは1で、末尾がyaml.scanner.ScannerError: mapping values are not allowed hereとファイル名、行、列になりました。補間キーの誤りを入れた状態でも終了コードは1です。どちらもexit 2に変換してClaude Codeへ返せます。
HYDRA_FULL_ERROR=1を付けているのは、例外の種類までClaude Codeに返すためです。そのぶんトレースバックが長くなるのでtail -n 15で切ります。先頭はHydraの内部パスで埋まりますが、原因は末尾にあります。
この構成で拾えないもの
- 対象は引数なしの合成です。
db=postgresのようなoverrideつきで起きる食い違いは拾えないので、次の節のテストに任せます - マルチラン(
-m)の掃引で初めて出る組み合わせは対象外です - アプリが
config_nameを複数持つ場合は、ファイルの場所から対象を決める処理が要ります
overrideの組み合わせはテストで受け持たせる
hookは引数なしの合成しか見ません。よく使うoverrideの組み合わせは、Compose APIで書いたテストに任せるほうが確実です。composeはoverridesを受け取り、@hydra.main()を通さずに合成だけを実行します。
# tests/test_compose.py
import pytest
from hydra import compose, initialize
@pytest.mark.parametrize("overrides", [[], ["db=postgres"]])
def test_compose(overrides):
with initialize(version_base=None, config_path="../conf"):
cfg = compose(config_name="config", overrides=overrides)
assert cfg.db.portdefaultsにdb: mysqlがある構成でoverrides=["+db=postgres"]を渡すと、hydra.errors.ConfigCompositionException: Multiple values for db. To override a value use 'override db: postgres'が上がりました。コマンドラインで見たメッセージと同じ内容が、例外として届きます。pytestの失敗出力はそのままClaude Codeに読ませられるので、組み合わせを増やしたいときはparametrizeの行を足すだけで済みます。
hookは「書いた直後に素早く気づく」、テストは「組み合わせを網羅して残す」役割です。hookのコマンドをpytest tests/test_compose.py -qに置き換える手もありますが、テストが増えると編集のたびに待つ時間が伸びます。
つまずきやすい点
--cfgの出力をそのまま信じない: コマンドラインのoverrideも反映されるので、再現コマンドと同じoverrideを付けないと別の設定を見ることになります- 警告を無視しない:
_self_が無いとの警告は、値の優先順位に直結する情報です Hydraとhydrationは別物: ReactのText content does not matchのような描画エラーは、Claude Codeでhydrationエラーを追うで扱っています
同じ「編集のたびに検証コマンドを走らせ、失敗出力をClaude Codeへ返す」構成は、ESPHomeのyaml設定でも使われています。ビルドエラーを返すhookの書き方はGodotのGDExtensionの例が参考になります。
まとめ
Hydraの合成エラーは、ファイル単体を読むより--cfg job --resolveで合成後の値を出し、期待値との差分を言葉にさせたほうが早く原因に届きます。_self_の位置は値の優先順位を変え、--resolveを付けないと補間先の誤りは通ってしまいます。PostToolUseのhookに同じコマンドを入れれば、conf/を書き換えた直後に失敗が戻ります。ただし、overrideつきの組み合わせや掃引は別途確かめる必要があります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。