Claude Code の自動投入メカニズム——CLAUDE.md と Skills のライフサイクル
Claude Code を使っていると、プロンプトを書いただけで CLAUDE.md の内容や Skills が自動投入される。Skillsに関してはアドホックに送信されるといったことはよく聞くが、そのアドホックのトリガは何なのかと思ったことはないだろうか?
この記事では、コードベースを読み解きながら、以下の 3 つの自動投入メカニズムを明らかにする:
- @シンボル展開:ユーザーが明示的に指定したファイルの投入
- CLAUDE.md 投入:階層的な命令ファイルの自動ロードと優先順位
- Skills 投入:動的なスキル発見と通知のライフサイクル
1. @シンボル展開——「一度きり」の使い捨て
1.1 投入の起点
ユーザーが @filename を含むプロンプトを送信すると、src/utils/processUserInput/processUserInput.ts が処理を開始する:
// processUserInput.ts:502-513
const attachmentMessages = shouldExtractAttachments
? await toArray(
getAttachmentMessages(
inputString,
context,
ideSelection ?? null,
[],
messages,
querySource,
),
)
: []
getAttachmentMessages は、プロンプト内の @ シンボルを検出し、指定されたファイルの内容を読み込んでアタッチメントメッセージとして会話履歴に追加する。
1.2 展開された内容の寿命
重要な特性:展開のアクションは一度きり。展開された内容は履歴に残る。
@filename を書いた瞬間にファイルが読み込まれ、その内容が会話履歴に追加される。以降のターンでも、その内容は履歴の一部として残り続ける(コンパクションまで)。
ただし、再展開はされない:
- ファイルがディスク上で変更されても、履歴の内容は古いまま
- 同じファイルを
@filenameと再度書かない限り、新しい内容は読み込まれない - モデルは「履歴に残っている古い内容」を見続ける
1.3 コンパクションによる蒸発
会話が長くなり、トークン制限(Context Window)に近づくと、src/services/compact/compact.ts による圧縮処理が走る。
古いメッセージやアタッチメントは、要約(Summary)に置き換えられるか、完全に履歴から削除される。この際、@ で展開したファイル内容も蒸発し、LLM の直接的な記憶からは消える。
具体的な流れ:
Turn 1: @file.txt を展開 → file.txt の内容が履歴に追加
Turn 2-10: モデルは file.txt の内容を「見えている」(履歴に残っているから)
Turn 11: コンパクション発生 → file.txt の内容が要約または削除される
Turn 12-: モデルは file.txt の詳細を「忘れている」(蒸発したから)
※ ファイルがディスク上で変更されても、履歴の内容は変更時点のスナップショットのまま保持される
@展開の特性まとめ:
| 項目 | 挙動 |
|---|---|
| 展開のアクション |
一度だけ(@filename と書いた時のみ) |
| 展開された内容 | 履歴に残り続ける(コンパクションまで) |
| ファイル変更の反映 | されない(履歴は変更時点のスナップショット) |
| 投入場所 | ユーザー発言の直後のアタッチメント |
| 寿命 | コンパクションで要約または削除される |
2. CLAUDE.md 投入——階層的な優先順位と矛盾の解決
2.1 投入順序の全体像
CLAUDE.md は、src/utils/claudemd.ts の getMemoryFiles 関数で自動的にロードされる。コメント(1-26行目)に明確な仕様が記載されている:
/**
* Files are loaded in the following order:
*
* 1. Managed memory (eg. /etc/claude-code/CLAUDE.md) - Global instructions for all users
* 2. User memory (~/.claude/CLAUDE.md) - Private global instructions for all projects
* 3. Project memory (CLAUDE.md, .claude/CLAUDE.md, and .claude/rules/*.md in project roots) - Instructions checked into the codebase
* 4. Local memory (CLAUDE.local.md in project roots) - Private project-specific instructions
*
* Files are loaded in reverse order of priority, i.e. the latest files are highest priority
* with the model paying more attention to them.
*/
2.2 ロードの順序と実装
src/utils/claudemd.ts:790- の getMemoryFiles 関数は、以下の順序でファイルをロードする:
// 1. Managed memory(ポリシー設定、全ユーザー向け)
const managedClaudeMd = getMemoryPath('Managed')
result.push(...(await processMemoryFile(managedClaudeMd, 'Managed', processedPaths, includeExternal)))
// Managed .claude/rules/*.md も同様にロード
const managedClaudeRulesDir = getManagedClaudeRulesDir()
result.push(...(await processMdRules({ rulesDir: managedClaudeRulesDir, type: 'Managed', ... })))
// 2. User memory(ユーザーのホームディレクトリ、全プロジェクト向け)
if (isSettingSourceEnabled('userSettings')) {
const userClaudeMd = getMemoryPath('User')
result.push(...(await processMemoryFile(userClaudeMd, 'User', processedPaths, includeExternal)))
}
// 3. Project & Local memory(カレントディレクトリから上位へ走査)
// ディレクトリ階層を下から上へ走査し、各ディレクトリで:
// - CLAUDE.md
// - .claude/CLAUDE.md
// - .claude/rules/*.md
// - CLAUDE.local.md
// をロード
ファイルは優先度の逆順でロードされ、最後にロードされたファイルが最も高い優先度を持つ。 モデルは後からロードされたファイルにより注意を払う。
2.3 カレントディレクトリの走査順序
Project と Local メモリは、カレントディレクトリから上位へ走査される。つまり、カレントディレクトリに近いファイルほど後でロードされ、より高い優先度を持つ。
例:/home/user/project/subdir/deep/ で作業している場合
1. /home/user/project/CLAUDE.md (最初にロード、優先度: 低)
2. /home/user/project/subdir/CLAUDE.md (次にロード、優先度: 中)
3. /home/user/project/subdir/deep/CLAUDE.md (最後にロード、優先度: 高)
2.4 矛盾する内容がある場合の解決
CLAUDE.md ファイルに矛盾する内容がある場合、後からロードされたファイルの指示が優先される。
これは LLM の注意メカニズムに依存する設計で、プロンプト内で後に登場する情報ほど、モデルはより重視する傾向がある。
具体例:
-
~/.claude/CLAUDE.mdに「常にJavaScriptを使う」と書かれている -
/project/subdir/CLAUDE.mdに「このディレクトリではTypeScriptを使う」と書かれている
→ /project/subdir/ 配下で作業する場合、TypeScript の指示が優先される
2.5 @include ディレクティブ
CLAUDE.md ファイル内で @path 記法を使うと、他のファイルをインクルードできる:
<!-- CLAUDE.md -->
## 基本ルール
@./base-rules.md
## プロジェクト固有ルール
@~/shared-config/project-style.md
src/utils/claudemd.ts:451-535 の extractIncludePathsFromTokens 関数が、マークダウンの text ノードから @path パターンを抽出する。
構文:
-
@pathまたは@./relative/path: 相対パス -
@~/path: ホームディレクトリからのパス -
@/absolute/path: 絶対パス
特性:
- インクルードされたファイルは、インクルード元ファイルの前に追加される
- 循環参照は
processedPathsSet によって防止される - 存在しないファイルは静かに無視される
- コードブロック内の
@pathは無視される(Lexer のトークン解析による)
2.6 HTML コメントの自動除去
CLAUDE.md 内のブロックレベル HTML コメント(<!-- ... -->)は、自動的に除去される:
// claudemd.ts:292-301(stripHtmlComments)、303-334(stripHtmlCommentsFromTokens)
export function stripHtmlComments(content: string): {
content: string
stripped: boolean
} {
if (!content.includes('<!--')) {
return { content, stripped: false }
}
return stripHtmlCommentsFromTokens(new Lexer({ gfm: false }).lex(content))
}
これにより、CLAUDE.md に開発者向けのメモを残しつつ、モデルには見せないようにできる:
<!-- TODO: この部分を後で改善する -->
プロジェクトでは bun を使っています。
モデルには「プロジェクトでは bun を使っています。」のみが届く。
制約:
- 未閉鎖のコメント(
<!--だけで-->がない)はそのまま残る - インラインのコメント(段落内の
<!-- ... -->)も残る - コードブロック内のコメントは保護される
2.7 フロントマターによる条件付き投入
CLAUDE.md や .claude/rules/*.md ファイルは、frontmatter で paths を指定することで、特定のファイルパスに対してのみアクティブにできる:
---
paths:
- src/api/**
- src/services/**
---
このディレクトリの API コードでは、必ず OpenAPI スキーマに準拠してください。
src/utils/claudemd.ts:254-279 の parseFrontmatterPaths 関数が、paths フィールドを解析し、glob パターンに変換する。
マッチングの仕組み:
-
ignoreライブラリを使用(gitignore 形式のマッチング) -
path/**の/**サフィックスは自動的に削除される -
**のみ(match-all)の場合は、paths 指定なしとして扱われる
CLAUDE.md 投入の特性まとめ:
| 項目 | 挙動 |
|---|---|
| 投入回数 | セッション開始時に一度、コンパクション後に再投入 |
| 投入順序 | Managed → User → Project(root → cwd) → Local |
| 優先順位 | 後からロードされたファイルが高い優先度 |
| 矛盾の解決 | LLM の注意メカニズムに依存(後の指示を重視) |
| 条件付き投入 | frontmatter の paths で特定ファイルパスに限定可能 |
| HTML コメント | ブロックレベルコメントは自動除去 |
| @include | 他ファイルをインクルード可能、循環参照は防止 |
3. Skills 投入——動的な発見と永続性
3.1 Skills のロードタイミング
Skills は、ファイル操作(Read/Write/Edit)が起点となって動的にロードされる。
トリガーの仕組み(2 段階):
第 1 段階:ディレクトリ発見
Read/Write/Edit ツールが実行されると、discoverSkillDirsForPaths 関数が呼ばれる:
- 操作されたファイルのディレクトリから開始
- CWD まで上方向に階層を走査
- 各ディレクトリで
.claude/skillsの存在を確認 - 見つかったら、バックグラウンドでスキルをロード
// src/tools/FileReadTool/FileReadTool.ts:579-587
const newSkillDirs = await discoverSkillDirsForPaths([fullFilePath], cwd)
if (newSkillDirs.length > 0) {
for (const dir of newSkillDirs) {
context.dynamicSkillDirTriggers?.add(dir)
}
// バックグラウンドでロード(ファイル操作をブロックしない)
addSkillDirectories(newSkillDirs).catch(() => {})
}
重要: CWD レベルの .claude/skills はセッション開始時にロード済みなので、動的発見ではスキップされる。
第 2 段階:条件付きアクティベーション
activateConditionalSkillsForPaths 関数が、frontmatter の paths パターンでフィルタリング:
// src/skills/loadSkillsDir.ts:997-1040
export function activateConditionalSkillsForPaths(
filePaths: string[],
cwd: string,
): string[] {
for (const [name, skill] of conditionalSkills) {
if (skill.type !== 'prompt' || !skill.paths || skill.paths.length === 0) {
continue
}
const skillIgnore = ignore().add(skill.paths) // gitignore 形式
for (const filePath of filePaths) {
const relativePath = isAbsolute(filePath)
? relative(cwd, filePath)
: filePath
if (skillIgnore.ignores(relativePath)) {
// マッチ → アクティブ化
dynamicSkills.set(name, skill)
conditionalSkills.delete(name)
activated.push(name)
break
}
}
}
return activated
}
具体例:
# .claude/skills/api-helper/SKILL.md
---
paths:
- src/api/**
- src/services/**
---
このスキルは、src/api/user.ts や src/services/auth.ts を Read/Write/Edit した時にアクティブ化される。
トリガーの具体例
シナリオ:API ファイルを Read する
プロジェクト構造:
/home/user/project/
.claude/skills/
api-docs/
SKILL.md # API ドキュメント生成スキル(paths: src/api/**)
src/
api/
user.ts
components/
.claude/skills/
react-refactor/
SKILL.md # React リファクタリングスキル(paths: src/components/**)
ユーザーが「src/api/user.ts を読んで」と依頼し、モデルが Read ツールを実行:
第 1 段階:ディレクトリ発見
discoverSkillDirsForPaths が実行される(src/skills/loadSkillsDir.ts:861-915):
- ファイルのディレクトリから開始:
/home/user/project/src/api/ - 上方向に走査(CWD まで):
/home/user/project/src/api/.claude/skills → 存在しない /home/user/project/src/.claude/skills → 存在しない /home/user/project/.claude/skills → CWD レベルなのでスキップ - CWD レベルのスキルは起動時にロード済みなのでスキップされる
- gitignore チェック:node_modules 内のスキルなどを防ぐ
第 2 段階:条件付きアクティベーション
すでにロード済みの api-docs スキルの frontmatter をチェック:
---
paths:
- src/api/**
---
ファイルパス src/api/user.ts を CWD からの相対パスに変換し、ignore ライブラリでマッチング:
const skillIgnore = ignore().add(skill.paths)
if (skillIgnore.ignores('src/api/user.ts')) {
// マッチ!→ アクティブ化
dynamicSkills.set(name, skill)
}
結果: api-docs スキルがアクティブ化される
重要な設計判断
-
CWD レベルのスキルはセッション開始時にロード
- 理由:CWD は作業の起点なので、最初からスキルが利用可能であるべき
-
深いディレクトリのスキルが優先
- スキルディレクトリは深さでソートされる(深い順)
- 理由:カレントディレクトリに近いスキルほど、具体的な文脈を持つ
-
gitignore されたディレクトリはスキップ
-
node_modules/pkg/.claude/skillsのようなサードパーティーのスキルを防ぐ
-
-
バックグラウンドでロード
- ファイル操作(Read/Write/Edit)をブロックしない
- スキルロードは次のターンで通知される
3.2 通知は次のターン
ツール実行が完了し、LLM への次のリターン(フォローアップ)を作成する際、src/utils/attachments.ts:874 の getDynamicSkillAttachments が呼ばれる:
maybe('dynamic_skill', () => getDynamicSkillAttachments(context)),
getDynamicSkillAttachments 関数(2547-2601行目)は、dynamicSkillDirTriggers に基づいてスキルディレクトリを並列読み込みし、新規発見されたスキルのアタッチメントを生成する。
つまり:ファイル操作した Turn では認識されず、次の Turn で通知される。
3.3 永続性と揮発性の二面性
セッション内永続(Registry)
一度発見されたスキルは、src/skills/loadSkillsDir.ts:981-983 の dynamicSkills Map に保持される:
export function getDynamicSkills(): Command[] {
return Array.from(dynamicSkills.values())
}
この Map は、セッション中(REPL実行中など)はメモリ上に残り続ける。LLM が Skill ツールを呼び出せば、いつでも利用可能。
通知の揮発
スキル発見の**通知(アタッチメント)**自体は、履歴の一部であるため、コンパクションによって消える可能性がある。
しかし、src/utils/attachments.ts:2607-2632 の sentSkillNames という仕組みがあり、同一セッション内で一度通知したスキルを何度も通知しないようになっている:
// Track which skills have been sent to avoid re-sending. Keyed by agentId
const sentSkillNames = new Map<string, Set<string>>()
重要:コンパクション時に sentSkillNames はリセットされない。
src/services/compact/compact.ts:524-529 のコメント:
// Intentionally NOT resetting sentSkillNames: re-injecting the full
// skill_listing (~4K tokens) post-compact is pure cache_creation with
// marginal benefit. The model still has SkillTool in its schema and
// invoked_skills attachment (below) preserves used-skill content.
これにより、コンパクション後も約 4K トークン/イベントを節約する。
3.4 レジューム時の特別扱い
セッションをレジューム(--resume)する際、src/utils/conversationRecovery.ts:395-401 の処理が走る:
// A prior process already injected the skills-available reminder — it's
// in the transcript the model is about to see. sentSkillNames is
// process-local, so without this every resume re-announces the same
// ~600 tokens. Fire-once latch; consumed on the first attachment pass.
if (message.attachment.type === 'skill_listing') {
suppressNextSkillListing()
}
トランスクリプト内に skill_listing アタッチメントが既に存在する場合、次回の投入を抑制する。これにより、レジューム時に約 600 トークンの重複を防ぐ。
3.5 ファイルとしての永続化
プロジェクト内の .claude/skills/ やユーザーホームの ~/.claude/skills/ にあるファイルは当然永続する。
しかし、Claude Code が「学習」して記憶に焼き付けるような永続化(Vector DB 等への保存)は行わない。あくまで「そのセッションで利用可能なツール」としてメモリ上に展開されるのみ。
3.6 paths パターンの完全ガイド
Skills の frontmatter paths フィールドでは、gitignore と同じ構文が使える(ignore ライブラリを使用)。
基本パターン
ディレクトリ全体:
---
paths:
- src/api/**
- src/services/**
---
特定のファイル名パターン:
---
paths:
- src/components/**/*Modal.tsx
- src/components/**/*Dialog.tsx
---
マッチ例:
-
src/components/UserModal.tsx✅ -
src/components/auth/LoginModal.tsx✅ -
src/components/Button.tsx❌
拡張子指定:
---
paths:
- "**/*.test.ts"
- "**/*.spec.ts"
---
直下のファイルのみ:
---
paths:
- src/*.config.js
- "*.json"
---
マッチ例:
-
src/babel.config.js✅ -
src/api/config.js❌(サブディレクトリ)
複数の拡張子:
---
paths:
- "src/**/*.{ts,tsx}"
- "tests/**/*.{js,jsx}"
---
除外パターン:
---
paths:
- src/**
- "!src/legacy/**"
- "!**/*.test.ts"
---
マッチ例:
-
src/api/user.ts✅ -
src/legacy/old.ts❌(除外) -
src/api/user.test.ts❌(除外)
プレフィックス/サフィックス:
---
paths:
- "**/*Controller.ts"
- "**/test_*.py"
- "**/*.generated.*"
---
マッチ例:
-
src/api/UserController.ts✅ -
tests/test_auth.py✅ -
src/schema.generated.ts✅
高度なパターン
複数条件の組み合わせ:
---
paths:
# React コンポーネントかつ Modal/Dialog
- "src/components/**/*{Modal,Dialog}.{tsx,jsx}"
# API ルートかつテストファイルではない
- "src/api/**/*.ts"
- "!src/api/**/*.test.ts"
# スタイルファイル(CSS/SCSS/LESS)
- "**/*.{css,scss,less,sass}"
---
よくある間違い
❌ 絶対パスは使えない:
---
paths:
- /home/user/project/src/api/** # NG
---
理由:パスは CWD からの相対パスで評価される
❌ .. で親ディレクトリは参照できない:
---
paths:
- ../sibling-project/** # NG
---
理由:コードで明示的にスキップされる(src/skills/loadSkillsDir.ts:1021-1027)
❌ ~ でホームディレクトリは参照できない:
---
paths:
- ~/project/** # NG
---
理由:~ は展開されない。CWD からの相対パスのみ。
❌ 広すぎるパターンは避ける:
---
paths:
- "**" # NG:全ファイルにマッチ
---
理由:paths なしと同じ扱いになる。特定の目的に絞るべき。
実用例
React コンポーネント最適化スキル:
---
name: React Performance Optimizer
paths:
- "src/components/**/*.{tsx,jsx}"
- "src/pages/**/*.{tsx,jsx}"
- "!src/**/*.test.{tsx,jsx}"
- "!src/**/*.stories.{tsx,jsx}"
---
このスキルは React コンポーネントのパフォーマンス最適化を支援します。
API エンドポイント文書化スキル:
---
name: API Documentation Helper
paths:
- "src/api/**/routes/**/*.ts"
- "src/api/**/controllers/**/*.ts"
- "!**/*.test.ts"
---
API エンドポイントの OpenAPI ドキュメントを生成します。
データベースマイグレーションスキル:
---
name: Migration Helper
paths:
- "**/migrations/**/*.sql"
- "**/migrations/**/*.ts"
- "prisma/schema.prisma"
---
データベースマイグレーションファイルの作成を支援します。
Skills 投入の特性まとめ:
| 項目 | 挙動 |
|---|---|
| トリガー | Read/Write/Edit ツールの実行 |
| 発見条件 | ファイルのディレクトリ階層に .claude/skills が存在 |
| アクティブ化条件 | スキルの frontmatter paths パターンがファイルパスにマッチ |
| マッチング形式 | gitignore 形式(ignore ライブラリ) |
| 通知タイミング | 次のターンの getAttachmentMessages
|
| セッション内保持 |
dynamicSkills Map に登録され、セッション終了まで有効 |
| 通知の揮発 | アタッチメントはコンパクションで消えるが、機能は残る |
| 再通知の抑制 |
sentSkillNames により、一度通知したスキルは再通知されない |
| コンパクション時 |
sentSkillNames はリセットされない(約 4K トークン節約) |
| レジューム時 | 既存の skill_listing を検出し、重複を防ぐ |
4. まとめ——自動投入の設計思想
Claude Code の自動投入メカニズムは、以下の 3 つの原則で設計されている:
4.1 「展開」と「内容」の使い分け
-
@シンボル展開:
-
展開アクション:一度きり(
@filenameと書いた瞬間のみ) - 展開された内容:履歴に残る(コンパクションまで)
- 用途:ユーザーが明示的に参照したいファイル
-
展開アクション:一度きり(
-
CLAUDE.md:
- ロードアクション:セッション開始時とコンパクション後に再投入
- 内容:常にシステムプロンプトの一部として存在
- 用途:プロジェクト全体に適用される永続的な命令
-
Skills:
- 発見アクション:ファイル操作時に一度
- 通知:次のターンで一度だけ
-
機能:セッション中は永続(
dynamicSkillsMap に保持) - 用途:条件付きで提供される動的なツール
4.2 階層的な優先順位
CLAUDE.md は、以下の階層構造で優先順位が決定される:
低い優先度
↓
Managed memory(全ユーザー向け)
↓
User memory(全プロジェクト向け)
↓
Project memory(プロジェクトルート → カレントディレクトリ)
↓
Local memory(プライベートなプロジェクト固有設定)
↓
高い優先度
矛盾する指示がある場合、後からロードされたファイル(より高い優先度)が勝つ。これは LLM の注意メカニズムに依存する。
4.3 トークン効率の最大化
- @include によるファイル再利用
- HTML コメントの自動除去
- sentSkillNames によるスキル再通知の抑制(約 4K トークン/コンパクション節約)
- コンパクション時の再投入は、CLAUDE.md のみで Skills は通知されない
4.4 ファイル操作をトリガーとした動的な拡張
Skills の動的ロードは、「ファイルに触れた時点で、そのファイルに関連するツールを提供する」という思想を体現している。
これにより、プロジェクト全体の Skills を最初から投入する必要がなく、必要なものだけを必要なタイミングで提供できる。