◉CLAUDE LABJP
●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Articles/Claude Code
⟐ Claude Code/2026-05-10Intermediate

When Claude Code's Read Doesn't Reach the End of a File — Catching the PARTIAL view Notice and Sizing offset/limit

When Read returns only part of a large file, Claude Code says so with a PARTIAL view notice. Here is how to catch it, and how to size offset and limit by byte budget rather than line count, with measurements from my own sandbox.

claude-code132troubleshooting92read-toolfilesbash5

One evening I handed Claude Code a 120,000-line access log and asked it to pull every line containing a particular checkout keyword. The count that came back was an order of magnitude off from what grep -c reported on the same file.

For a while I assumed the tool had quietly cut the file short. Then I scrolled back through the transcript and found a PARTIAL view notice sitting at the top of the Read response — telling me how much it had returned and how to ask for the rest. Nothing had been silent. I had skipped the line.

Separate "the tool said nothing" from "I didn't look." Until you can tell those apart, you keep assigning the mismatch to the wrong cause and the afternoon goes by. Working solo on my own apps, with nobody to catch the discrepancy for me, that habit has to be mine.

What follows is what I've since worked out about how Read behaves on large files, what unit to use when sizing offset and limit, and where the job belongs in bash instead.

It Doesn't Stop at "2000 Lines"

The old explanation was that Read returns the first 2000 lines, and I ran on that assumption for a long time. Reading the current tools reference again, the cutoff is a token limit, not a fixed line count.

How you call itWhat happens past the token limit
Whole-file read, no offset or limitReturns the first page with a PARTIAL view notice saying how much you got and how to read more
Read with an explicit offset or limitReturns an error rather than a partial result
Explicit limit whose range could never fitStops without loading the rest and tells you to use a smaller limit or reach for Grep
offset past the last lineReturns a notice with the file's line count
Empty fileReturns a notice that the file exists but has no contents

So Read is telling you where it stopped in almost every case. The hard part is that the line gets buried at the top of a long response, and a summary built from partial data still reads plausibly. If a tool claims to have found something that isn't there, you get suspicious. If it says "not found," you tend to believe it.

There's also some history worth knowing. Before v2.1.208, Claude Code loaded the whole requested range into memory before rejecting it, so a file with one extremely long line could exhaust memory. It now stops before loading. Avoiding minified files was never only about saving tokens.

Three Signals

First, is there a PARTIAL view notice in the response? That check is definitive, and none of the heuristics below substitute for it.

Second, did Read return an error? An error on a call with an explicit offset or limit means the range you asked for was too big. It did not quietly hand you half of it.

Third, is the answer "not found" for something you know is in the file? When a query about December comes back empty on a file that obviously covers the year, check wc -l before anything else.

# First check whenever the numbers don't line up
wc -l /path/to/large.csv
ls -lh /path/to/large.csv

Noticing the signals isn't hard. Sustaining that attention on a tired Thursday is. So I moved the noticing into a script.

Budget in Bytes, Not Lines

Before handing over any sizeable file, I run a small script that prints a plan. It used to key off "more than 2000 lines, so split it" — and that unit was simply wrong. Bytes per line vary enormously by file type.

Here's what four files of different character measured in my sandbox.

FileTotalLinesBytes/lineFirst 2000 lines
Access log12,851,091 B120,000108 B210,657 B
Sales CSV1,890,031 B60,00132 B62,699 B
Pretty-printed JSON2,148,909 B160,00314 B26,390 B
Minified JSON968,901 B1968,901 B968,901 B

From 14 B to 108 B per line — roughly an eightfold spread. The same "2000 lines" is 210 KB in the log and 26 KB in the JSON. Against a 40,000 B budget per read, that's 5.27x over in one case and only 66% of the budget used in the other. Both are misses.

So I rekeyed the judgment on bytes.

#!/usr/bin/env bash
# read-plan.sh — decide how to read a file before handing it to Claude Code
# Keyed on a byte budget, because bytes-per-line varies severalfold by file type
# usage: ./read-plan.sh path/to/file [budget_bytes]
set -euo pipefail
 
f="${1:?usage: read-plan.sh <file> [budget_bytes]}"
budget="${2:-40000}"          # roughly how many bytes one Read may cost
long_line_limit=500           # a single line above this makes Read a bad fit
 
[ -f "$f" ] || { echo "NOT_FOUND $f"; exit 1; }
 
bytes=$(wc -c < "$f" | tr -d ' ')
lines=$(wc -l < "$f" | tr -d ' ')
[ "$lines" -eq 0 ] && lines=1                 # no newline at all: treat as one line
maxlen=$(awk '{ if (length($0) > m) m = length($0) } END { print m+0 }' "$f")
bpl=$(awk -v b="$bytes" -v l="$lines" 'BEGIN { printf "%d", (b/l)+1 }')
 
