Skip to content

Scars in the Source: Lessons from Claude Code's Leaked Comments

2026年3月31日、AnthropicのClaude Codeのソースコードがnpmレジストリのソースマップ経由で流出した。TypeScript約1,900ファイル、51万行超。ランタイムはBun、ターミナルUIはReact + Ink。

grepした。コメントを拾い、構造を追い、気になる箇所を読んだ。

エージェントループの核心が驚くほどシンプルだったこと——LLMを呼ぶ、ツールを実行する、結果を戻す、の繰り返し。ツール定義の質がそのままエージェントの質を決めていること。壊れた補助機能がメインフローを止めないFail-Open哲学。プロンプトキャッシュの制御に費やされた膨大な工夫。ターミナルのセル粒度でフレームディフを取るレンダラも印象に残った。

ただ、これは意図しない流出だ。具体的な実装パターンや設計判断に踏み込むのはフェアじゃないと思う。

代わりに、コメントの話をしたい。

51万行のコードには、大量のコメントが埋まっている。日付入りのインシデント記録。BigQueryの分析結果。「5回バイパスされた」というセキュリティの苦闘。「inherently fragile」という率直な設計の後悔。コメントは実装そのものよりも雄弁だ。本番で血を流したエンジニアたちが、コードに刻んだ傷跡。

同じようなプロダクト——LLMエージェントツール——を作るなら、この傷跡は他人事ではない。

LLMエージェントツールとは

大規模言語モデル(LLM)にファイル操作やコマンド実行などの「ツール」を持たせ、ユーザーの指示に応じてツールを自律的に呼び分けながらタスクを遂行するソフトウェア。Claude Code、GitHub Copilot CLI、Cursor などが代表例。

リトライは味方じゃない

LLM APIを叩くプロダクトなら、リトライは呼吸するように書く。429が返ってきたら待って再送。5xxなら少し待って再送。正しい作法だ。

ある規模を超えると、リトライそのものが問題を増幅し始める。

コメントの一つに、会話コンテキストの自動圧縮処理が失敗したとき、無限にリトライを繰り返していた記録がある。

1,279 sessions had 50+ consecutive failures (up to 3,272) in a single session, wasting ~250K API calls/day globally.

——1,279セッションが50回以上連続で失敗。最大3,272回。全体で1日約25万APIコールを浪費していた

BigQueryのデータ分析で発覚したとある。修正はシンプルだ。連続3回失敗したら諦める。サーキットブレーカー。

サーキットブレーカー

電気回路のブレーカーと同じ発想。連続して失敗が続いたら、一定期間リクエストを送ること自体をやめる。壊れた相手に何度もリクエストを投げて状況を悪化させるのを防ぐパターン。

もう一つ。容量逼迫時のリトライについて、ユーザーが結果を待っている処理と裏方の処理を明確に分けていた。

during a capacity cascade each retry is 3-10x gateway amplification

——容量カスケード時、1回のリトライがゲートウェイの負荷を3〜10倍に増幅する

裏方の処理——サマリー生成やタイトル付け——のリトライは、ユーザーには見えないのに、システム全体を沈める。だからユーザーが待っている処理だけリトライし、裏方は即座に失敗させる。

リトライを「する・しない」の二択で考えていると、ある日25万コールが溶けていることにデータ分析で気づく、という展開になる。LLM APIのコールは直接お金に変わる。

接続は黙って死ぬ

LLMのレスポンスはストリーミングで返ってくる。トークンが一つずつ流れてきて、ユーザーはリアルタイムでテキストが生成されるのを見る。

この体験を支えるストリーミング接続には、厄介な性質がある。SDKやHTTPクライアントのタイムアウトは、多くの場合、接続の確立だけをカバーしている。接続が確立したあと、静かに切れると——タイムアウトは発火しない。セッションは永遠にハングする。

Claude Codeではこの問題に対して、最後のチャンクが届いてからの経過時間を自前で計測し、閾値を超えたら強制切断するウォッチドッグを入れていた。

