---
title: "Claude Code のFable5でRemix2 -> React Router 7のマイグレーションさせてみた"
date: 2026-06-14
categories:
  - "AI IDE"
  - "Claude Code"
  - "Cursor"
url: "https://hidetaka.dev/ja/writing/dev-notes/migrate-remix2-to-rr7-by-fable-5"
---

Fable 5 の発表ページを読んでいたら、おすすめユースケースにこんな記述がありました。

> Software engineering. During early testing, Stripe reported that Fable 5 compressed months of engineering into days. In a 50-million-line Ruby codebase, the model performed a codebase-wide migration in a day that would otherwise have taken a whole team over two months by hand.
> 
> [https://www.anthropic.com/news/claude-fable-5-mythos-5](https://www.anthropic.com/news/claude-fable-5-mythos-5)

「移行タスクが得意」という主張です。ちょうど手元に Remix 2 で作った Cloudflare Pages アプリがあったので、React Router 7 への移行をやらせてみることにしました。

対象のアプリには Stripe（Checkout / webhook）・Clerk（認証）・動的 OGP 画像生成が入っており、それなりに複雑な状態です。

## Fable 5 向けのプロンプトを作る

Fable 5 は従来モデルとプロンプトの作り方が異なるという記述が公式 Docs にあります。

> Claude Fable 5 には Claude Opus 4.8 とは異なる動作上の特徴がいくつかあり、プロンプトやスキャフォールディングの更新が必要になる場合があります。
> 
> [https://platform.claude.com/docs/ja/build-with-claude/prompt-engineering/prompting-claude-fable-5](https://platform.claude.com/docs/ja/build-with-claude/prompt-engineering/prompting-claude-fable-5)

個人開発で時間をかけすぎても仕方ないので、上記のページを Markdown でコピーして Fable 5 に丸投げしました。

```
claude fable 5への指示の出し方について教えて。
anthropicのdocsを添付する。

例えばremix2で作られたcloudflare pagesアプリがある。stripe / clerkなども入っており、OGP生成処理などややこしい状態だ。
これをreact router7に移行したりcloudflare workersへ移行したりすることを指示するには、それぞれどんなプロンプトを出すのが良いということなのか？
```

出てきたプロンプトがこれです。

```
## 背景と目的
このリポジトリは Remix 2 で構築した Cloudflare Pages アプリで、Stripe（Checkout / webhook）、
Clerk（認証）、動的 OGP 画像生成を含む。Remix 2 のメンテナンス終了に備え、
React Router 7 の framework mode へ移行したい。次フェーズで Cloudflare Workers への
移行も予定しているため、このフェーズでは挙動を変えずにフレームワークだけを差し替えることが目的。

## やること
Remix 2 から React Router 7 への移行を、移行完了の検証まで含めてエンドツーエンドで実施する。

## 境界（やらないこと）
- Stripe / Clerk / OGP 生成のロジックは、移行に必要な API 差分の吸収以外は変更しない
- 機能追加・抽象化の導入・移行と無関係なリファクタリング・依存パッケージの不要な更新はしない
- 後方互換 shim や feature flag は使わず、新 API に直接書き換える

## 検証
- ルートごとに移行したら、ビルドとローカル起動で該当ルートが動くことをツール実行結果で確認する
- 特に次の3点は実際に動かして確認する：(1) Clerk のログイン/保護ルート、
  (2) Stripe webhook の受信と署名検証（stripe CLI の listen/trigger を使う）、
  (3) OGP 画像エンドポイントが画像を返すこと
- 一定間隔で、別コンテキストの検証用サブエージェントに差分レビューをさせる

## 報告と中断のルール
- 進捗報告は、このセッションのツール実行結果で裏付けられる内容のみ。未検証なら未検証と明記する
- テストやビルドが失敗したら、出力を添えてそのまま報告する
- ユーザーに確認を取るのは「破壊的・不可逆な操作」「スコープの実質的変更」
  「ユーザーしか持たない情報（環境変数の値など）が必要」の3つの場合のみ。それ以外は最後まで進める

## メモ
移行中に判明した Remix 2 と React Router 7 の API 差分や、Stripe/Clerk/OGP まわりで
ハマった点は MIGRATION_NOTES.md に1項目1教訓で記録する（次フェーズの Workers 移行で参照する）。
```

「境界（やらないこと）」「報告と中断のルール」をセットで渡しているのがポイントで、余計なところに手を出さないための制約と、どこまで自律で進めるかの裁量範囲を最初に明示しています。

## Claude Code on the web に渡して実行

このプロンプトをそのまま Claude Code on the web に渡します。

![Claude Code on the webにプロンプトを投入した画面。下部に「Meet Fable 5」バナーとFable 5使用中のインジケーターが表示されている](https://wp-api.wp-kyoto.net/wp-content/uploads/2026/06/fa40af9a10bda9f757b0891eecff896f-20260611161932.png)

インターフェース下部に「Fable 5 中」と表示されており、Fable 5 で動作していることが確認できます。あとは信じて待ちます。

## ライブラリ差し替えから型チェックまで自動でこなす

最初に着手したのは依存パッケージの差し替えです。

![Clerk移行先は @clerk/react-router と判断し、Remixの依存を全削除してReact Router 7系へ一括差し替えしているターミナル出力](https://wp-api.wp-kyoto.net/wp-content/uploads/2026/06/b6fc862d22d66dcaeb8ebb16f3616db9-20260611162001.png)

`@remix-run/{react,cloudflare,cloudflare-pages,dev}` と `@clerk/remix` をまとめてアンインストールし、`react-router@7.17.0` + `@react-router/{dev,cloudflare,fs-routes}` + `@clerk/react-router` をインストールしています。結果は「added 39 packages, removed 1 package, changed 13 packages」でした。

続けて `vite.config.ts` や `react-router.config.ts`、`entry.client.tsx` の編集に入りました。この時点でのトークン消費と経過時間がこちら。

![vite.config.ts編集中の画面。6分18秒・2.2kトークン消費・もうすぐ完了と表示](https://wp-api.wp-kyoto.net/wp-content/uploads/2026/06/9f21a55652267332e857f9948d70108d-20260611162044.png)

開始から約 6 分、トークン消費は 2.2k でした。

## Smoke Testからの不具合修正

ビルドを通したあと、ローカルサーバーを起動して各ルートの動作確認（Smoke test）まで進めていきます。

![型チェック27件全通過・ビルド完了・Smoke testをルートごとに実行している画面](https://wp-api.wp-kyoto.net/wp-content/uploads/2026/06/4985a51f9095a0edf3d166aea5ac782f-20260611163241.png)

型チェック（27 件）が全通過し、ビルドも成功。サーバーを起動して `curl` でルートを一本ずつ叩いていきます。これはいわゆるSmoke test ですね。

![Smoke test結果。/en・/categories/cloudflare・/feed・/sitemap.xmlは200だが、記事詳細ページ系が500を返している](https://wp-api.wp-kyoto.net/wp-content/uploads/2026/06/d8c5e49de7c7d3449a488ef83cd1c932-20260611163410.png)

トップやカテゴリページは 200 ですが、記事詳細ページが 500 になっています。自動的に原因特定に入り、8 コマンドで問題を切り分けていきます。

![HTTP 500の原因調査として8コマンドを実行。Inspect error payload・Decode turbo-stream payload・Test OGP image generation endpointなどが実行済みに。OGP画像エンドポイントはPNG 1200x630を返した旨の報告が続く](https://wp-api.wp-kyoto.net/wp-content/uploads/2026/06/48a039ce57f90c85be101bcfd8d9bdf5-20260611163641.png)

途中で OGP 画像エンドポイントを確認したところ、PNG（1200×630）が正常に返ってきたことを確認しています。500 は post 詳細ページのみに絞られました。この後も調査と修正がしばらく続きます。

## 30 分経過：Too many open files でスタック

移行前後のルートを比較するバックグラウンドタスクを動かしていたあたりで、こんなエラーが出てきました。

![ルート比較表に多数のDIFFが表示され、kj/async-io-unix.c++:918のToo many open filesエラーが赤字で表示されている](https://wp-api.wp-kyoto.net/wp-content/uploads/2026/06/997349bc2dc3b3daf12a57c72bb39cd1-20260611165813.png)

StripeやClerkなどのキーをClaude Code on the Web環境には提示していないため、「APIキーが原因だったら無視していいよ」と助け舟を出してみました。

![ユーザーからのAPIキー無視の指示を受け、Claude Codeが全workerd/wranglerプロセスをkillしてFDリミットを上げてサーバーを再起動し、ESLintを実行している](https://wp-api.wp-kyoto.net/wp-content/uploads/2026/06/453561073871639520e523b061b3828b-20260611170416.png)

ある程度割り切ることにしたのか、 ESLintでの作業にシフトし、赤文字の連続が一旦おさまりました。

## 57 分・96.9k tokens でひとまず完了、そのままセルフレビューへ

![57分17秒・96.9kトークン時点の画面。MIGRATION_NOTES.md +40行、Gitコミット2件をプッシュ。react-router devが200で動作確認完了の報告](https://wp-api.wp-kyoto.net/wp-content/uploads/2026/06/e51408abd2fac4057ec030c4b2120bd0-20260611171125.png)

57 分 17 秒・96.9k トークン時点で、`react-router dev`（開発サーバー）が 200 で動作確認できたと報告が入りました。その後サブエージェントを利用してセルフレビューが始まります。

サブエージェントは `git diff main...HEAD` を全件読んで差分をレビューしていました。

![Review RR7 migration diffタイトルのサブエージェントパネル。git diff main...HEADをレビューし、rootAuthLoader sourceを読んでいる](https://wp-api.wp-kyoto.net/wp-content/uploads/2026/06/644ecce92498bb184280513bd68fd2f4-20260611171312.png)

そして出てきた重大指摘がこれです。

![entry.server.tsxにstreamTimeout=60_000を追加した差分。コメントに「single fetchでloaderが返すPromiseが約5秒でabortされAI検索のllmAnswerが必ずタイムアウトする」と記されている](https://wp-api.wp-kyoto.net/wp-content/uploads/2026/06/6fd126c6813393c8f3fda7d783d168ef-20260611172339.png)

React Router 7 の single fetch モードでは、loader が返す Promise（旧 `defer` 相当）を `streamTimeout` が未設定だと約 5 秒で打ち切ります。このアプリには AI 検索機能（`llmAnswer`）があり、5 秒ではほぼ確実にタイムアウトします。サブエージェントがこれを発見して `entry.server.tsx` に `export const streamTimeout = 60_000` を追加しました。

## 最終的な作業結果

![移行完了サマリ。フレームワーク差し替えの詳細・Clerk v2固定の理由・11ルートのroot-data.server.ts抽出が列挙されている](https://wp-api.wp-kyoto.net/wp-content/uploads/2026/06/7204327953b75bba6e10f2f1b662b4b8-20260611172415.png)

最終的なサマリをまとめると以下のとおりです。

-   `@remix-run/*` → `react-router@7.17.0` + `@react-router/{dev,cloudflare,fs-routes}` へ差し替え。ルートファイルのリネームなし、`flatRoutes()` で既存の規約を継続

-   `json()` / `defer()` を全廃。プレーンオブジェクト返却へ切り替え、404 は `data(..., {status:404})`、webhook は `Response.json()`（shim なし）

-   `entry.*` は `HydratedRouter` / `ServerRouter` へ切り替え。`AppLoadContext` 拡張は `declare module 'react-router'` へ

-   Clerk は v3 が `context.cloudflare.env` へのアクセスを全書き換えするため、「挙動を変えない」方針を優先して v2.4.15（latest-v5 タグ）を採用。v3 化は Workers 移行フェーズで対応

-   子ルートが root loader を直接 `.json()` していた旧パターンを `app/utils/root-data.server.ts` に切り出し、11 ルートを置換（API 差分の吸収として必要な唯一の構造変更）

### スタックしていた箇所（未検証）

![未検証項目：Clerkの実ログインフローとmypage/bookmarks・user.createdでのStripe顧客作成webhookの2点（実鍵がないため）](https://wp-api.wp-kyoto.net/wp-content/uploads/2026/06/0d9e0fd4c20bca94a14aa55cebcf55d5-20260611172446.png)

実鍵がないため検証できなかった項目は 2 つです。Clerk の実ログインフローと `user.created` webhook での Stripe 顧客作成。いずれも「ロジック自体は無変更」と明記されており、本番環境で確認するしかない部分です。

### コンテキストウィンドウの消費量

![コンテキストウィンドウの内訳。合計197.6k / 1.0M（20%）。メッセージ169.4k・MCP tools 122.3k（deferred）](https://wp-api.wp-kyoto.net/wp-content/uploads/2026/06/0b8f0d381ee8133aff19c188fba3a5dd-20260611172538.png)

コンテキストウィンドウ全体では 197.6k / 1.0M（20%）を消費しました。

## 案の定APIキーが関係する箇所で障害が発生

移行ブランチをデプロイしたところ問題が出たので、Cursor でデバッグ・復旧しました。困った時の Cursor composer-2.5。

### Cursor が確認した障害の全容

サイト障害（HTTP 500）の原因として Cursor が特定したのは4件です。

-   `@clerk/react-router: Missing publishableKey` が SSR 時に発生

-   Layout 内の `useRouteLoaderData('root')` では RR7 で clerkState が取れない

-   `rootAuthLoader` が返す Response を JSON に展開していなかった

-   `getAuth()` が Cloudflare Workers 上で publishableKey を解決できなかった

修正ファイルは3つ。`app/root.tsx`（rootAuthLoader の Response JSON 化 + ClerkProvider を App コンポーネントへ移動）、`app/utils/auth.ts`（getAuth() に publishableKey を明示渡し）、`robots[.]txt.tsx` / `llms[.]txt.tsx`（import を react-router に変更）です。本番デプロイ後に HTTP 200 を確認しました。

## まとめ

約 1 時間・197.6k トークンで Remix 2 → React Router 7 の移行がひとまず完了しました。APIキーなしの環境という制約の中で、ライブラリ差し替え・型チェック 27 件通過・ビルド・Smoke test・OGP エンドポイント確認まで自律でこなしています。

セルフレビューである程度不具合を検知していることもそうですが、APIキー関係の処理系統というモデルやエージェント自身にはどうにもできない要因部分以外では問題が起きなかったのもかなり印象的です。Claude Code on the Webではなく`.env`ファイルのあるローカルPCでやればゼロ障害で完遂したのかもしれません。

前からやっておきたいと思いつつ手を出せていなかった領域だったので、今回のFable 5解放はちょうど良いタイミングでした。ただ、じゃあこれが Devin や Cursor Agent でも完遂できたのではないか？という点については個人的に気になるところもあり、ライブラリ移行系などはこれからもいろいろな切り口で試してみたいなと思います。