4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Codeの中身を読んだら、自分のCLAUDE.mdが恥ずかしくなった — cli.js解析で分かったコンテキスト設計の実際

4
Posted at

Claude Codeの中身を読んだら、自分のCLAUDE.mdが恥ずかしくなった — cli.js解析で分かったコンテキスト設計の実際

はじめに

Claude Code、使ってますか?

「CursorやCopilotとどっちがいいの?」——よく聞かれます。自分も最初はそう考えていたんですが、Claude Codeの本体であるcli.jsを読んでみたら、比較のレイヤーがそもそも違った。

cli.jsは、npm経由でインストールした場合 ~/.nvm/versions/node/{version}/lib/node_modules/@anthropic-ai/claude-code/cli.js にあります(which claude でパスを確認できます)。開いてみると12MBのミニファイされたJavaScriptが1ファイルにバンドルされている。正直、最初は「これ読めるのか?」と思いました。

でも文字列検索で引っかかるプロンプトのテンプレート文やツール定義を拾っていくと、だんだん全体像が見えてきた。そこにあったのは、Anthropicが自社モデルの特性を知り尽くした上で設計したコンテキストの組み立て方でした。システムプロンプトの構成順序、キャッシュの最適化、動的な情報の注入タイミング——いわゆる「コンテキストエンジニアリング」の実装がまるごと入っている。

そして何より、自分がそれまで書いていたCLAUDE.mdが、cli.jsの設計意図と全然噛み合っていなかったことに気付いた。この記事は、その反省と学びの記録です。

注記: 本記事の解析はミニファイされたバンドルコード(v2.1.74、14,798行)の文字列検索とパターン分析に基づいています。関数名は難読化されており推定マッピングです。バージョンアップで変わる可能性があります。


「コンテキストエンジニアリング」って何の話?

最近、「プロンプトエンジニアリング」の上位概念として「コンテキストエンジニアリング」という言葉をよく見かけるようになりました。2025年6月にAndrej Karpathyが「prompt engineeringよりcontext engineeringと呼ぶべきだ」とポストしたあたりから広まった言葉で、Anthropicも2025年9月に公式ブログで取り上げています。

プロンプトエンジニアリングが「モデルへの指示をどう書くか」に注目するのに対して、コンテキストエンジニアリングは「モデルに渡す情報全体をどう設計するか」を考えます。プロンプトの文言だけじゃなくて、どんな外部情報をいつ渡すか、会話履歴をどう管理するか、キャッシュをどこで切るか——そういう全体設計の話です。

Karpathyの表現を借りると「コンテキストウィンドウに、次のステップに必要な情報をぴったり詰め込む技術と科学」。

で、cli.jsを読んで思ったのは、これがまさにその実装だということでした。


なぜcli.jsを読む価値があるのか

CursorもCopilotもWindsurfも、バックエンドにLLMを使っています。でもそれらは外部からAPIを叩く立場です。モデルの内部特性——コンテキストウィンドウ内のどの位置の情報がどの程度影響するか、キャッシュがどう効くか——を外から推測しながら設計している。

一方、Claude Codeはモデルを作った会社が作っている。recency bias(LLMがコンテキスト後方の情報をより重視する傾向)を前提にした優先順位設計とか、プロンプトキャッシュの境界配置とか、こういうのは自分たちのモデルの癖を知っているからこそできる設計判断ですよね。

つまりcli.jsを読むと、Anthropicの中の人が考える「Claudeをこう使えば一番性能が出る」を覗けるわけです。


CLAUDE.mdが実際にどう処理されているか

自分がcli.jsを読んで一番「えっ」と思ったのがこれです。たぶんClaude Codeユーザーにとっても一番実用的な話なので、先に書きます。

CLAUDE.mdはシステムプロンプトじゃない

意外だったんですが、CLAUDE.mdの内容はシステムプロンプトの一部ではありません。ck1()という関数が、CLAUDE.mdの内容を<system-reminder>タグでラップして、各ターンのユーザーメッセージの前にメタメッセージとして注入しています。

実際のテンプレートはこんな感じです:

<system-reminder>
As you answer the user's questions, you can use the following context:
# claudeMd
Codebase and user instructions are shown below. Be sure to adhere to
these instructions. IMPORTANT: These instructions OVERRIDE any default
behavior and you MUST follow them exactly as written.

Contents of /path/to/CLAUDE.md:

