Skip to main content

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_outline can parse (see the table below).
  • The file is inside the project, meaning under the session’s working directory once symlinks are resolved. smart_outline and smart_unfold apply 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_outline and smart_unfold is 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; run npx claude-mem repair once the network allows it.
Code files the gate covers: 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

When a Read isn’t blocked but the file has current history, the timeline is added to the Read as context: once per session for each file, and again only after a newer observation of the file lands. Reads that stop earlier in the chart get nothing added. Every whole-file Read of a gated file is blocked, not only the first one.

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 a PreToolUse:Read hook error; that is the gate at work, not a failure.
Each timeline line is <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. With CLAUDE_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:
It applies from the next Read and the next stored observation, with no restart. Only the value 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:
  1. Read(file_path): blocked. Claude gets the timeline and the directions above.
  2. smart_outline(file_path): every symbol with its line range.
  3. Read(file_path, offset=120, limit=40): allowed. Just the function to change.
  4. Edit(...): works, because step 3 was a Read of the file.
A targeted Read right after a block doesn’t repeat the timeline, since the block already showed it.

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:
This lets Claude tell how old each observation is. The format matches the SessionStart context header (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

Changing WorkerService.shutdown() in this repository’s src/services/worker-service.ts (1,932 lines). Without the gate:
With the gate:
Token counts on this page are estimates at 4 characters per token.

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

The PreToolUse 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.
A block isn’t free. The denied Read is a turn, and so is every 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 in CLAUDE_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=false where the hook can see it: export it in your shell before starting Claude Code, or add it under env in ~/.claude/settings.json. It overrides the settings file.
A change made in the settings file or the viewer applies from the next Read, with no restart. Only the value 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:
  1. Session Start: inject a timeline of recent observations (layer 1: metadata)
  2. File Read Gate: answer a whole-file Read of a code file with that file’s timeline (layer 1: metadata)
  3. get_observations: fetch specific observation details on demand (layer 2: details)
  4. smart_outline / smart_unfold: read the current code’s structure, or one symbol (layer 3: source)
  5. Read with offset and limit: exact lines, for example before an Edit (layer 3: source)
Each layer costs more than the one before. The gate makes Claude start at the cheapest layer and escalate only when needed. A whole-file Read still happens wherever the gate steps aside: small and non-code files, files changed since their last observation, subagents, other hosts, installs without the tree-sitter CLI, or with the gate turned off.