File Read
Feature Definition
Section titled “Feature Definition”The file read tool gives the model structured access to file contents. Rather than injecting entire files into the system prompt at session start, the model requests files on demand as tool calls, receiving back the contents with line numbers attached. This enables the model to read only what it needs, pay context cost proportionally, and read files that weren’t known at session start.
The challenge is not reading files — that is trivial. The challenge is presenting file contents in a form the model can reason about efficiently: line-numbered so edits can reference specific lines, truncated at predictable boundaries so the model does not unexpectedly consume its whole context window on a large file, and extended to handle non-text formats (images, PDFs, binary data) without crashing.
Distinction from Repo Mapping
Section titled “Distinction from Repo Mapping”Repo mapping (documented at Repo Mapping) injects a compact structural summary of the entire codebase: function names, class definitions, call graph. The read tool gives verbatim line content when the model needs to see the actual code. The two are complementary: repo map for orientation, read tool for precision.
See Also
Section titled “See Also”- List, Glob, and Grep for the normal discover -> narrow -> read workflow.
- File Edit and File Write for mutation primitives that depend on accurate reads.
- Token Budgeting for why bounded read output shape matters.
Aider Implementation
Section titled “Aider Implementation”Aider does not expose a read_file tool to the model. File contents enter the conversation through two other mechanisms.
The /add chat command adds a file to the “chat files” set. Its full contents are injected into the system message at the start of every subsequent turn. The model sees the file on every call, not on demand.
The repo map injects extracted symbol tags (function names, class names, referenced identifiers) from files not explicitly added. The model sees a structural summary, not the file contents.
Neither mechanism is a tool call in the JSON tool-use sense. The model cannot request a file it did not already have in context — it must ask the user to /add it.
Consequences:
- The model cannot discover file contents autonomously without the user’s help
- Added files stay in context for the entire session, consuming tokens even when no longer relevant
- There is no line-range or offset mechanism; the whole file is always included
Codex Implementation
Section titled “Codex Implementation”Codex exposes read_file defined in codex-rs/core/src/tools/spec.rs with the implementation in codex-rs/core/src/tools/handlers/read_file.rs (commit 4ab44e2c5).
Parameter Schema
Section titled “Parameter Schema”struct ReadFileArgs { /// Absolute path to the file. file_path: String,
/// 1-indexed line number to start reading from. Defaults to 1. offset: usize,
/// Maximum number of lines to read. Defaults to 2000. limit: usize,
/// Reading mode: "slice" (simple range) or "indentation" (smart block). mode: ReadMode,
/// Only used when mode = "indentation". indentation: Option<IndentationArgs>,}
enum ReadMode { Slice, // returns lines [offset, offset+limit) Indentation, // smart block expansion around an anchor line}
struct IndentationArgs { /// Center point for block expansion. Defaults to offset. anchor_line: Option<usize>,
/// How many parent indentation levels to include (0 = unlimited). max_levels: usize,
/// Include sibling blocks at the same indentation depth. include_siblings: bool,
/// Include comments/docstrings immediately above the anchor. include_header: bool,
/// Hard line cap regardless of indentation expansion. max_lines: Option<usize>,}Slice Mode
Section titled “Slice Mode”The simplest mode. Reads lines [offset, offset + limit) from the file.
Output (read_file.rs:156–221):
L1: fn main() {L2: let x = 42;L3: println!("{}", x);L4: }Every line is prefixed with L<number>: . Line numbers are 1-indexed and match the actual file line numbers, not the output position. If offset exceeds the file’s total line count, the tool returns an error rather than an empty result.
Constants:
MAX_LINE_LENGTH = 500characters; lines beyond this are truncated with…- No total byte cap at this layer; the caller’s context budget handles it
Indentation Mode
Section titled “Indentation Mode”Codex’s distinctive feature. Instead of a fixed line range, the model specifies an anchor line and the tool expands outward to include the full indentation block that contains it (read_file.rs:223–430).
Algorithm:
- Read the entire file into memory once
- Compute the effective indentation depth of each line (treating blank lines as transparent)
- Find the anchor line (defaults to
offsetifanchor_lineis not given) - Determine the anchor’s effective depth
- Walk upward from the anchor, collecting lines at depth ≥ anchor depth
- Walk downward from the anchor, collecting lines at depth ≥ anchor depth
- If
max_levels > 0: also include up tomax_levelsparent levels (lines at shallower depth above the anchor) - If
include_siblings = false: stop at the first gap in indentation, excluding blocks that share the same depth but start after a blank line - If
include_header = true: include comment lines immediately above the anchor (detects//,#,--prefixes viaCOMMENT_PREFIXES) - Apply
max_lineshard cap if given - Trim leading and trailing blank lines from the result
Tab expansion: Tabs are counted as TAB_WIDTH = 4 spaces for indentation purposes.
Example use case: The model knows a function is at line 200 but doesn’t know where it ends. With mode: indentation, anchor_line: 200, max_levels: 1, include_header: true, it gets the function’s docstring, signature, and complete body without knowing the end line number.
Output Format
Section titled “Output Format”Both modes return lines in the format:
L<number>: <content>Joined with newlines. No wrapping XML or structured envelope — just the formatted text, injected directly as a tool result.
OpenCode Implementation
Section titled “OpenCode Implementation”OpenCode exposes a read tool in packages/opencode/src/tool/read.ts (commit 7ed44997).
Parameter Schema (Zod)
Section titled “Parameter Schema (Zod)”parameters: z.object({ filePath: z.string() .describe("The absolute path to the file or directory to read"),
offset: z.coerce.number() .describe("The line number to start reading from (1-indexed)") .optional(),
limit: z.coerce.number() .describe("The maximum number of lines to read (defaults to 2000)") .optional(),})Note the z.coerce.number() — the model often emits offset/limit as strings, and Zod’s coerce converts them silently rather than failing schema validation.
File Type Detection
Section titled “File Type Detection”read.ts:110–139 dispatches based on content type before attempting to read as text:
Images (except SVG):
if (mime.startsWith("image/") && mime !== "image/svg+xml") { const data = await fs.readFile(filepath); return { type: "image", mimeType: mime, data: data.toString("base64"), };}Returned as a base64 attachment alongside the text content block, enabling multimodal models to see the actual image.
PDFs:
Same pattern — base64-encoded, with application/pdf mime type.
Binary file detection (read.ts:148–158):
// A file is binary if:// 1. It contains a null byte (\x00), OR// 2. More than 30% of its first 512 bytes are non-printable (< 0x20 and not whitespace)function isBinary(buf: Buffer): boolean { if (buf.includes(0x00)) return true; const nonPrintable = buf.slice(0, 512).filter(b => b < 0x20 && b !== 9 && b !== 10 && b !== 13).length; return nonPrintable / Math.min(buf.length, 512) > 0.3;}Binary files return an error message telling the model the file is binary and suggesting alternatives.
SVG and other text types: Fall through to the line-based reader.
Directory Listing
Section titled “Directory Listing”If the path points to a directory, read.ts:68–107 enumerates entries:
const entries = await fs.readdir(filepath, { withFileTypes: true });const formatted = entries .sort((a, b) => a.name.localeCompare(b.name)) .map(e => e.isDirectory() ? e.name + "/" : e.name);
// Apply offset/limit (1-indexed)const page = formatted.slice((offset - 1) || 0, (offset - 1 || 0) + (limit || formatted.length));Output:
<path>/home/user/project/src</path><type>directory</type><entries>index.tslib/README.mdtests/</entries>Line-Based File Reading
Section titled “Line-Based File Reading”read.ts:140–180 for text files:
const DEFAULT_READ_LIMIT = 2000; // linesconst MAX_LINE_LENGTH = 2000; // chars per lineconst MAX_BYTES = 50 * 1024; // 50 KB total byte cap
const lines = content.split("\n");const pageLines = lines.slice((offset - 1) || 0, ((offset - 1) || 0) + (limit || DEFAULT_READ_LIMIT));
let byteCount = 0;const result = pageLines.map((line, i) => { let l = line; if (l.length > MAX_LINE_LENGTH) { l = l.slice(0, MAX_LINE_LENGTH) + "…"; } byteCount += Buffer.byteLength(l, "utf8"); if (byteCount > MAX_BYTES) return null; // stop adding lines return `L${(offset - 1 || 0) + i + 1}: ${l}`;}).filter(Boolean).join("\n");Output Format
Section titled “Output Format”<path>/absolute/path/to/file.ts</path><type>file</type><content>L1: import { foo } from "./foo";L2:L3: export function bar() {L4: return foo();L5: }</content><summary> Total lines: 42 | Showing: lines 1–5 | Truncated: false</summary>XML tags provide a structured envelope. The summary line tells the model how many total lines the file has and whether it received the full view or a truncated slice.
InstructionPrompt Hints
Section titled “InstructionPrompt Hints”After reading, read.ts may append any InstructionPrompt hints associated with the file path (from the instruction resolution system). These are additional instructions embedded in AGENTS.md or similar files relevant to the file’s directory. Injected as a separate block after the file content.
Permission Gating
Section titled “Permission Gating”// assert the path is inside Instance.directory or a configured allowlistawait assertExternalDirectory(ctx, filepath, { allowRead: true });
// request read permission (almost always auto-approved; "always: ['*']" pattern)await ctx.ask({ permission: "read", patterns: [filepath], always: ["*"], // auto-approve all reads once permission granted once metadata: {},});In practice, reads are almost never blocked — the always: ["*"] wildcard means the first approval auto-approves all future reads. But the hook exists so the plugin system can intercept reads of specific sensitive files.
Pitfalls & Hard Lessons
Section titled “Pitfalls & Hard Lessons”Line Number Consistency Is Critical
Section titled “Line Number Consistency Is Critical”If the tool outputs L42: function foo(), the model may later emit an edit targeting line 42. If the tool’s line counting differs from the file’s actual line count (e.g., due to Windows \r\n vs Unix \n), the edit will target the wrong line. Both Codex and OpenCode split on \n only — never on \r\n — and strip trailing \r per line if present. OpenOxide must match this behavior.
The 2000-Line Default Is Often Wrong
Section titled “The 2000-Line Default Is Often Wrong”A 2000-line limit handles most source files, but generated files, lockfiles (package-lock.json, Cargo.lock), and minified JS can be 10,000+ lines. When a model reads one of these files and hits the truncation, it sees a partial picture and may make wrong assumptions. The tool should include a clear summary indicating truncation so the model knows to use grep or glob instead.
Binary Detection False Positives
Section titled “Binary Detection False Positives”The 30% non-printable heuristic flags UTF-16 encoded files as binary because they contain \x00 bytes. A UTF-16 encoded README would appear binary even though it is perfectly valid text. Better detection reads the BOM (\xff\xfe for UTF-16 LE, \xfe\xff for UTF-16 BE) and re-decodes accordingly rather than treating non-ASCII as binary.
Directory Listing at Scale
Section titled “Directory Listing at Scale”Listing a directory with tens of thousands of entries (e.g., node_modules/) returns all entries before applying offset/limit. The readdir() call blocks until all entries are collected, which can take seconds for very large directories. Apply a pre-emptive cap (MAX_ENTRIES = 5000) and return a truncation message rather than listing all entries.
Image Handling Context Cost
Section titled “Image Handling Context Cost”Returning a base64-encoded image as an attachment costs tokens at the vision encoding rate, not the text token rate. A 1 MB PNG might cost more tokens than reading 10,000 lines of code. The tool should warn the model if an image exceeds a size threshold (e.g., 200 KB) and offer to describe it via the model’s vision capability instead of returning the raw bytes.
Offset Is 1-Indexed, Not 0-Indexed
Section titled “Offset Is 1-Indexed, Not 0-Indexed”Both Codex and OpenCode use 1-indexed offsets (line 1 is the first line). This is consistent with how editors display line numbers. If OpenOxide uses 0-indexed offsets, the model will systematically request one line off from what it intends.
Codex’s MAX_LINE_LENGTH Is Too Short
Section titled “Codex’s MAX_LINE_LENGTH Is Too Short”Codex caps lines at 500 characters. Minified CSS, long SQL strings, and some generated code have lines well over 500 characters. A truncated line at column 500 may not be valid syntax, and the model may try to edit it without realizing it saw an incomplete line. OpenCode’s 2000-character limit is more practical.
OpenOxide Blueprint
Section titled “OpenOxide Blueprint”Architecture: Unified Read Handler
Section titled “Architecture: Unified Read Handler”#[derive(Deserialize, JsonSchema)]pub struct ReadFileParams { /// Absolute path to the file or directory. pub file_path: PathBuf,
/// 1-indexed starting line. Defaults to 1. pub offset: Option<usize>,
/// Maximum lines to return. Defaults to 2000. pub limit: Option<usize>,
/// Reading mode. Defaults to "slice". pub mode: Option<ReadMode>,
/// Options for indentation mode. pub indentation: Option<IndentationOptions>,}
#[derive(Deserialize, JsonSchema)]#[serde(rename_all = "snake_case")]pub enum ReadMode { Slice, Indentation,}Constants
Section titled “Constants”pub const DEFAULT_READ_LIMIT: usize = 2000;pub const MAX_LINE_LENGTH: usize = 2000; // match OpenCode, not Codex's 500pub const MAX_BYTES: usize = 50 * 1024;pub const MAX_DIR_ENTRIES: usize = 5000;Dispatch
Section titled “Dispatch”pub async fn execute(params: ReadFileParams, ctx: &ToolContext) -> ToolResult { let path = ctx.resolve_path(¶ms.file_path)?;
if path.is_dir() { return read_directory(&path, params.offset, params.limit).await; }
let mime = mime_guess::from_path(&path).first_or_octet_stream(); if is_image_mime(&mime) { return read_as_image(&path, &mime).await; } if mime.essence_str() == "application/pdf" { return read_as_pdf(&path).await; }
let bytes = tokio::fs::read(&path).await?; if is_binary(&bytes) { return Err(ToolError::BinaryFile(path)); }
match params.mode.unwrap_or(ReadMode::Slice) { ReadMode::Slice => read_slice(&bytes, params.offset, params.limit), ReadMode::Indentation => read_indentation(&bytes, params.indentation), }}Indentation Mode
Section titled “Indentation Mode”Implement Codex’s algorithm verbatim. It is genuinely useful and no other implementation has it. Key differences from Codex to fix:
- Use
MAX_LINE_LENGTH = 2000not 500 - Return a summary line showing how many lines were in the block vs the file total
- Expose
max_linesas a required field (not optional) with a clear default
Output Format
Section titled “Output Format”Match OpenCode’s XML envelope:
<path>{absolute_path}</path><type>file</type><content>L{n}: {line}...</content><summary>Total: {total} lines | Showing: {start}–{end} | Truncated: {bool}</summary>For directories:
<path>{absolute_path}</path><type>directory</type><entries>{name}{name}/...</entries><summary>Total: {n} entries | Showing: {start}–{end}</summary>Crates
Section titled “Crates”[dependencies]tokio = { features = ["fs"] }mime_guess = "2"base64 = "0.22"serde = { features = ["derive"] }schemars = "0.8"