InstructionsLoaded hook fires 3x per file per compact event (causes 3x context reload waste)
Summary
The InstructionsLoaded hook fires 3 times per instruction file per single user-experience compact event, with identical memory_type and load_reason per emission. Net effect: per-compact reload bytes-into-context is roughly 3× what naive accounting would suggest.
Evidence (from one project's telemetry, ~280 sessions)
I built an InstructionsLoaded → DuckDB pipeline (ops.instructions_loaded table) following the documented file_path / memory_type / load_reason / bytes / globs / trigger_file_path / parent_file_path / cwd schema. After 1,000+ events captured, the pattern is:
For session 663a7924-1e60-448a-81b3-c1063f1c4060:
| File | load_reason | memory_type | distinct loads in single compact event |
|---|---|---|---|
| .claude/rules/coaching-style.md | compact | Project | 3 |
| .claude/rules/memory-management.md | compact | Project | 3 |
| ~/.claude/CLAUDE.md | compact | User | 3 |
| .claude/rules/anti-sycophancy-contract.md | compact | Project | 3 |
| (every other file in .claude/rules/) | compact | Project | 3 |
Same pattern at session_start for some sessions (2-3× per file), single-emission for others.
Reproducer (any project with multiple .claude/rules/*.md files):
# .claude/hooks/instructions-loaded-logger.py
import json, sys, time
payload = json.loads(sys.stdin.read())
with open("/tmp/il_log.jsonl", "a") as f:
f.write(json.dumps({
"ts": time.time(),
"file_path": payload.get("file_path"),
"load_reason": payload.get("load_reason"),
"memory_type": payload.get("memory_type"),
}) + "\n")
Wire under InstructionsLoaded in .claude/settings.json. Run a session that triggers a compact. Inspect /tmp/il_log.jsonl — each file path appears 3× per compact event.
Why it matters
For repos with 20-50 .claude/rules/*.md files (~150 KB total), every compact event emits ~450 KB of InstructionsLoaded callbacks corresponding to ~450 KB of context reload. Combined with the 5-minute prompt-cache TTL default (#46829), this creates a positive-feedback loop: compaction triggers near-saturation context reload, which accelerates the next compaction.
Measured impact in one project (~50 active rules): ~87 KB → ~261 KB instruction reload per compact (3× multiplier confirmed). Roughly half of total session-level context overhead.
Expected behavior
Each instruction file should fire InstructionsLoaded exactly once per load event, matching the user-experience semantics ("compact happened, context was reloaded").
Suggested fix paths
- De-duplicate at emission: only fire
InstructionsLoadedonce per(session_id, file_path, load_reason, parent_file_path)tuple per logical event.
- Surface the multi-pass intent: if 3× is intentional (scan/validate/load phases), expose a
phasefield in the hook payload so callers can de-duplicate or log meaningfully.
- Document current behavior under InstructionsLoaded in https://docs.claude.com/en/docs/claude-code/hooks so users instrumenting it know to expect multi-emission.
Related
- #46829 (cache TTL regression) — same architectural area; combined effect compounds context burn
- #38524 (ContextThreshold hook proposal) — would let users react to saturation but doesn't address upstream waste
- #49226 (context_utilization API request) — observability layer that would make this trivially visible
Environment
- Claude Code: latest stable (Apr 2026)
- Platform: macOS (Darwin 25.4.0)
- ~5,300 instruction file load events captured in DuckDB before reporting
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