InstructionsLoaded hook fires 3x per file per compact event (causes 3x context reload waste)

Resolved 💬 1 comment Opened Apr 23, 2026 by shardin2 Closed May 27, 2026

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

  1. De-duplicate at emission: only fire InstructionsLoaded once per (session_id, file_path, load_reason, parent_file_path) tuple per logical event.
  1. Surface the multi-pass intent: if 3× is intentional (scan/validate/load phases), expose a phase field in the hook payload so callers can de-duplicate or log meaningfully.
  1. 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

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