Log which CLAUDE.md files Claude Code loads, and when, with the InstructionsLoaded hook
Use the InstructionsLoaded hook to record every CLAUDE.md and .claude/rules/ file as it enters context: matcher values, input JSON fields, a logging example, and its limits.
Contents
When you are unsure whether a rule is really loaded, or when a subdirectory CLAUDE.md came into play, the /context list shows the current state but not the timeline. Claude Code's InstructionsLoaded hook fires every time an instruction file enters context and hands you the file path, the reason, and the file that triggered it.
In short: register a command under hooks.InstructionsLoaded with a load_reason matcher, parse the JSON on stdin with jq, and append it to a log. You then have a record of which instructions took effect and when.
KEY POINT
What you will learn
- When InstructionsLoaded fires and the
load_reasonvalues usable in a matcher - The input JSON fields (
file_path,memory_type,globs,trigger_file_path, and more) - A logging hook configuration, and what this hook cannot do
When it fires and how to match
InstructionsLoaded fires when a CLAUDE.md or .claude/rules/*.md file is loaded into context: once at session start for eagerly loaded files, and again during the session when Claude accesses a subdirectory with a nested CLAUDE.md or when a rule with paths: frontmatter matches.
The matcher runs against load_reason.
load_reason | Meaning |
|---|---|
session_start | loaded at session start |
nested_traversal | loaded while traversing into a subdirectory |
path_glob_match | loaded because a paths: glob matched |
include | loaded through an @path include |
compact | re-loaded after a compaction event |
"matcher": "session_start" limits it to startup; "matcher": "path_glob_match|nested_traversal" limits it to lazy loads.
用語解説
Asynchronous event: InstructionsLoaded runs in the background without pausing Claude. Exit code 2 and JSON output fields such as systemMessage and continue are discarded, so it cannot block or modify loading.
The input JSON
On top of the common fields (session_id, transcript_path, cwd, hook_event_name, and so on), the hook receives:
| Field | Contents |
|---|---|
file_path | absolute path of the instruction file that was loaded |
memory_type | scope: "User", "Project", "Local", or "Managed" |
load_reason | one of the values above |
globs | the paths: globs; present only for path_glob_match loads |
trigger_file_path | the file whose access triggered a lazy load |
parent_file_path | for include loads, the instruction file that included this one |
The official example input:
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../transcript.jsonl",
"cwd": "/Users/my-project",
"hook_event_name": "InstructionsLoaded",
"file_path": "/Users/my-project/CLAUDE.md",
"memory_type": "Project",
"load_reason": "session_start"
}
A logging configuration
This appends one line per load. Put it in .claude/settings.json to share with the team, or ~/.claude/settings.json for yourself.
{
"hooks": {
"InstructionsLoaded": [
{
"matcher": "session_start|nested_traversal|path_glob_match|include|compact",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/log-instructions.sh"
}
]
}
]
}
}
#!/bin/bash
# .claude/hooks/log-instructions.sh
input=$(cat)
file_path=$(jq -r '.file_path' <<<"$input")
reason=$(jq -r '.load_reason' <<<"$input")
scope=$(jq -r '.memory_type' <<<"$input")
trigger=$(jq -r '.trigger_file_path // ""' <<<"$input")
echo "$(date -Is) [$reason/$scope] $file_path ${trigger:+<- $trigger}" >> ~/.claude/instructions.log
exit 0
The script needs execute permission (chmod +x). The basics of hooks and the meaning of ${CLAUDE_PROJECT_DIR} are covered in the hub article Run lint and format automatically with hooks.
The log contains absolute paths
file_path and trigger_file_path are absolute, so your home directory name and project locations end up in the log. Be careful when sharing it. And because this hook cannot stop a load, use claudeMdExcludes when the goal is to keep another team's CLAUDE.md out.
What to use it for
- Confirming that a path-scoped rule fires for the files you expect (look at
globsandtrigger_file_path) - Seeing when a subdirectory CLAUDE.md was loaded (
nested_traversal) - Confirming instructions were re-injected after compaction (
compact) - Auditing which instruction files entered a session, for compliance
The loading rules themselves are explained in When does Claude Code load a CLAUDE.md in a subdirectory? and Scope rules to specific files with .claude/rules/.
Summary
- InstructionsLoaded is an asynchronous hook that fires whenever CLAUDE.md or
.claude/rules/*.mdloads - The matcher runs against
load_reason:session_start,nested_traversal,path_glob_match,include,compact - Input includes
file_path,memory_type,globs,trigger_file_path, andparent_file_path - It cannot block or modify loading; use it for logging, auditing, and observability
- To stop a file loading, use
claudeMdExcludes
FAQ
- Can an InstructionsLoaded hook block a CLAUDE.md from loading?
- No. The hook has no decision control; its exit code and JSON output are ignored. It is an asynchronous event for audit logging and observability.
- How do I fire only when a subdirectory CLAUDE.md loads?
- Use nested_traversal as the matcher. Add path_glob_match to include path-scoped rules: path_glob_match|nested_traversal.
- Does it tell me when CLAUDE.md is reloaded after compaction?
- Yes. That fires with load_reason set to compact.
Primary sources
This article was drafted by AI from official documentation and reviewed by the site operator before publishing. Found a mistake? Let us know via the contact page.