# Multibyte share: heavier text costs more tokens for the same byte count
ascii=$(LC_ALL=C tr -cd '\000-\177' < "$f" | wc -c | tr -d ' ')
mb_pct=$(awk -v a="$ascii" -v b="$bytes" 'BEGIN { printf "%d", (b-a)*100/b }')
 
# Binary check via NUL bytes; the first 4 KB is enough
raw=$(head -c 4096 "$f" | wc -c | tr -d ' ')
stripped=$(head -c 4096 "$f" | LC_ALL=C tr -d '\000' | wc -c | tr -d ' ')
kind=text; [ "$raw" != "$stripped" ] && kind=binary
 
echo "file=$f bytes=$bytes lines=$lines bytes_per_line=$bpl max_line=$maxlen multibyte=${mb_pct}% kind=$kind"
 
if [ "$kind" = binary ]; then
  echo "PLAN=BASH_ONLY   reason=binary (never Read this)"
elif [ "$maxlen" -gt "$long_line_limit" ]; then
  echo "PLAN=BASH_ONLY   reason=longest line is ${maxlen} B (even limit=1 can overshoot; use jq or sed)"
elif [ "$bytes" -le "$budget" ]; then
  echo "PLAN=READ_DIRECT reason=${bytes} B fits the ${budget} B budget"
else
  chunk=$(( budget / bpl )); [ "$chunk" -lt 1 ] && chunk=1
  blocks=$(( (lines + chunk - 1) / chunk ))
  echo "PLAN=READ_RANGED reason=${bytes} B > budget ${budget} B"
  echo "  limit=${chunk} lines x ${blocks} blocks (derived from ~${bpl} B per line)"
  echo "  when that is a lot, narrow with grep or awk first"
fi

Running the same four files through it, one 40,000 B budget produces four different answers.

FilePlanSuggested limitBlocks
Access logREAD_RANGED370 lines325
Sales CSVREAD_RANGED1,250 lines49
Pretty-printed JSONREAD_RANGED2,857 lines57
Minified JSONBASH_ONLY——

370 lines and 2,857 lines. A fixed 2000 fits neither. Derive the chunk from the file's own bytes-per-line, not from a number you memorized. Three outcomes, and that's the whole point: READ_DIRECT gets handed over as-is, READ_RANGED gets split at the suggested limit, BASH_ONLY gets summarized in bash first.

Two practical caveats. wc -l counts newline characters, so a file without a trailing newline reports one line short. And text with a high multibyte share costs more tokens for the same byte count, so lower the budget when multibyte runs high. I'd rather round toward caution here than re-derive the judgment under pressure.

Working with offset and limit

When You Know Roughly Where to Look

For a known region — the minutes around an error, a particular section — pass offset and limit directly.

Read(file_path="/path/to/server.log", offset=4500, limit=200)

For logs, grep -n "ERROR" server.log | head surfaces the line numbers first, so offset can point straight at one. Getting greedy with limit here produces an error rather than a partial read, so I treat the value from read-plan.sh as my ceiling.

When You Can't See the Target

For "every line containing X" or "how many rows match Y", filter in bash before calling Read.

# Narrow first, then read what is left
grep -n "checkout" /path/to/access.log > /tmp/checkout-lines.txt
wc -c /tmp/checkout-lines.txt

Note that the check is bytes, not lines. If the filtered output fits the budget, read it. If not, narrow again with head.

When You Want a Whole-File Scan

That's bash territory. Read exists to give Claude context, not to be a data-processing engine. Aggregations and full scans belong in bash, awk, or Python, and only the summary should cross over.

The same instinct shows up in Why Claude Code's Glob and Grep Return Zero Results. Asking whether a search tool found nothing or simply couldn't see everything is the habit that protects you here too.

A Sentinel Line Proves Your Split Had No Gaps

PARTIAL view tells you where a single read stopped. It doesn't tell you whether your own walk across 300 blocks skipped one. Boundary gaps are yours to verify.

The trick I use is to append a marker line to a copy of the file.

# Append a sentinel to a temporary copy
cp /path/to/large.csv /tmp/with-sentinel.csv
printf 'EOF_MARKER_%s\n' "$(date +%s)" >> /tmp/with-sentinel.csv
 
# Keep the expected value handy
tail -1 /tmp/with-sentinel.csv

Then ask: "when you finish reading, echo back the line starting with EOF_MARKER_." If it comes back, you reached the bottom. If not, either your offset walk has a gap or one of the blocks errored out.

It turns "I think it read everything" into "I have proof it did," and as a preflight before an aggregation task that trade is cheap. Always append to a copy, never to the original.

CSVs Are Where This Bites Hardest

The App Store Connect reports for my apps run to tens of thousands of rows a month. Handed over raw, the months that live at the bottom of the file may not make it into the first page, and what comes back is a confident "not found."

My CSV routine now looks like this.

# 1. Inspect the header
head -1 /tmp/sales-2026.csv
 
# 2. Filter on the column you care about (rows where revenue >= 1000)
awk -F',' '$5 >= 1000' /tmp/sales-2026.csv > /tmp/filtered.csv
wc -c /tmp/filtered.csv
 