[CLAUDE.mdの内容がここに入る]

      IMPORTANT: this context may or may not be relevant to your tasks.
      You should not respond to this context unless it is highly
      relevant to your task.
</system-reminder>

2つの矛盾するように見える文が共存しています。「OVERRIDE any default behavior(デフォルトの動作を上書きせよ)」と「may or may not be relevant(関連があるかないか分からない)」。

これ、最初は混乱したんですが、読み解くとちゃんと意味がある。CLAUDE.mdの指示はシステムプロンプトのデフォルト動作より優先されるけど、今のタスクに関連がなければ無視してよい、というニュアンスです。逆に言えば、曖昧に書いた指示は「関連性なし」と判断されてスルーされる可能性がある。

毎ターン再注入される

CLAUDE.mdのファイル読み込み自体はw8()(async memoize)でキャッシュされますが、その内容は毎ターン、会話メッセージの前に再注入されます。つまり長いCLAUDE.mdを書くと、そのまま毎回のトークンコストに跳ね返る。

ちなみにmemoizeとはいえ、CLAUDE.mdファイルの変更検知時やプロジェクト切り替え時にはキャッシュがクリアされて再読み込みされるので、編集した内容はちゃんと反映されます。

マージの階層は3つじゃない

よく「CLAUDE.mdは3階層でマージされる」と言われますが、cli.jsの実装を見ると実際はもっと多い。