WebSocketにも同じ問題がある。

~98% of ws_closed never recover via poll alone

——WebSocket切断の約98%が、ポーリングだけでは回復しない

データで裏付けられた数字だ。

ストリーミングAPIを使うプロダクトなら、「接続は成功したが途中で黙って死ぬ」シナリオは避けて通れない。SDKのタイムアウト設定をいくら調整しても、ストリーミングフェーズの沈黙には効かないことがある。自前のアイドル監視を入れるべきかは、セッションが無限にハングしていいかどうかで決まる。

ランタイムの約束を検証する

Claude CodeのランタイムはBunだ。コメントを読むと、本番環境で踏んだ地雷がいくつも記録されている。

ファイル監視がメインスレッドとイベント配信スレッドの間でデッドロックを起こす。成功レスポンスでエラーイベントが発火する。TCPソケットが部分書き込みを自動バッファリングしない。これらはBun固有のIssue番号とともに記録されていた。

V8ヒープの外で起きるメモリリークも複数ある。TLSやソケットのバッファはV8のガベージコレクタの管理外だ。明示的に解放しなければ、プロセスの物理メモリ使用量は増え続ける。AbortSignal.timeout()のタイマーもリクエストごとに約2.4KBのネイティブメモリを残留させていた。V8のヒープスナップショットを取っても異常は見つからない。ヒープの外で起きているのだから。

同期から非同期へのファイルシステムAPI移行で、予想外のパフォーマンス劣化が起きた記録もある。

sequential for { await pathExists } loops add ~1-5ms of event-loop overhead per iteration

——逐次的な for { await pathExists } ループは、1イテレーションごとに約1〜5msのイベントループオーバーヘッドを加算する

1回のawaitに1〜5ミリ秒。プラグインの数とコンポーネントの種類を掛け合わせると、数百ミリ秒に膨張する。同期APIを非同期に置き換えれば速くなる、という素朴な期待は、ループの中では裏切られることがある。

これはBunだけの話ではない。どのランタイムでも、ドキュメントに書いてある振る舞いと本番の負荷下での振る舞いは別物だ。ランタイムのバグを見つけるのは、たいていユニットテストではなく本番の長時間セッションだ。

5回負けてから気づくこと

セキュリティに関するコメントで、最も印象的だったのがこれだ。

ALLOWLIST, not blocklist. 5 rounds of bypass patches taught us that a value-dependent blocklist is structurally fragile

——ブロックリストではなく許可リスト。5回のバイパスパッチで、値に依存するブロックリストは構造的に脆弱だと学んだ

Bashの特殊変数を使った攻撃に対して、ブロックリスト(禁止リスト)方式で5回パッチを当てた。5回ともバイパスされた。シェルのスコープモデルは複雑で、自前のパーサーとシェルの実際の挙動は乖離する。ブロックリストは「すべての攻撃パターンを列挙する」という、原理的に勝ち目のない戦いだ。

6回目で許可リスト(allowlist)に全面切り替えした。許可されていないものはすべて拒否する。

ブロックリストと許可リスト

ブロックリスト(blocklist)は「危険なものを列挙して禁止する」方式。新しい攻撃パターンが出るたびにリストを更新する必要がある。許可リスト(allowlist)は逆で、「安全なものだけを列挙して許可する」方式。リストにないものはすべて拒否されるため、未知の攻撃にも対応できる。

5回負けないと方針転換できなかったのか。たぶん、1回目のバイパスは例外に見えた。2回目は運が悪かったように見えた。3回目で不安になり、5回目でようやく「これは構造の問題だ」と認めた。ブロックリストが原理的に脆弱だという一般論は誰でも知っている。自分のコードでそれを認めるには、5回分の証拠が必要だったのだろう。

別の箇所では、ランダム生成した5文字の確認コードが不適切な単語を綴ってしまう問題も記録されていた。

