Log which CLAUDE.md files Claude Code loads, and when, with the InstructionsLoaded hook

Claude Code Published:

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.

Verified on Sep 10, 2026 These tools change quickly. Please also check the latest official documentation.
Contents
  1. When it fires and how to match
  2. The input JSON
  3. A logging configuration
  4. What to use it for
  5. Summary

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_reason values 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_reasonMeaning
session_startloaded at session start
nested_traversalloaded while traversing into a subdirectory
path_glob_matchloaded because a paths: glob matched
includeloaded through an @path include
compactre-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:

FieldContents
file_pathabsolute path of the instruction file that was loaded
memory_typescope: "User", "Project", "Local", or "Managed"
load_reasonone of the values above
globsthe paths: globs; present only for path_glob_match loads
trigger_file_paththe file whose access triggered a lazy load
parent_file_pathfor 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 globs and trigger_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/*.md loads
  • 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, and parent_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.