1. Managed     ← OS管理パス(macOSなら ~/Library/Application Support/ClaudeCode/)
2. User        ← ~/.claude/CLAUDE.md + ~/.claude/rules/*.md
3. Project     ← {project}/CLAUDE.md + {project}/.claude/CLAUDE.md
                 + {project}/.claude/rules/*.md
                 ※ 親ディレクトリをたどりながら再帰的に収集
4. Local       ← {project}/CLAUDE.local.md(gitignore向け)
5. AutoMemory  ← 自動メモリ
6. TeamMemory  ← チームメモリ

全部結合されてから<system-reminder>でラップされる。CLAUDE.mdを短く保つべき理由がここにもあります——自分が書いた分だけじゃなくて、他の階層の分も合算されてトークンを食う。


プロンプト組み立ての全体構造

CLAUDE.mdの話だけだと部分的なので、cli.jsがシステムプロンプト全体をどう組み立てているかも見ておきます。

メイン関数(H4z、6248行付近)が返す配列はこうなっています:

[配列 → nullフィルタリング]

1. Opening           — "You are an interactive agent..."
2. # System          — ツール通信ルール、Hooks説明
3. # Doing tasks     — コーディング指針 [Output Style未設定時のみ]
4. # Executing actions with care — 破壊的操作への注意
5. # Using your tools — ツール使い分けルール
6. # Tone and style  — 簡潔さ、フォーマット
7. # Output efficiency — [Feature Flag制御]
8. キャッシュ境界マーカー [条件付き]
9. 動的セクション群  — Memory, 環境情報, MCP指示, Skills一覧...

.filter((M) => M !== null) で条件を満たさないセクションが除外されます。

ポイントは静的で変わらないセクションが先頭、動的に変わるセクションが末尾という配置。後述するキャッシュ戦略と密接に関係しています。

3つのレイヤー

この構造をざっくり分けると3つのレイヤーになります。

レイヤー 内容 優先度
Layer 1: ベースプロンプト # System, # Doing tasks, # Tools等 低(先頭に配置)
Layer 2: 動的コンテキスト Memory, 環境情報, MCP指示, Skills
Layer 3: ターンレベル注入 CLAUDE.md, 日付(<system-reminder> 高(毎ターン注入)

LLMにはrecency biasがあるので、後方に出現するほど影響力が高い。cli.jsは意図的にこの順序で配置しています。

あなたがCLAUDE.mdで書いた指示がシステムプロンプトのデフォルト動作を上書きできるのは、「OVERRIDE」という文言だけでなく、この配置順序にも支えられているわけです。


cli.jsに既に書いてある指示

ここが、自分のCLAUDE.mdが恥ずかしくなったポイントです。

cli.jsの # Doing tasks セクションには、こういう指示が最初から入っています:

- In general, do not propose changes to code you haven't read.
- Do not create files unless they're absolutely necessary
- Avoid over-engineering. Only make changes that are directly requested
- Don't add features, refactor code, or make "improvements" beyond what was asked
- Don't add error handling, fallbacks, or validation for scenarios that can't happen
- Don't create helpers, utilities, or abstractions for one-time operations

# Tone and style には:

- Only use emojis if the user explicitly requests it
- Your responses should be short and concise

自分は以前、CLAUDE.mdに「ファイルを読んでからコードを変更してください」「不要なファイルを作らないでください」みたいなことを書いていました。cli.jsに既にあるんですよ、これ。毎ターン再注入されるCLAUDE.mdのトークンを、重複する指示で無駄にしていたわけです。

もっと悪いのは、微妙に表現を変えて書いていたこと。cli.jsの指示と自分の指示で表現が違うと、LLMが矛盾として処理する可能性がある。

CLAUDE.mdにはプロジェクト固有の情報だけを書く——これがcli.jsの設計から導かれる原則です。ビルドコマンド、テストの実行方法、プロジェクト固有のコーディング規約、アーキテクチャの説明。cli.jsが既にカバーしている汎用的な開発ルールは書かない。


キャッシュの仕組みが想像以上に細かい

cli.jsのキャッシュ設計を見たとき「ここまでやるのか」と思いました。2段階になっています。

APIプロンプトキャッシュ(サーバーサイド)

静的セクション全体にAPIレベルでキャッシュが効いています。

cache_control: { type: "ephemeral", ttl: "1h", scope: "global" }

Anthropicはこれで90%のコスト削減を実現しているとのこと。静的セクションを先頭に集めているのは、プロンプトキャッシュが先頭からの一致でヒット判定するからです。変わらない部分が先頭にあれば、キャッシュが効きやすい。

クライアントセクションキャッシュ

動的セクションもキャッシュ制御されています。コードには2種類の定義関数があって:

// セッション中キャッシュされる
pB("memory",          () => loadMemory())
pB("env_info_simple", () => getEnvInfo())
pB("language",        () => getLanguage())
pB("output_style",    () => getOutputStyle())

// 毎ターン再計算される
D_4("mcp_instructions", () => getMcpInstructions(),
    "MCP servers connect/disconnect between turns")

pB()はセッション中維持、D_4()は毎ターン破棄して再計算。MCP指示だけが毎ターン再計算なのは、MCPサーバーの接続・切断がターン間で起きるから。芸が細かい。

この設計思想は、自分たちのLLMアプリを作るときにもそのまま使えます。変わらない指示は先頭に、変わる情報は末尾に。キャッシュ境界を意識した配置。


Anthropicもプロンプトの正解を手探りしている

cli.jsから複数のFeature Flagが見つかりました。

フラグ名 何を制御しているか デフォルト
tengu_sotto_voce "# Output efficiency"セクションの表示 false(非表示)
tengu_bergotte_lantern 簡潔さの追加指示の文言 false
tengu_tight_weave パスのフォーマット指示 true
tengu_grey_step2 Effortレベルの設定 false

これ、プロンプトの文言レベルでA/Bテストをしているということです。たとえば# Output efficiencyセクションはtengu_sotto_vocefalseの環境では送信されていない。つまり「この文言を入れた場合と入れない場合で、出力品質にどれくらい差が出るか」を比較実験しているわけです。

考えてみれば当然で、Anthropicですら自社モデルの最適なプロンプトを一発では決められない。実験して測定して改善している。自分たちのCLAUDE.mdも同じアプローチを取っていいはずで、書いて試して直す、その繰り返しが正しい。


Deferred Tools——ツールを全部渡さない工夫

Claude Codeは大量のツールを扱えます(MCP経由のものも含めると数十個になることもある)。でもツール定義にはdescriptionやパラメータスキーマが含まれていて、全部初回から送ると膨大なトークンを食う。

cli.jsはこれを「Deferred Tools」パターンで解決しています。

[初回送信 — 名前だけ]
<available-deferred-tools>
AskUserQuestion
WebFetch
WebSearch
mcp__backlog__get_issues
...
</available-deferred-tools>

[モデルが使いたいと判断した時点で]
ToolSearch → フルスキーマ取得 → ツール利用可能に

名前だけ渡しておいて、必要になったらフルスキーマを取りに行く。遅延ロードです。これで初期プロンプトサイズを大幅に削れる。

多数のツールやAPIを統合するアプリを作るとき、この考え方はそのまま流用できます。


ツールの説明文に「指示」が入っている

最後にもう一つ面白かったこと。ツールのdescriptionフィールドに、使い方だけじゃなくて行動指示が埋め込まれています。

たとえばWriteツール:

NEVER create documentation files (*.md) or README files unless explicitly requested

Bashツール:

IMPORTANT: Avoid using this tool to run find, grep, cat, head, tail,
sed, awk, or echo commands...

システムプロンプトやCLAUDE.mdだけが行動制御の場所じゃない。ツール定義のdescriptionも「指示の伝達経路」として使われている。こういう多層的な設計は、自分がカスタムツールを定義するときの参考になります。


実践編: やってはいけないこと、やるべきこと

ここまでの内容を踏まえて、CLAUDE.mdまわりのアンチパターンと推奨パターンをまとめます。

やってはいけないこと

cli.jsと重複する汎用ルールを書く

# こう書いていませんか?
- ファイルを読まずにコードを変更しないこと
- 不要なファイルを作成しないこと
- 過度なエンジニアリングを避けること

これらは全部、cli.jsの # Doing tasks に入っています。毎ターン再注入されるCLAUDE.mdのトークンを無駄にしているだけです。

Hooksで止めるべきことをCLAUDE.mdでお願いする

# これは「お願い」にすぎない
- mainブランチに直接pushしないでください

LLMはプロンプトの指示を無視することがあります(sycophancyやcontext overloadで)。「絶対にやってほしくないこと」はHooksのPreToolUseexit 1を返して物理的にブロックする。プロンプトは「お願い」、Hooksは「強制」です。

<system-reminder>タグをCLAUDE.md内で使う

cli.jsが自動で<system-reminder>にラップするので、自分で書くと二重ネストになります。意図しないパースが起きる可能性がある。

やるべきこと

よく設計されたCLAUDE.mdの例を一つ出します:

# MyProject

TypeScript + React のフロントエンドプロジェクト。

## ビルド・テスト
- dev: `npm run dev`
- build: `npm run build`
- test: `npm test`
- 単一テスト: `npm test -- -t "テスト名"`
- lint + fix: `npm run lint -- --fix`

## 規約
- React functional components only
- CSS Modules for styling(inline style禁止)
- Named exports only(default export禁止)
- State管理: Zustand(Redux, Context API禁止)
- any型の使用禁止

## アーキテクチャ
- APIクライアントは src/api/ に集約、各エンドポイントごとにファイル分割
- バリデーションは zod スキーマで定義し、src/schemas/ に配置
- 環境変数は src/config.ts 経由でのみアクセス

30行弱。「ファイルを読んでから変更しろ」みたいな汎用ルールは一切ない。ビルドコマンドは具体的に、禁止事項は明確に。これくらいの粒度がcli.jsの設計意図と噛み合います。


agents / hooks / skills の設計もcli.jsから逆算できる

CLAUDE.md以外の.claude構成ファイルについても、同じ発想が使えます。

agents/*.md

ポイント 理由
whenToUseを具体的に書く 起動判定はdescriptionで行われる
toolsは必要最小限にする ツール記述のトークンコストがそのまま効く
レビュー系はcontext: forkを使う 独立コンテキストで迎合(sycophancy)を断つ

Hooks

ポイント 理由
「絶対に守らせたいルール」はHooksで プロンプトの指示はsycophancyで無視されうる
プロンプトで十分なことはHooksにしない Hooksは優先順位モデルの外で動作する別レイヤー
PreToolUseexit 1を活用する 操作キャンセルの確実な手段

まとめ

cli.jsを読んで自分が一番変わったのは、CLAUDE.mdに対する考え方です。

以前は「思いつく限りのルールを書いておけば安心」と思っていた。でもcli.jsの処理を知ると、それがトークンの無駄遣いであり、場合によっては指示の矛盾を生んでいたことが分かった。

結局のところ、これはソフトウェアエンジニアリングの基本的な話と同じです。APIを使うとき、そのAPIがリクエストをどう処理するかを理解した上で入力を組み立てる。cli.jsという「処理エンジン」の仕様を知った上で、.claudeという「入力」を設計する。

cli.jsのバージョンが上がればプロンプトの文言は変わるし、Feature Flagも切り替わります。でもここで見てきた考え方——レイヤーごとの優先度設計、キャッシュを意識した配置、毎ターン注入のコスト意識——は、バージョンに依存しないはずです。

あなたのCLAUDE.md、何行ありますか? この記事を読んだ後で見返すと、削れる行が見つかるかもしれません。


参考

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?