5 random letters can spell things

——ランダムな5文字は単語になりうる

内部のチャットで共有されたジョークとともに、ブロックリストが追加されている。こういう問題は、起きるまで誰も想像しない。

壊れたキルスイッチ

大規模なプロダクトには、問題が発生したときに機能を無効化するキルスイッチが不可欠だ。Claude Codeにも複数のキルスイッチが用意されている。自動アップデートの停止、バックグラウンドジョブの負荷分散、自動モードの無効化。

フィーチャーフラグとキルスイッチ

フィーチャーフラグは、コードをデプロイし直さなくてもサーバー側の設定変更だけで機能のON/OFFを切り替えられる仕組み。キルスイッチはその応用で、障害時に特定の機能を即座に無効化するために使う。GrowthBook、LaunchDarkly などのサービスが代表的。

しかし、キルスイッチ自体が壊れることがある。

フィーチャーフラグのキャッシュが、プロセス起動時のスナップショットのまま凍結していた。運用チームがサーバー側でキルスイッチを入れても、すでに起動しているセッションには届かない。

起動パフォーマンスの問題もある。

tengu_startup_perf p99 climbed to 76s

——起動パフォーマンスのp99が76秒に到達

起動時間のp99が76秒に達していた。プラグインのコネクタが40以上に増えたことが原因だ。キルスイッチを入れてもプロセスが再起動しなければ意味がない。再起動しても76秒かかるなら、それも問題だ。

「緊急時にこのスイッチを押せば止まる」という前提は、テストしなければ信頼できない。キルスイッチが届かないシナリオ——長時間セッション、遅い再起動、キャッシュの凍結——は、本番でしか顕在化しないことが多い。

数字を刻む文化

このコードベースで最も目を引いたのは、コメントに具体的な数字が埋め込まれている、その密度だ。

p99 / p99.99(パーセンタイル)

p99は「100回中99回目に遅いケース」の値。p99 = 76秒なら、99%のリクエストは76秒以内に完了するが、1%はそれより遅い。p99.99は上位0.01%の極端なケース。平均値では見えない「最悪のユーザー体験」を捉えるのに使う。

マジックナンバーの横に、必ず根拠が書いてある。ある上限値の横にはp99.99の実測値。出力トークンの予約サイズの横にはp99の実測値と「デフォルトが実使用量の8〜16倍」という分析。ある閾値の横には「N=26.3M」——2630万サンプルのベースラインから導いた値だと明記されている。

日付入りのBigQuery分析が複数箇所に引用されている。キャッシュ検出の偽陽性率、APIコールの浪費量、WebSocketの回復率。すべてデータで裏付けられた判断だ。

changes the tool description on every query() call, invalidating the cache prefix and causing a 12x input token cost penalty.

——毎回のAPIコールでツール説明文を変更すると、キャッシュプレフィックスが無効化され、入力トークンコストが12倍になる

「12倍」という数字がなければ、この問題の深刻さは伝わらない。LLMプロダクトのコスト最適化は、こういう見えにくいキャッシュ無効化との戦いだ。

プロンプトキャッシュとは

LLM APIでは、システムプロンプトやツール定義など「毎回同じ内容」の部分をサーバー側でキャッシュし、入力トークンのコストを大幅に下げる仕組みがある。キャッシュが効くには、リクエストの先頭部分(プレフィックス)が前回と一致している必要がある。ツール説明文のような「毎回同じはず」の部分が微妙に変わると、プレフィックスが一致しなくなり、キャッシュが丸ごと無効化される。

社内の長時間テストも文化として定着している。

Ant soak: 1,734 dedup hits in 2h, no Read error regression.

——社内soakテスト: 2時間で1,734回の重複排除ヒット、Readエラーの回帰なし

2時間のテストで1,734回のヒットを記録し、エラーレートをベースラインと比較している。「動いた」ではなく「ベースラインに対して回帰がないことを確認した」。

