bypassPermissions で動かしたセッションが、.claude/rules/api.md に書いた規約を一度も参照しませんでした。ファイルの置き場所も paths の glob も合っていて、/context にも異常は出ません。原因は Claude がそのファイルを Read ツールではなく cat -n で開いていたことでした。
paths 付きルールは Read ツールが動いた時だけ載る
公式ドキュメント “How Claude remembers your project” の “Path-specific rules” にはこうあります。
Path-scoped rules trigger when Claude reads files matching the pattern, not on every tool use.
実測では、この「reads」に該当したのは Read ツールの呼び出しだけでした。Bash で cat しても、Write で新規作成しても、この判定は走りません。Claude Code v2.1.266 で InstructionsLoaded フックを仕込み、操作ごとに何が載るかを記録しました。
検証用プロジェクトの構成
/tmp/rules-lab/
├── CLAUDE.md
├── .claude/
│ ├── settings.json # InstructionsLoaded と PostToolUse のフック
│ └── rules/
│ ├── always.md # paths なし
│ └── api.md # paths: src/api/**/*.py
└── src/
├── api/handler.py
├── db/CLAUDE.md # 入れ子の CLAUDE.md
├── db/models.py
└── util.py
api.md は 6 行です。
---
paths:
- "src/api/**/*.py"
---
# API rule
- Every handler must return a dict with a "status" key.
起動時に載るのは paths なしのルールだけ
claude -p "/context" の Memory Files 表に api.md は出ません。
### Memory Files
| Type | Path | Tokens |
|---------|------------------------------------------------|--------|
| User | /Users/moha/.claude/CLAUDE.md | 894 |
| Project | /private/tmp/rules-lab/CLAUDE.md | 28 |
| Project | /private/tmp/rules-lab/.claude/rules/always.md | 21 |
フックのログも同じ 3 ファイルが load_reason=session_start で並ぶだけでした。”Set up rules” の “Rules without paths frontmatter are loaded at launch with the same priority as .claude/CLAUDE.md” の通りで、always.md は CLAUDE.md と同格に扱われます。
Read が走った直後に path_glob_match で載る
「Read ツールで src/api/handler.py を読んで DONE と返せ」と指示した回のログです(--model sonnet、2 ターン、$0.099)。
[LOAD] reason=session_start file=.claude/rules/always.md
[LOAD] reason=session_start file=CLAUDE.md
[LOAD] reason=session_start file=/Users/moha/.claude/CLAUDE.md
[TOOL] Read {'file_path': '/private/tmp/rules-lab/src/api/handler.py'}
[LOAD] reason=path_glob_match file=.claude/rules/api.md
globs=['src/api/**/*.py'] trigger=src/api/handler.py
Read の PostToolUse の直後に api.md が path_glob_match で載り、trigger_file_path には読んだファイルが入ります。同じ指示を cat src/api/handler.py に変えると、最後の 2 行が消えます(2 ターン、$0.080)。
[LOAD] reason=session_start file=.claude/rules/always.md
[LOAD] reason=session_start file=CLAUDE.md
[LOAD] reason=session_start file=/Users/moha/.claude/CLAUDE.md
[TOOL] Bash {'command': 'cat src/api/handler.py'}
ファイルの中身は両方ともモデルに渡っています。違うのは api.md が一緒に渡ったかどうかだけ。
InstructionsLoaded フックで載った理由を記録する
InstructionsLoaded は v2.1.69 で入ったフックイベントで、CLAUDE.md か .claude/rules/*.md がコンテキストに載るたびに発火します。同じバージョンで、claude -p でも paths 付きルールが載るよう直っています。ヘッドレスで検証するならこれ以降のバージョンを使ってください。
settings.json とログ用スクリプト
{
"hooks": {
"InstructionsLoaded": [
{ "hooks": [ { "type": "command", "command": "python3 /tmp/rules-lab/.claude/hooklog.py" } ] }
],
"PostToolUse": [
{ "hooks": [ { "type": "command", "command": "python3 /tmp/rules-lab/.claude/hooklog.py" } ] }
]
}
}
import sys, time, json
d = json.loads(sys.stdin.read())
with open("/tmp/rules-lab/hook.log", "a") as f:
f.write(json.dumps({"t": time.time(), "d": d}) + "\n")
PostToolUse を同じログに流しておくと、どのツールの直後にどのファイルが載ったかを 1 本の時系列で追えます。フックは project の .claude/settings.json に置いただけで発火しました。
load_reason と trigger_file_path
入力 JSON には共通フィールドに加えて file_path、memory_type(User / Project / Local / Managed)、load_reason、globs、trigger_file_path、parent_file_path が入ります。load_reason は session_start、nested_traversal、path_glob_match、include、compact の 5 値。matcher に書くのはこの値です。globs と trigger_file_path が入るのは遅延ロードの時だけです。
このフックに decision control はありません。”InstructionsLoaded decision control” の節が “They can’t block or modify instruction loading” と明記していて、exit 2 を返しても何も止まりません。用途は記録に限られます。
ツール別の読み込み結果
同じ検証プロジェクトで、操作ごとに何が載ったかを並べました。
| 操作 | 載ったファイル | load_reason |
|---|---|---|
| Read で src/api/handler.py を読む | api.md | path_glob_match |
| Bash で cat src/api/handler.py | なし | – |
| Write で src/api/new_handler.py を新規作成 | なし | – |
| Read で src/db/models.py を読む | src/db/CLAUDE.md | nested_traversal |
| Grep / Glob ツール | ツール自体が提供されず未検証 | – |
Write の行は、src/api/ に新しいハンドラを作らせる依頼で効いてきます。読む対象が無いので、api.md は最後まで載りません。入れ子の CLAUDE.md も同じ仕組みで、Read が走った時に nested_traversal として載ります。Grep と Glob は手元の -p セッション(--model sonnet)ではモデルに提供されていませんでした。指示するとモデルは「Grep ツールはこの環境では利用できません」と返します。
bypassPermissions では同じ依頼で cat が選ばれる
ここまでは「Read を使え」「cat を使え」と明示していました。指示を外して “Read src/api/handler.py and tell me in one sentence what handle() returns.” とだけ頼み、既定モデルの Claude Fable 5.1 でモードだけ変えます。
同じ依頼で回答が変わる
--permission-mode bypassPermissions では 2 回とも cat -n が選ばれ、api.md は載りませんでした。--permission-mode default では Read が選ばれ、回答に規約違反の指摘まで付きます。
# bypassPermissions (2回とも同じ)
[TOOL] Bash {'command': 'cat -n /private/tmp/rules-lab/src/api/handler.py'}
→ `handle()` ignores its `event` argument and always returns
the dictionary `{"message": "hello"}`.
# default
[TOOL] Read {'file_path': '/private/tmp/rules-lab/src/api/handler.py'}
[LOAD] reason=path_glob_match file=.claude/rules/api.md
→ `handle()` returns a fixed dict, `{"message": "hello"}`, regardless of
the `event` argument. Note that this violates the project's API rule in
`.claude/rules/api.md`, which requires every handler to return a dict
containing a `"status"` key.
読んだファイルは同じ 3 行。api.md が一緒に渡ったかどうかで、回答の中身まで変わりました。この差の原因を、Zenn の odacchi 氏の記事はバイナリ内の未公開フラグとシステムプロンプトの文言まで追っています。公式ドキュメントに記載がないため、この記事ではフラグの操作は扱いません。
–append-system-prompt では戻らなかった
bypassPermissions のまま --append-system-prompt "Read files with the Read tool, not with cat or sed." を付けても、モデルは cat を選びました(1 回、$0.43)。システムプロンプト末尾への追記 1 行では、既定の振る舞いを覆せていません。
守らせたい規約は paths を外すか PreToolUse で止める
“CLAUDE.md vs auto memory” の節に前提が書いてあります。
Claude treats them as context, not enforced configuration. To block an action regardless of what Claude decides, use a PreToolUse hook instead.
paths 付きルールは、載った時だけ効く文脈です。Read が走らなければ規約は無いのと同じ。置き場所かフックで、引き金の方を変えます。
paths なしルールに Read の指示を書く
アンチパターンは、必須の規約を paths 付きファイルだけに置く構成です。
# .claude/rules/api.md (paths: src/api/**/*.py)
- Every handler must return a dict with a "status" key.
- Read files with the Read tool. # ここに書いても、載る前に cat される
Read の指示を置くのは、起動時に載るファイルの方。always.md に 1 行足して、bypassPermissions で同じ依頼を出します。モデルは Read を選び、api.md が path_glob_match で載りました(1 回、$0.36)。
# .claude/rules/always.md (paths なし)
- Reply in English.
- Read files with the Read tool. Never use cat, head, or sed to read files.
ただしこれも文脈です。守られる保証まで要るなら、どうするか。
PreToolUse で cat を exit 2 にする
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(cat *)",
"command": "echo 'Use the Read tool instead of cat so that path-scoped rules load' >&2; exit 2"
}
]
}
]
if で cat から始まるコマンドだけに絞り、exit 2 で止めて stderr の文言をモデルに返します。PreToolUse は bypassPermissions でも動く前提の仕組みです。トランスクリプトには、cat が止められて Read に切り替わる流れがそのまま残りました(3 ターン、$0.097)。
tool_use: Bash {"command": "cat -n /private/tmp/rules-lab/src/api/handler.py"}
tool_result: PreToolUse:Bash hook error: [...]: Use the Read tool instead of cat
so that path-scoped rules load
tool_use: Read {"file_path": "/private/tmp/rules-lab/src/api/handler.py"}
tool_result: 1 def handle(event):
2 return {"message": "hello"}
[LOAD] reason=path_glob_match file=.claude/rules/api.md
フックの型と exit code の扱いはClaude Code Hooksのprompt型/agent型とは—5つの使い分けにまとめています。Write で新規作成する経路はこのフックでは拾えません。新規ファイルに効かせたい規約は、paths を外して常時載せておくしかありません。
rules と skills の違い
検索サジェストに並ぶ「rules skills 違い」には、”Organize rules with .claude/rules/” の Note が一文で答えています。
Rules load into context every session or when matching files are opened. For task-specific instructions that don’t need to be in context all the time, use skills instead, which only load when you invoke them or when Claude determines they’re relevant to your prompt.
| rules | skills | |
|---|---|---|
| 載るタイミング | 起動時、または paths に一致するファイルの Read 時 | /名前 で呼んだ時、またはモデルが関連すると判断した時 |
| 置き場所 | .claude/rules/*.md、~/.claude/rules/ | .claude/skills/<name>/SKILL.md |
| 向く内容 | 常に守る規約、特定ディレクトリの規約 | 手順が長いタスク、たまにしか使わない作業 |
skills は入れただけで毎ターン説明文がコンテキストに載り続けます。その量はClaude Code /skill-doctorの読み方—未使用スキル15本の消し方で測りました。
paths の glob と 1,000 パターンの予算
brace 展開と角括弧
paths は YAML のリストで、src/**/*.{ts,tsx} のような brace 展開が使えます。展開後のパターン数は 1 ファイルの paths 全体で 1,000 個・4 MiB が上限です。超えたパターンは展開されず、literal な { として扱われて何にも一致しません。v2.1.217 より前は brace が多いと起動時に CLI が固まっていました。[ は bracket expression の開始と解釈されるので、ファイル名に含まれる [ は \[ とエスケープします。
symlink・ユーザー階層・compact 後
.claude/rules/ 配下の symlink は解決して読まれ、循環 symlink も検出されて止まりません。v2.1.198 からは、symlink 経由でファイルに到達した場合も paths の一致判定が働きます。~/.claude/rules/ はプロジェクトのルールより先に載り、衝突したら後に載った方が勝ちます。--setting-sources から project を外すと、プロジェクトのルールは丸ごと読まれません。手元でも /context の Memory Files はユーザーの CLAUDE.md 1 行だけになりました。/compact 後の扱いは “Instructions seem lost after /compact” にあります。プロジェクト直下の CLAUDE.md は再注入されますが、paths 付きルールは次に Read が走るまで戻りません。共有ルールを symlink で繋ぐ構成はClaude CodeはAGENTS.mdを読まない—@importとsymlinkで繋ぐで扱いました。
まとめ
- paths 付きルールが載るのは Read ツールが一致ファイルを読んだ時だけで、Bash の cat と Write の新規作成では載らない
- bypassPermissions では同じ依頼で 2 回とも cat が選ばれ、default モードとは回答の内容まで変わった
- InstructionsLoaded フックの load_reason と trigger_file_path を見れば、載った理由と引き金のファイルが分かる
- 守らせたい規約は paths なしルールに置くか、PreToolUse で cat を exit 2 にして Read へ寄せる
検証は Claude Code v2.1.266、有料の -p 実行 15 回で合計 $3.53 でした。