Core Concepts

Read Modes

10 read modes for every situation plus a lossless anchored editing mode: auto, full, map, signatures, diff, aggressive, entropy, task, reference, lines:N-M, anchored. Decision tree to pick the right one.

ctx_read supports 10 distinct read modes, each optimized for a different situation, plus a lossless anchored mode that powers reliable hash-anchored editing. The right mode saves 60–90% of tokens compared to reading raw files; cached re-reads drop to ~13 tokens.

Decision Tree: Which Mode?

Start here to pick the optimal mode for your situation:

Q1:

Will you edit this file?

Yes → Use anchored — edit by reference with ctx_patch (no exact-recall); use full then diff for plain re-reads
No → Continue to Q2

Q2:

Do you need the file's role/API surface, or line-level detail?

Role/API → Use map (85-95% savings)
Signatures → Use signatures (87% savings)
Line detail → Continue to Q3

Q3:

Is the file large (>500 lines) or contains boilerplate?

Large + boilerplate → Use entropy (60-85% savings)
Large + comments → Use aggressive (20-40% savings)
Only specific lines → Use lines:N-M
Not sure → Use auto (LeanCTX picks for you)

Q4:

Is there an active task set?

Yes → Use task (filters to task-relevant lines + graph context)
Just a pointer → Use reference (~1 line, 0 content)

Mode Comparison Table

ModeOutputToken SavingsBest For
autoVaries - selects best modeBest-effortWhen unsure which mode to use
fullComplete file + structured headers~99% on re-readsFiles you'll edit
anchoredFull file, each line tagged N:hh|…Lossless (adds anchors)Editing via ctx_patch without exact-recall
mapDependencies, exports, API surface85-95%Understanding a file's role
signaturesAll function/class/type signatures~87%Understanding API surfaces
diffOnly changed lines since last read80-97%Checking recent changes
aggressiveFull file minus comments + whitespace20-40%Verbose files with many comments
entropyShannon-filtered, deduplicated60-85%Files with repetitive patterns
taskTask-relevant lines + graph contextHighly variableTask-driven development
reference1-line pointer (no content)~99.9%Keeping track of files
lines:N-MSpecific line rangesProportionalSurgical edits in large files

Auto Mode (Default)

auto is the default mode — when you omit the mode parameter, LeanCTX selects the optimal mode based on file size, language, whether the file is cached, and the current task context. This is the recommended starting point if you're unsure.

ctx_read src/server.rs --mode auto
→ [auto] resolved to map
  F2=server.rs [262L]
  deps: rmcp::model::, rmcp::handler::...
  exports: -
  API: fn tool_def(...), fn execute_command(...)
  [2135 tok saved (93%)]