コードに数字を刻む習慣は、未来の自分と同僚へのギフトだ。なぜこの値なのか。どのデータに基づいているのか。それがなければ、半年後に誰かがその数字を変えようとしたとき、根拠なく変えるか、怖くて触れないかの二択になる。

正直な傷跡

最も好感を持ったのは、コードベースの随所に見られる正直さだ。

inherently fragile — each pass can create conditions a prior pass was meant to handle

——本質的に脆い——各パスが、前のパスが処理するはずだった状態を作り出しうる

メッセージの正規化処理が「本質的に脆い」ことを認めている。多段階の変換パイプラインで、各段が前段の前提条件を壊す可能性がある。

the error message is fragile and will break if the API wording changes

——このエラーメッセージ判定は脆く、API側の文言が変われば壊れる

APIのエラーメッセージ文字列でリトライ判定をしている箇所。API側の文言が変わったら壊れることを、書いた本人が認めている。

IMPORTANT: Do not add any more blocks for caching or you will get a 400

——重要: キャッシュ用のブロックをこれ以上追加するな。400エラーになる

将来の開発者への率直な警告。理由の説明ではなく、地雷の存在だけを伝える一行。

これらは「恥ずかしいコード」ではない。技術的負債の存在を隠さない文化の表れだ。「ここは壊れやすい」「ここは妥協している」と明示することで、次にこのコードに触る人間が同じ穴に落ちずに済む。

完璧なコードを目指すのは正しい。でも、不完全さを正直に記録することのほうが、長期的にはチームを助ける。

取り入れたい3つの習慣

51万行のコメントをgrepして、自分のプロダクトに持ち帰りたいと思ったことが3つある。

コメントに数字を刻む

マジックナンバーの横に根拠を書く。「p99 = 4,911 tokens」「N=26.3M」「BQ 2026-03-10」。日付、サンプルサイズ、パーセンタイル。これがあるだけで、半年後にその値を変えていいかどうかの判断ができる。

わたしのコードにもマジックナンバーはたくさんある。閾値を決めた理由を覚えているうちに、数字で残す。「なんとなくこのくらい」で決めた値には「根拠なし、要計測」と書く。正直さも数字の一部だ。

リトライに「誰が待っているか」を問う

リトライロジックを書くとき、「何回リトライするか」「何秒待つか」は考える。「このリトライでユーザーが待っているか」は、意識しないと抜け落ちる。

ユーザーが画面の前で待っているなら、リトライする価値がある。バックグラウンドのサマリー生成やメタデータ更新なら、失敗しても誰も気づかない。そこにリトライを入れると、障害時に負荷を増幅するだけだ。リトライの上限を決めるのと同じくらい、「そもそもリトライすべきか」を分岐させることが大事だった。

壊れやすさを明示する

「inherently fragile」「will break if the API wording changes」。こう書けるのは強さだ。

技術的負債のコメントは、恥ずかしいから書かないのではなく、書かないことが恥ずかしい。次にこのコードに触る人間——3ヶ月後の自分を含めて——が、同じ穴に落ちるか落ちないかは、この一行にかかっている。「IMPORTANT: Do not add any more blocks for caching or you will get a 400」。こういうコメントが一つあるだけで、半日のデバッグが消える。

コメントが残すもの

本番で動くプロダクトのコメントには、ドキュメントやブログ記事には載らない種類の知識がある。リトライが暴走した日の記録。キルスイッチが届かなかった夜の記録。5回バイパスされてから方針を変えた判断。数千万サンプルから導いた閾値の根拠。

設計ドキュメントには残らない。コードレビューのコメント欄からも消える。ソースコードに刻まれたコメントだけが、その瞬間の判断と文脈を保存している。

同じようなLLMエージェントを作るとき、同じ地雷を踏む確率は高い。ストリーミングの沈黙、リトライの暴走、キャッシュの落とし穴。先に踏んだ人たちが残した傷跡は、地図になる。