Cloudflare Workers へのデプロイが、ある日突然「エントリポイントが見つからない」で止まりました。wrangler.toml を開くと main は確かに書いてあります。スペルも合っています。パスも存在します。それでも通りません。
原因は書き方ではなく、書いた場所でした。[build] セクションを追記したあとに main を足していたため、TOML の規則どおり main は build テーブルの中の項目として読まれていたのです。ファイルの上では正しく、パーサから見ると別物でした。
このときエラーメッセージは「main を書き足してください」と案内してきます。すでに書いてあるものを書けと言われる状況ほど、原因から遠ざかる指示はありません。
個人開発では、こうした一件が半日を持っていきます。そして厄介なのは、同じ種類の食い違いが、もっと静かな形で日常に紛れ込んでいることです。
書いた場所で意味が変わる、という失敗の再現
先ほどの構成を最小化して再現しました。wrangler 4.125.0 で確認しています。
name = "probe-order"
compatibility_date = "2025-05-05"
[ build ]
command = "node scripts/stamp.mjs"
main = "index.js"
compatibility_flags = [ "nodejs_compat" ]
これを dry-run に通すと、次の2つが同時に出ます。
▲ [WARNING] Processing wrangler.order.toml configuration:
- Unexpected fields found in build field: "main","compatibility_flags"
✘ [ERROR] Missing entry-point to Worker script or to assets directory
警告のほうが真犯人を名指ししています。ところが視線は赤い ERROR に吸い寄せられ、main を疑わない方向へ進みます。私自身、このとき最初に見たのはエントリポイントのパスでした。
対処そのものは単純で、トップレベルの項目を [build] より前へ移すだけです。復旧してから残ったのは、「設定ファイルは、書いた内容と読まれた内容がずれても教えてくれないことがある」という感触でした。
誤った設定キーは、3つの層に分かれる
その感触を確かめるため、手元でよく使うツールに同じ種類の誤りを入れて、反応を並べました。実行環境は Node 22.23.2 の Linux、2026年8月25日時点の各最新系です。
ツール 入れた誤り 反応 終了コード
TypeScript 5.9.3 strictNullCheck(正: strictNullChecks)TS5025 で停止。正しい候補まで提示 2
TypeScript 5.9.3 include を compilerOptions の中へTS5023 で停止 2
ESLint 9.39.5(flat config) 未知のキーを2つ追加 ConfigError で即中断 2
Wrangler 4.125.0 compatability_flags(綴り違い)WARNING を出すが処理は続行 0
npm 10.9.8 depedencies(綴り違い)無警告。「up to date」と表示して0パッケージ 0
Vitest 3.2.7 無効キーを3つ同時に追加 無警告。全テスト通過 0
同じ「1文字の綴り違い」でも、止まるものと、素通りするものへ綺麗に割れました。試した6件のうち、終了コード 0 で通過したものが3件、ちょうど 50% あります。半分は、CI から見れば成功として記録される誤りだったということです。
npm の例が個人的にはいちばん堪えました。dependencies を depedencies と打ち間違えた package.json に対して npm install は 327ms で完了し、「up to date」と表示し、終了コード 0 を返し、node_modules を作りませんでした。依存が1つも入っていない状態が、成功として記録されます。
同じファイルで scripts を scripst と間違えた場合は違いました。npm run build が「Missing script: build」で即座に落ちます。呼び出す側があるキーは声を上げ、宣言するだけのキーは黙る、という分かれ方です。
静かな層は、テストの緑まで巻き込む
Vitest の結果は、もう少し踏み込んで確かめました。次の設定には3つの誤りが入っています。
import { defineConfig } from 'vitest/config' ;
export default defineConfig ({
test: {
include: [ 't/**/*.test.ts' ],
timeout: 10 , // 正しくは testTimeout
retires: 3 , // 正しくは retry
coverageThreshold: 95 , // 存在しないキー
} ,
}) ;
対象のテストは 800ms 眠るだけのものです。timeout: 10 が効いていれば当然落ちます。
✓ t/a.test.ts (1 test) 804ms
Test Files 1 passed (1)
804ms かけて通過しました。警告は1行も出ていません。同じテストを testTimeout: 10 に直して走らせると、今度は Test timed out in 10ms. で落ちます。
つまりこの設定ファイルは、書き換える前と後で挙動が完全に同一でした。差分は残り、レビューは通り、テストは緑で、実際には何も変わっていません。制限時間を入れたつもりのリポジトリが、無制限のまま動き続けます。
タイムアウトの設定は「普段は何も起きない」種類の設定です。効いていないことに気づく機会は、実際に暴走が起きた日にしか訪れません。その日に気づくのでは遅い、というのがこの層の性質です。
エージェントに設定を任せると、この差が効いてくる
Antigravity のエージェントに設定変更を任せる場面を考えます。「テストのタイムアウトを 10 秒にしてください」と頼めば、エージェントは設定ファイルを開き、それらしいキーを足し、差分を見せ、完了を報告します。
報告は嘘ではありません。ファイルは確かに書き換わっています。ただし、そのキーが実行時に読まれる保証はどこにもありません。エージェントが参照した資料が古い版のものだったり、隣接するツールのキー名と混ざったりすれば、静かな層のツールは何も言わずに受け取って捨てます。
人間が手で書く場合との差は、量と速度にあります。半日で20か所の設定を触る運用に移行すると、「書けたが効いていない」箇所の絶対数が増えます。そして増えたことに気づく手がかりが、静かな層には存在しません。
ここで有効な考え方は、エージェントの出力を疑うことではありません。設定ファイルの内容を検証するのをやめて、設定の効きを検証する ことです。ファイルの中身は、実行時の挙動の代理指標にすぎないからです。
似た問題として、失敗の終了コードが呼び出し側で消えてしまう経路もあります。こちらはAntigravity CLI の失敗を握りつぶしていたのは呼び出し側のパイプでした で扱いました。無人で走る処理を止める設計という点では、消えたのは中身ではなく作業ディレクトリでした とも地続きです。
効きを確かめるプローブを1本置く
やり方は素朴です。その設定が効いていれば必ず失敗するテスト を1本用意し、実際に失敗することを確認します。
// probe.test.ts — testTimeout が効いていれば必ず失敗する
import { it } from 'vitest' ;
it ( 'config probe: must time out' , async () => {
await new Promise (( r ) => setTimeout (r, 5000 ));
});
// vitest.probe.config.ts — 本番の設定と同じキーを使う
import { defineConfig } from 'vitest/config' ;
export default defineConfig ({
test: { include: [ 'probe.test.ts' ], testTimeout: 10 } ,
}) ;
呼び出し側では、成功と失敗の意味が反転します。
#!/usr/bin/env bash
# プローブは「落ちること」が正常。通ってしまったら設定が効いていない
npx vitest run --config vitest.probe.config.ts > /tmp/probe.log 2>&1
if [ $? -eq 0 ]; then
echo "config-probe: testTimeout が効いていません(プローブが通過しました)" >&2
exit 1
fi
echo "config-probe: ok"
手元で実行したところ、キーが正しいときはプローブが終了コード 1 で落ち、timeout の綴り違いに戻すと終了コード 0 で通過しました。設定が効いているかどうかが、そのまま数値として出てきます。
プローブに向くのは、普段は発火しない安全装置 です。タイムアウト、リトライ上限、同時実行数の上限、カバレッジ下限、リクエストサイズの制限。いずれも「効いていない状態」と「たまたま発火していない状態」が外からは同じに見えます。
逆に、常時使われる設定にプローブは要りません。出力先ディレクトリの設定が間違っていれば、成果物が出ない時点で気づきます。声を上げるキーに、わざわざ見張りを付ける必要はありません。
警告止まりのものは、失敗へ昇格させる
Wrangler のように警告を出してくれるツールは、あと一歩です。警告は出るのに終了コードが 0 のままなので、CI のログでは他の出力に埋もれます。数十行のビルドログの中で、黄色い1行を毎回見つける前提の運用は続きません。
出力を読んで昇格させるだけの薄いラッパで足ります。
#!/usr/bin/env bash
# assert-config.sh — wrangler の「Unexpected fields」警告を失敗へ変える
set -o pipefail
OUT = "$( CI = 1 npx wrangler deploy --dry-run --outdir= " ${1 :- . / . wrangler-dryrun } " -c " ${2 :- wrangler . toml } " 2>&1 )"
STATUS = $?
echo " $OUT "
if printf '%s' " $OUT " | grep -q "Unexpected fields found" ; then
printf '%s\n' " $OUT " | grep -A2 "Unexpected fields found" >&2
echo "config-assert: 未知のフィールドを検出したため中断します" >&2
exit 1
fi
exit $STATUS
綴りを間違えた設定に対しては終了コード 1、実運用の wrangler.toml に対しては終了コード 0 で通りました。運用中の設定ファイルを通したときに未知フィールドがゼロだったことは、副次的な確認としても有り難い結果でした。
このラッパには2つ注意点があります。ひとつは、警告文の表現が将来変わりうること。文字列一致に依存する以上、ツールを上げた直後は「検出できているか」をわざと壊した設定で1回試すべきです。もうひとつは、set -o pipefail を入れておかないと、パイプの途中の失敗が握りつぶされることです。設定の見張り役自身が黙る側に回っては本末転倒になります。
どこまで置くか、という線引き
すべての設定にプローブを用意するのは現実的ではありませんし、テストの実行時間だけが伸びます。私はこの取捨を、次の順で行っています。
壊れても静かなもの を先に選ぶ。安全装置、上限値、遮断条件
そのうち、壊れたときの損失が大きいもの だけを残す。無制限に走り続ける処理、課金に触れる処理(私の場合は Stripe の Webhook を受ける側の検証まわりです)、破壊的な操作
残ったものに1本ずつプローブを置く。多くのリポジトリでは2〜4本に収まる
数を絞る理由は運用の継続性にあります。プローブは、本体の設定が正しく変更されたときにも落ちる可能性があります。10本置けば、設定を1つ触るたびにどれかが騒ぎ、やがて誰も見なくなります。見張りの数は、見張りを維持できる数までです。
置かない判断も同じくらい重要です。声を上げるキー、常時使われるキー、変更頻度が極端に低いキーは、対象から外すことをお勧めします。既にツール側が検査してくれている領域に、二重の見張りを置く意味はありません。
手元の設定に一度だけ通しておく
最初にやることを1つだけ挙げるなら、普段使っているツールが3つの層のどこにいるかを確かめる ことです。設定ファイルのキーをわざと1文字変えて、いつもどおりのコマンドを走らせるだけで済みます。落ちるか、警告だけか、何も言わずに通るか。ものの数分で分かります。
黙って通ったツールがあれば、そこが今後エージェントに設定を任せられない領域、あるいはプローブを置くべき領域です。私自身、この確認をしてから設定変更の依頼の仕方が変わりました。「設定を足してください」ではなく「設定を足して、効いていることを示すテストも足してください」と頼むようになっています。
書いたつもりの設定が本当に効いているかは、結局のところファイルの外にしか答えがありません。まだ何も起きていないうちに、一度だけ確かめておく価値のある種類の確認だと感じています。