Suspect files always get full (#361): when the active task text names a file (e.g. "fix the sort in versioncmp.c"), auto forces full for that file ahead of any compression-favouring intent, so the agent always receives the body it needs to localise and edit the defect.

Structure-first cold reads (structure_first, #361): an opt-in bias (off by default; enable with the LEAN_CTX_STRUCTURE_FIRST env var) that lets auto prefer map on a cold read of a medium-sized source file. It stays capability-safe — the active-diagnostic, edit-failure, and small-file guards still force full.

ctx_multi_read honours auto per file (#421): batch reads no longer force every file to full — each file resolves to its own optimal mode, exactly like a single ctx_read. Omit mode (= auto) and reserve full for the read immediately before an edit.

Full Mode

Returns the complete file with structured headers (dependencies, exports, file references). The content is cached with a BLAKE3 hash. Subsequent reads of the same unchanged file return a compact cache-hit message (~13 tokens) instead of re-transmitting the full content.

# First read: full content
ctx_read auth.ts
→ F1=auth.ts 123L deps:db,crypto exports:createUser,validateSession
  [complete file content...]

# Second read: cache hit
ctx_read auth.ts
→ F1=auth.ts cached 2t 123L

Anchored Mode — edit by reference (v3.8.16)

anchored is a lossless read mode — a strict superset of full — built for editing. Every line is prefixed with a tiny anchor N:hh|: the 1-based line number N and hh, the first four hex chars of a BLAKE3 hash of that line's content. You then edit by reference with ctx_patch, naming a line by its (number, hash) instead of reproducing its exact text. This removes the str_replace exact-recall tax — the model no longer has to echo a verbatim old string (and silently fail when a single trailing space differs).

ctx_read src/auth/service.ts --mode anchored
→ F1=src/auth/service.ts 4L [anchored: N:hh|line → edit via ctx_patch]
  1:9f2a|import { db } from './db';
  2:a31c|export function createUser(email: string) {
  3:7b6e|  return db.insert({ email });
  4:1c40|}

Then edit those lines by reference — ctx_patch understands these ops:

  • set_line — replace one line (an empty new_text deletes it).
  • replace_lines — replace an inclusive range (start_line/start_hash through end_line/end_hash).
  • insert_after — insert after a line (line=0 inserts at the top and needs no hash).
  • delete — drop one line (by line + hash) or a range.
  • replace_symbol — swap a whole function/method body by name (name + new_body), reusing ctx_refactor's symbol resolution and CONFLICT guard.
  • create — write a strictly new file (new_text is the full content; existing paths are refused, and it can't be mixed with anchored ops in one batch). Since v3.8.19.
# Replace line 4 by reference — the old text is never resent
ctx_patch path="src/auth/service.ts" op="set_line" line=4 hash="1c40"
  new_text="  return db.insert({ email, createdAt: Date.now() });"

# Batch edits in one atomic call (anchors reference the read snapshot)
ctx_patch path="src/auth/service.ts" ops=[
  {"op":"insert_after","line":1,"hash":"9f2a","new_text":"import { now } from './clock';"},
  {"op":"delete","line":3,"hash":"7b6e"}
]

Drift-safe by construction: the edit side re-derives each hash from the file currently on disk. If a line changed since you read it, its anchor no longer matches and the op is rejected rather than silently editing the wrong line — re-read with mode=anchored to refresh the anchors.

ctx_patch path="src/auth/service.ts" op="set_line" line=4 hash="1c40" new_text="…"
→ anchor miss at L4 (expected 1c40, found 8be2) — file drifted, re-read mode=anchored

Anchors are whitespace-tolerant (the hash ignores trailing whitespace, so re-indentation never spuriously invalidates them) and deterministic — an anchored read is a pure function of the file content, so identical re-reads stay byte-stable and prompt-cache prefixes are never disturbed.

Map Mode

Returns the dependency graph, export list, and API signatures without implementation details. Uses tree-sitter AST parsing across 26 languages for accurate extraction (Lua and Luau are now first-class graph-indexed languages too, #360). Best when you need to understand a file's role without reading its implementation.

ctx_read src/auth/service.ts --mode map
→ F1=src/auth/service.ts [map] 214L (91% saved)
  DEPS: express, jsonwebtoken, bcrypt, ./db/users
  EXPORTS: authenticateUser, generateToken, refreshSession
  TYPES: AuthConfig, TokenPayload, SessionData
  API: authenticateUser(req,res)->Promise<User>
       generateToken(user:User)->string
       refreshSession(token:s)->Result<Session>

Non-code files (v3.6.18+): map mode also produces structured summaries for Markdown (heading outline), JSON (key structure with types), YAML (key hierarchy), TOML (section headers + key=value pairs), and lock files (workspace dependency summaries). Savings: 85-95% on large config and documentation files.

Export de-duplication (#361): map no longer repeats exports that the API: section already lists with full signatures — every export is shown exactly once (re-exports and const aliases the API can't capture still surface).

Signatures Mode

Extracts all function signatures, interface definitions, type aliases, and class declarations. More detailed than map - includes private functions, nested types, and multi-line signatures with compact type abbreviations (:s for string, :n for number).

ctx_read src/server/router.rs --mode signatures
→ F3=src/server/router.rs [signatures] 489L (87% saved)
  pub fn register_routes(app: &mut App) -> Result<()>
  pub fn handle_request(req: Request) -> Response
  fn validate_params(params: &Params) -> Result<()>
  pub struct RouterConfig { base_path: String, timeout: Duration }
  pub trait RouteHandler { fn execute(&self, ctx: &Context) -> Result<Response> }

Diff Mode

Shows only the lines that changed since the file was last cached. Requires a prior read of the same file. Only sends the delta, which can save 80-97% compared to re-reading the full file.

# After editing auth.ts (previously read as F1)
ctx_read src/auth/service.ts --mode diff
→ F1=src/auth/service.ts [diff] Δ3 lines (94% saved)
  ~L42: -  const token = jwt.sign(payload, SECRET)
        +  const token = jwt.sign(payload, SECRET, { expiresIn: '15m' })
  +L67:    validateRefreshToken(refreshToken: string): boolean

Aggressive Mode

Returns the full file with all comments, JSDoc blocks, and excessive whitespace stripped. Preserves all functional code. Best for verbose files where documentation comments dominate the token count.

ctx_read src/utils/helpers.ts --mode aggressive
→ F5=src/utils/helpers.ts [aggressive] 186L (32% saved)
  import { format } from 'date-fns';
  export function formatDate(d: Date): string {
    return format(d, 'yyyy-MM-dd');
  }
  export function slugify(text: string): string {
    return text.toLowerCase().replace(/\s+/g, '-');
  }

Entropy Mode

Uses Shannon entropy scoring and Jaccard similarity to filter out repetitive, low-information lines. Ideal for generated files, large configs, or schema files where many declarations follow the same pattern.

ctx_read src/generated/schema.ts --mode entropy
→ F4=src/generated/schema.ts [entropy] 1847L→312L (83% saved)
  Shannon filter: removed 1535 repetitive lines
  Kept: unique type definitions, variant patterns
  export interface UserSchema { id: string; email: string; ... }
  export interface OrderSchema { id: string; items: Item[]; ... }
  // 47 similar interfaces deduplicated

Task Mode (Graph-Driven)

The most intelligent mode. It filters the file down to task-relevant lines using an information bottleneck filter, then appends a Graph Context footer with related files (imports, imported-by, transitive dependencies, type providers).

Prerequisites:

  1. Set a task via ctx_session action=task value="your task description"
  2. Optionally build the project graph with ctx_graph action=build for richer context
  3. Read with ctx_read path="..." mode=task
ctx_session action=task value="fix auth refresh token bug"
ctx_read src/auth/service.rs --mode task
→ F1=src/auth/service.rs 412L [task-filtered: 412→38]
  ...task-relevant lines only...
--- GRAPH CONTEXT (4 related files, 9180 tok) ---
  src/auth/token.rs [imports]
  src/http/middleware.rs [imported-by]
--- END GRAPH CONTEXT ---

Reference Mode

Ultra-compact: returns a single line with file metadata (line count, token estimate, type). No content is transferred. Use to register a file as "known" without spending tokens on it.

ctx_read src/auth/service.ts --mode reference
→ F1=src/auth/service.ts [ref] 214L ~847tok ts

Lines Mode

Read only specific lines. Supports single lines, ranges, and comma-separated combinations:

  • lines:42 - single line
  • lines:10-80 - line range
  • lines:10-80,120,200-240 - multiple ranges
ctx_read src/server.rs --mode lines:42-48
→ F2=src/server.rs [lines:42-48] 7L
  42| pub fn start_server(config: &Config) -> Result<()> {
  43|     let addr = config.bind_address();
  44|     let listener = TcpListener::bind(addr)?;
  45|     info!("Server listening on {}", addr);
  46|     for stream in listener.incoming() {
  47|         handle_connection(stream?)?;
  48|     }

Aggressiveness Knob (v3.8.10)

One number, 0.01.0, sets compression intensity across the whole read path: 0.0 is lossless, 1.0 is the most aggressive summarization, and the value maps consistently onto the read modes, the entropy filter and the information-bottleneck stage. On an auto read it routes through the proven density path, so a single number drives whole-file intensity. Leave it unset to keep each mode's calibrated default.

Set it at three scopes — the most specific wins:

  • Per read — the aggressiveness argument on ctx_read.
  • Per shell — the LEAN_CTX_AGGRESSIVENESS environment variable.
  • Globally — compression_aggressiveness in config.toml.
ctx_read("src/server.rs", aggressiveness=0.8)   # denser one-off read
export LEAN_CTX_AGGRESSIVENESS=0.3              # gentle, session-wide
lean-ctx config set compression_aggressiveness 0.5

Output stays deterministic: the same content + mode + aggressiveness always renders byte-for-byte identically (and an unset value reproduces the pre-knob output exactly), so prompt-cache prefixes are never disturbed.

Protect Spans (v3.8.10)

Aggressive and entropy modes are line-based lossy filters. Protect spans keep chosen lines verbatim no matter how hard the rest of the file is compressed, so a critical invariant or a load-bearing config line is never the one that gets dropped. Two ways to pin content:

  • protect tokens — pass ctx_read("…", protect=["TODO", "SAFETY"]) and every line containing one of those substrings survives the lossy filters intact.
  • <lc_safe>…</lc_safe> markers — wrap a span directly in the source; the content between the markers passes through byte-for-byte and the markers themselves are stripped from the output. These are universal — the same markers are honoured on the ctx_read, ctx_shell and proxy compression paths.

Like the aggressiveness knob, protect spans fold into the read cache key, so protected and unprotected reads of the same file cache independently and stay deterministic.

Anti-Inflation Invariant (v3.8.6)

A read never returns more tokens than the raw file. On a tiny file the one-line framing header (file reference + deps/exports summary) can be net overhead that only pays off across re-reads, so when framing would exceed the bare content, auto-resolved and full reads ship the file verbatim — a read is break-even at worst and a win whenever a compressed mode or a cached re-read applies. Explicitly requested views (map, signatures, lines:) are always honoured untouched, and the same guarantee now covers the one-shot CLI read path.

Instruction File Protection (v3.5.13)

Files recognized as agent instructions are always delivered in full mode, regardless of the requested mode. This prevents critical agent instructions from being compressed or stripped.

Protected files and directories:

  • SKILL.md, AGENTS.md, RULES.md, .cursorrules
  • Files in /skills/, /.cursor/rules/, /.claude/rules/ directories

The guard is applied in 5 code paths: resolve_auto_mode, predict_from_defaults, select_mode_with_task, auto_degrade_read_mode, and CLI read_cmd.

Markdown File Exemption

Markdown files (.md, .mdx, .txt, .rst) are exempt from the aggressive default bucket, they return full mode by default to preserve prose content.


Structured Headers

Every ctx_read response includes a structured header with persistent file references:

F1=auth.ts 123L
 deps db,crypto
 exports createUser,validateSession
  • F1, F2, ... - file reference IDs (persist across the entire session)
  • deps - imported modules (compact list)
  • exports - exported names

Reference these IDs in subsequent operations instead of full paths to save tokens (e.g., "edit F1:42" instead of "edit src/auth/service.ts line 42").