「WebFetch で技術ドキュメントを読ませようとしたら、403 が返ってきた」「200 は返っているのに本文がほぼ空で、ナビゲーションだけが残っている」— Claude Code を日常的に使う中で、自動化スクリプトのある日突然うまく動かなくなる代表的な現象です。私自身、自分の運営する Lab 系サイト4本の自動投稿パイプラインで、月に何度かこの種のトラブルにぶつかります。
落ち着いて切り分ければ原因は4つに集約できます。それぞれの見分け方と、私が実運用で採用している対処を順に整理しておきます。
何が起きているのか — WebFetch の挙動を先に押さえる
Claude Code に組み込まれた WebFetch ツールは、内部的には Anthropic 側のサーバーが対象 URL に対して HTTP GET を発行し、得られた HTML をテキスト化してモデルに渡す仕組みです。ここで重要なのは、ブラウザではないという点です。
具体的には次の挙動を持ちます。
- JavaScript を実行しない: React や Next.js、Vue で構築されたクライアントレンダリング系のページは、初期 HTML のシェル(ローディングスピナーやナビゲーション)だけが返ります。
- 固定の User-Agent を持つ: 一部のサイト(特に Cloudflare の Bot Fight Mode を有効にしているもの)は、この User-Agent を機械的に弾きます。
- 結果を一定時間キャッシュする: 同一 URL に対して短時間に再リクエストすると、サーバー側のキャッシュから古いレスポンスが返ることがあります。
- ドメイン許可リストが効く:
--allowedDomainsを絞っている運用では、許可対象外のドメインへのアクセスが事前にブロックされます。
つまり「403」「空ページ」「タイムアウト」「想定と違う古い情報」のいずれも、原因はこの4軸のどれかに収まります。
原因1: Cloudflare などのアンチボット保護で 403
Anthropic のヘルプドキュメント、GitHub の生のファイル、Stack Overflow など、Cloudflare の手前にいるサイトでは、WebFetch のリクエストがしばしば 403 で弾かれます。エラーメッセージとしては次のような形で返ってくることが多いです。
Web fetch failed: HTTP 403 — the server refused to fulfill the request
これは Cloudflare の Managed Challenge や Bot Fight Mode が、WebFetch の User-Agent や TLS フィンガープリントを「明らかにブラウザではない」と判定して落としているサインです。私の経験では、特に技術ドキュメントを多用するエージェント運用で頻発します。
私が使っている対処
まず、対象 URL に 同じ情報の代替ソース がないかを最初に検討します。例えば GitHub の https://github.com/owner/repo/blob/main/README.md は 403 になりがちですが、https://raw.githubusercontent.com/owner/repo/main/README.md ならテキスト本文がそのまま取れます。同じく公式ドキュメントには、API リファレンス用の *.json や llms.txt を公開しているケースが増えています。
それでも当該 URL を取りに行く必要があるなら、Claude in Chrome(Chrome MCP)にフォールバックします。プロンプトの中で、こう指示しています。
WebFetch でこのURLを試してください。もし 403 または空ページが返ったら、
mcp__Claude_in_Chrome__navigate でアクセスし、
mcp__Claude_in_Chrome__get_page_text で本文を読み取ってください。
URL: https://example.com/docs
Claude in Chrome は実ブラウザで動くため、Cloudflare のチャレンジを通過できる場合がほとんどです。私のアプリ事業(AdMob を含む収益化)でも、AdMob の管理画面のような JavaScript 重めの SaaS は Chrome MCP 経由でないと触れません。
curl や wget を Bash ツールから叩く方法もありますが、Cowork モードや一部の環境ではネットワーク制限により失敗します。さらに WebFetch を回避する目的での bash 経由のフェッチは Cowork の方針として推奨されていません。Claude in Chrome を使うほうが筋が良いです。
原因2: クライアントレンダリングのページで本文が空
レスポンスは 200 OK なのに、<main> の中身が「Loading...」や「Enable JavaScript」だけ、というケースです。これは原因1よりも厄介で、エラーが出ないため気づきにくいのが特徴です。
代表的なサインは次のようなものです。
- ナビゲーションのリンク群は読めるのに、記事本文が見つからない
- 検索結果のリストが「0件」と表示される(実際にはサイトに記事が存在する)
- 価格表のテーブルが空欄
これは React や Next.js などで構築された SPA で起きます。初期 HTML にはアプリケーションのシェルしか含まれず、本文は fetch() 後にクライアントで描画されるためです。
対処: SSR の有無を確認し、Claude in Chrome へ切り替える
切り分けは、ブラウザの「ページのソースを表示」(View Source)で対象の本文テキストが含まれているかどうかで判断できます。含まれていなければ WebFetch では取れません。
最近の Next.js なら App Router 化で SSR されている場合もありますが、Vercel デプロイの古い構成や、Vite + React + React Router の構成では、本文がクライアントレンダリングのままになっていることが多いです。
この場合、私のワークフローは以下のようになります。
このページは SPA で、WebFetch では本文が取れない可能性が高いです。
mcp__Claude_in_Chrome__navigate でアクセスしてから、
mcp__Claude_in_Chrome__get_page_text でレンダリング後の本文を取得してください。
私が運営する Lab 系4サイト(Claude Lab / Gemini Lab / Antigravity Lab / Rork Lab)も Next.js 16 + App Router 構成ですが、念のためビルド後の view-source: で記事本文が HTML に含まれていることを確認するようにしています。WebFetch で取れることは、AI クローラに対するアクセシビリティそのものでもあるからです。
原因3: ドメイン許可リスト(--allowedDomains)で事前ブロック
Claude Code を CI や定期実行で動かす運用では、--allowedDomains でフェッチ可能なドメインを絞り込んでいるケースが少なくありません。この場合、許可対象外のドメインを WebFetch しようとすると、次のような表示が出ます。
WebFetch is not allowed for domain: stackoverflow.com
Allowed domains: docs.anthropic.com, github.com
仕様通りの挙動です。許可確認ダイアログを抑止しつつ、安全な範囲だけにフェッチを限定したいときに使うフラグですが、許可リストの更新を忘れると本番ジョブが静かに止まります。
対処
私は2014年から個人でアプリ開発を続けていますが、長く運用するほど許可リストは「育てる」ものになります。スクリプト側で固定するのではなく、設定ファイルに切り出しておくと管理が楽です。
# 例: ~/.claude-code/allowed-domains.txt として管理
docs.anthropic.com
www.anthropic.com
raw.githubusercontent.com
api.github.com
developer.apple.com
developer.android.com
admob.google.com
firebase.google.comCI からは次のように渡します。
claude code --allowedDomains "$(paste -sd, ~/.claude-code/allowed-domains.txt)" ...新規ドメインへのアクセスが必要になった時は、許可リストへの追記を Pull Request にして、変更履歴を残しておくと、後から「なぜこのドメインを許可したのか」を辿れます。
原因4: 古いキャッシュが返ってきている
WebFetch は内部で短時間のキャッシュを持っており、同一 URL に対しての再リクエストはキャッシュから返されることがあります。リアルタイムのステータスページや、価格を確認したいページで、「更新したはずなのに変わらない」という現象の正体はこれです。
対処
シンプルですが、URL にクエリパラメータを追加するのが最も確実です。
https://example.com/status?_=1716120000
末尾の _= パラメータは多くのサーバーで無視されるため、コンテンツには影響しません。一方、WebFetch にとっては「別の URL」として扱われ、キャッシュをバイパスできます。
ただし、本来キャッシュは料金とレイテンシの両方を抑えるための仕組みです。やみくもにバイパスするとコストに跳ね返るので、本当にリアルタイム性が必要な場面に限定するのがコツです。私の運用では、毎朝の SEO レポート生成(GA / GSC)のように 24 時間で更新される情報には、URL に日付付きパラメータを足すようにしています。
切り分けの順序を固定しておく
実運用で大事なのは、「いま起きている現象が4つのどれなのか」を最短で見極めることです。私が日常的に使っているチェックリストはこの順序です。
- レスポンスのステータスコードを確認します。403 / 429 なら原因1、200 なら原因2 か 4 を疑います
view-source:で本文 HTML が含まれているかを確認します。含まれていなければ原因2、Claude in Chrome へ切り替えます--allowedDomainsの設定を確認します。CI 環境で動かしているなら最優先で確認します- URL にダミークエリを足して再フェッチします。それで結果が変わるなら原因4です
ここまで切り分けても解決しないことは、私の経験ではほとんどありません。逆に言えば、原因不明のまま何度も WebFetch を叩き続けると、API 利用料だけが積み上がっていくので、最初の段階で見極める習慣をつけたほうが結果的に安く済みます。
次のアクション
WebFetch で詰まった時の現実的な次の一歩を一つだけ挙げるなら、Claude in Chrome(Chrome MCP)を別チャネルとして常に手元に置いておく ことだと感じています。Cowork の右上から有効化できますし、403 と空ページの両方を一度に解消できます。私自身、Lab 系サイトの自動運用と、累計5,000万ダウンロードのアプリ事業の両方でこの組み合わせを使っていますが、運用コストの観点でも安定性の観点でも、フォールバック先を一つ用意しておくことの価値は大きいです。
実装の参考になれば幸いです。