File Read Gate
What It Is
The File Read Gate stops Claude Code from reading a whole code file that claude-mem already has observations about. Claude gets the file’s observation timeline instead, with directions to cheaper sources:smart_outline for the file’s structure, smart_unfold for one symbol, get_observations for past work, and a Read with offset and limit for exact lines.
The gate is on by default. See How to Turn It Off.
This is progressive disclosure applied to file reads: show what exists first, then let Claude fetch only what it needs.
When It Blocks a Read
All of these must hold:- The session is Claude Code (not Qwen Code), and the Read comes from the main session, not a subagent.
- The file is code that
smart_outlinecan parse (see the table below). - The file is inside the project, meaning under the session’s working directory once symlinks are resolved.
smart_outlineandsmart_unfoldapply the same check, so a symlink in the project that points outside it is never blocked: those tools would refuse it. - The file is at least 32 KB. Smaller files with history get the timeline as context and are never blocked. See Size Thresholds.
- The file has observation history, and it has not been modified since the newest of those observations.
- The Read would return the whole file. Reads of part of a file always go through. See Whole-File vs Targeted Reads.
- The tree-sitter CLI that powers
smart_outlineandsmart_unfoldis installed. Without it, the Read goes through with the timeline as context, so Claude is never sent to a tool that can’t parse the file. Claude Code’s plugin install leaves this executable out, so the claude-mem worker downloads it from the tree-sitter GitHub releases in the background each time it starts without one. It only runs or installs a download whose SHA-256 matches the digest claude-mem pins for that tree-sitter version and platform. On a new install the gate turns on a few seconds into the first session. If the download can’t reach GitHub, the worker log says so; runnpx claude-mem repaironce the network allows it.
Markdown, YAML, TOML, JSON and every other file type are never blocked. Read is the right tool for them.
Qwen Code runs the Claude Code hooks, and Codex CLI and Kimi run the same hook, but none of them blocks: the hook adds the file’s timeline as context and lets the call through. Other hosts don’t run this hook.
How It Works
What Claude Sees
When the gate blocks a Read, Claude Code cancels it and gives Claude this message in its place. In the transcript it appears as aPreToolUse:Read hook error; that is the gate at work, not a failure.
<id> <time> <type> <title>, grouped by day, oldest first. Type icons: 🔴 bugfix, 🟣 feature, 🔄 refactor, 🔵 discovery, ⚖️ decision, ✅ change. With line anchors on, a line can end with where that observation’s code is now (@L212) or (code changed since).
The block goes to Claude, not to you. There is no approval prompt to click through. To let whole-file Reads through, turn the gate off.
Line Anchors
Off by default. WithCLAUDE_MEM_LINE_ANCHORS_ENABLED set to true, the timeline also says where in the file past work is now, so Claude can Read those lines with offset and limit instead of the whole file:
When a line was found, the context adds a direction to Read around the
@L number, and a block adds one before the timeline. The direction names Claude Code’s Read with offset and limit. Only Claude Code sessions record lines, so another host shows these rows only when its timeline includes Claude Code’s observations (CLAUDE_MEM_SESSION_START_INCLUDE_ALL_SOURCES).
How lines are recorded. When the worker stores an observation, it takes lines from the tool calls of the same batch, never from model output, and only in Claude Code sessions. For each Edit or MultiEdit edit it takes at most one line, inside the text that edit wrote, preferring the lines it changed, and only while that text occurs exactly once in the file. For each Read with an offset above 1 it takes at most one line of the window (its limit, or 50 lines), and only a line the stored tool response holds as a whole line, so <private> content and lines changed after the Read are never recorded. A line counts only with at least 8 letters or digits, at most 400 characters, and exactly one occurrence in the file at that moment, also when whitespace is ignored, so finding it again ignoring whitespace never lands on a copy that was already there. Failed or denied calls, whole-file Reads, Write, and files over 2 MB or holding binary data are skipped. Each observation keeps at most 3 lines, from the files it names, one per file before a second for any, in its metadata (cmem_line_anchors_v1), with no schema change. The recorded text is source code and goes wherever the observation goes, Cloud Sync included. Secrets are left out only when auto-redaction (CLAUDE_MEM_REDACT_ENABLED, off by default) is on: lines are then compared with the redacted tool input and response, so a line holding a secret it recognizes is never recorded.
How lines are found again. Before each Read, every line recorded for this file is looked up in the file as it is now: by its exact text, then by its text with whitespace squashed, so a re-indented line still counts. Found exactly once, the row shows its current number. Found nowhere, or in two or more places, the row says (code changed since): a line number is never guessed. Lines recorded for other files are ignored, and a file over 2 MB is not looked up, so its rows end with nothing.
Files modified since their history. Without line anchors, a file modified after its newest observation gets nothing added (see How It Works). With them, the timeline lists the observations that have lines recorded in the file, each marked as above, under the note The file changed after these observations: …. Observations without recorded lines stay hidden, and such a Read is never blocked.
Turning it on. In ~/.claude-mem/settings.json:
true turns it on, and observations stored while it was off have no recorded lines. An environment variable of the same name overrides the settings file, but the worker reads its environment only when it starts.
Cost. No model calls, no new dependencies. With it on, the worker reads each file an Edit, MultiEdit or targeted Read touched once per batch that stores an observation, and the hook reads the file once per Read when its timeline has a recorded line in it. A row grows by a few characters, plus one direction line when a line was found.
Whole-File vs Targeted Reads
Targeted Reads are how Claude gets exact lines for an Edit. Claude Code’s Edit tool refuses to change a file that hasn’t been Read in the session, and a Read of part of the file satisfies that rule. A typical edit:
Read(file_path): blocked. Claude gets the timeline and the directions above.smart_outline(file_path): every symbol with its line range.Read(file_path, offset=120, limit=40): allowed. Just the function to change.Edit(...): works, because step 3 was a Read of the file.
The Decision Tree
After a block, Claude’s options, cheapest first:
The smart tool costs come from the Smart Explore benchmark.
Current Date/Time for Temporal Reasoning
The message starts with the current date and time:YYYY-MM-DD time timezone), so Claude sees the same temporal markers throughout its session.
The gate already steps aside when the file was modified after its newest observation, so a blocked file hasn’t changed since that observation. Older titles can still describe earlier versions of the code, which is why the message labels them as history, not the file’s current contents.
Token Economics
Reading a large source file in full costs thousands to tens of thousands of tokens. The deny message that replaces it costs:
If Claude needs more detail, each observation costs ~300 tokens through
get_observations.
Real-World Example
ChangingWorkerService.shutdown() in this repository’s src/services/worker-service.ts (1,932 lines).
Without the gate:
Specificity Ranking
The worker returns up to 40 observations of the file, newest first. The gate keeps one per session (the newest), scores each by how specifically it concerns this file, and keeps the 15 highest. Ties go to the newer observation.
The score decides which observations make the cut. The ones kept are listed oldest first, grouped by day. An observation where this file was the main thing changed beats one where it was read alongside 20 others.
Hook Timing
ThePreToolUse hook on Read is synchronous. It has to be, because only a synchronous hook can block a tool call. Every Read waits for it.
- The hook gives the worker at most 3 seconds for its health check and the observation lookup together. If the worker is slow or not running, the hook stops waiting and the Read goes ahead with nothing added. The gate fails open.
- Claude Code’s timeout for this hook is 15 seconds, a backstop well above that budget.
- If the hook fails for any other reason, it returns a no-op. An error never blocks a Read.
Configuration
Size Thresholds
- Files under 1,500 bytes get nothing: no timeline and no block. At that size, reading the file costs about as much as the timeline would.
- Files from 1,500 bytes up to 32 KB get the timeline as context, but are never blocked.
- Code files of 32 KB and up can be blocked.
smart_outline, smart_unfold or targeted Read after it, and each turn re-reads the whole conversation. A block saves the file it keeps out of context. That saving outgrows those turns only on larger files. The read-gate eval (tests/evals/read-gate in the repository) asked the same walkthrough question with the gate on and off:
Both thresholds are fixed in
src/cli/handlers/file-context.ts.
Project Exclusions
Projects matching patterns inCLAUDE_MEM_EXCLUDED_PROJECTS skip the hook entirely: no block, no timeline. Configure this in ~/.claude-mem/settings.json:
How to Turn It Off
Any of these turns the gate off:-
Settings file. In
~/.claude-mem/settings.json: - Viewer. Open the settings (gear icon), expand Advanced, switch off Block full-file reads, and click Save.
-
Environment. Set
CLAUDE_MEM_FILE_READ_GATE_ENABLED=falsewhere the hook can see it: export it in your shell before starting Claude Code, or add it underenvin~/.claude/settings.json. It overrides the settings file.
false turns the gate off; any other value leaves it on. In settings.json, write it as the string "false", as above.
With the gate off, every Read goes through and the file’s timeline is still added as context, once per session for each file.
To turn off the whole hook, timeline included, set CLAUDE_MEM_DISABLE_FILE_CONTEXT=1 in the hook’s environment (see Turning off the tool hooks). Claude Code still starts the claude-mem hook launcher for each Read, but it exits before starting Bun or the worker bundle.
With the gate off, Claude reads whole files whenever it chooses. That costs more tokens on large files but saves the extra tool calls, which can be the better trade for small projects or when observations are sparse.
How It Fits Together
The File Read Gate is one piece of claude-mem’s layered context strategy:- Session Start: inject a timeline of recent observations (layer 1: metadata)
- File Read Gate: answer a whole-file Read of a code file with that file’s timeline (layer 1: metadata)
- get_observations: fetch specific observation details on demand (layer 2: details)
- smart_outline / smart_unfold: read the current code’s structure, or one symbol (layer 3: source)
- Read with offset and limit: exact lines, for example before an Edit (layer 3: source)