# 3. Compute the aggregate in bash
awk -F',' '{sum+=$5} END {print sum}' /tmp/sales-2026.csv

Asking Claude to interpret a small numeric summary beats asking it to reason over raw rows, both for context efficiency and for not missing anything.

The "Yesterday's Errors" Trap in Logs

A common version: you ask about yesterday's errors and get back only yesterday morning. The pattern I keep on hand for log triage is this.

# Error lines with surrounding context, most recent only
grep -nB 2 -A 5 "ERROR\|Exception" /var/log/app.log | tail -200

-B and -A give you context, tail keeps it recent. At the ~108 B per line I measured on that access log, 200 lines is about 20 KB — comfortably inside the budget. Live tailing (tail -f) isn't a good input for Claude; take a fixed snapshot and hand that over.

JSON Has Its Own Trap

Large JSON fails differently from logs and CSVs. Minified JSON has no newlines at all, so splitting by line count isn't even a coherent idea — the 968,901 B single line I measured overshoots the budget even at limit=1. Pretty-printed JSON runs the other way: about 14 B per line, so seeing "160,000 lines" and panicking leads you to slice it far finer than necessary.

For JSON I reach for jq and show only the shape plus a sample.

# What is the top-level shape?
jq 'keys' /tmp/big-payload.json
 
# How many entries are in the items array?
jq '.items | length' /tmp/big-payload.json
 
# A representative sample of three records
jq '.items[0:3]' /tmp/big-payload.json

A three-record sample plus a count and a key list is plenty to reason over. Handing across 160,000 lines of nested objects makes the answer worse, not better.

Don't Read Binary or Minified Files

Read assumes text. Pointing it at a minified JS bundle, an image, or a SQLite file burns context and degrades the response. Before v2.1.208, an extremely long single line could also exhaust memory.

My rule is simple: if one line exceeds 500 characters, summarize from bash regardless of how many lines there are. For a minified bundle, wc -c plus a look at package.json or the source map tells you more than dumping the file ever will.

PDFs and Jupyter notebooks are a separate case — Read has purpose-built handling. PDFs longer than 10 pages are read in ranges via a pages parameter, up to 20 pages at a time. Routing those to bash without knowing that is the long way around.

The broader idea of designing around tools that fail quietly is covered in Running Claude Code Without Breakage: A Failure-First Production Workflow. An unread tail is among the least visible failures in that family.

A Wider Context Window Sits on a Different Layer

Opus 5 is now the default Opus model in Claude Code, running with a 1M-token context window. The obvious thought follows: if the context is that wide, can't I just hand over the big file?

Not quite. Context size is how much the model holds at once; how much Read returns in one call is the tool's own decision. Different layers. Worth noting too that the 1M window is on by default for Max, Team, and Enterprise plans and on the API, so conditions vary — I'd check primary sources before building a workflow on it.

Where the wider window genuinely helps is downstream. Walking a file with offset across dozens of blocks and keeping all of them in play used to run into the "it forgot the early chunks" problem, and that has eased considerably.

So the stance is unchanged in one direction and improved in the other: still split large files or narrow them in bash, but expect to carry far more of the split results through a single session.

My Pre-Read Routine

Everything goes through read-plan.sh first. READ_DIRECT gets handed over; READ_RANGED uses the suggested limit, or gets pre-filtered with grep or awk. Anything resembling aggregation finishes in bash, with only the summary crossing over. When reaching the end of the file determines the answer, I append a sentinel before I start.

And when the response comes back, I look for the PARTIAL view line before I read the summary. Skipping that one line is what cost me half a day.

What to Try Next

Take a log or CSV of at least 100,000 lines, derive its bytes-per-line from wc -c and wc -l, and divide 40,000 by it to pick a limit. Call Read with that value and watch whether a PARTIAL view notice appears. That single loop is enough to start thinking in volume rather than line count.

Thanks for reading — I hope it saves someone the afternoon I lost to a tail I never actually read.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⟐ Claude Code2026-05-24
When Claude Code's Sequential Edits Cascade into Failure — Fixing 'old_string not found' and 'not unique' on the Same File
Run several Edits against the same file in one turn and the second or third one suddenly fails with 'not found' or 'not unique'. The cause is order-dependence inside a single context snapshot, and a Read + scoped patches usually rescue it.
⟐ Claude Code2026-05-10
Why Claude Code's Glob and Grep Return Zero Results
When Claude Code's Glob or Grep tool returns no matches even though the files clearly exist, the cause is almost always one of four things. Here's how to triage.
⟐ Claude Code2026-05-02
Stop the 'old_string is not unique' Error in Claude Code's Edit Tool — A Practical Rewrite Strategy
When Claude Code's Edit tool throws 'old_string is not unique in the file', the root cause is usually that the prompt didn't guarantee a uniquely identifiable target. Here's how to design rewrites that actually stick.