Stops Claude from reading a long file whole: a Read of a file over 1,000 lines without a range is refused with a hint to grep first and read only the lines it…

Reading a 5,000-line file whole puts all of it in the context window, and you pay for it again on every following turn. jr-read-cap refuses whole-file reads of long files and tells Claude to search first, then read only the range it needs.
jr-read-cap: src/app.ts has 2,500 lines, over the 1,000-line cap for whole-file reads.
Search it first (Grep for the symbol or text you need), then Read only that range with offset and limit.
allow_large) goes through, unless you turn that off./config)| Option | Default | |
|---|---|---|
| Line cap | 1000 | Minimum 50 |
| Let explicit large reads through | on |
/plugin install jr-read-cap --marketplace jrlprost/claude-code-mods
Reads the size and text of files Claude is about to read, to count lines. Writes nothing, runs nothing, sends nothing over the network.
hooks/register.ts 21 lines1import type { Register } from 'claude-code'
2
3import { countLines, denial, isWholeRead } from './cap'
4
5const MAX_TEXT_BYTES = 4 * 1024 * 1024
6
7export const register: Register = (on, options) => {
8 const cap = Math.max(50, Number(options.maxLines ?? 1000) || 1000)
9 const respectAllowLarge = options.respectAllowLarge !== false
10
11 // A whole-file Read of a long file costs its full length in context: ask for a range instead.
12 on('tool.call', { tool: 'Read' }, async ($, e, next) => {
13 if (!isWholeRead(e, respectAllowLarge)) return next(e)
14 const stat = await $.fs.stat(e.file_path)
15 if (stat.size > MAX_TEXT_BYTES) return { deny: denial(e.file_path, null, cap) }
16 const lines = countLines(await $.fs.read(e.file_path))
17 if (lines > cap) return { deny: denial(e.file_path, lines, cap) }
18 return next(e)
19 }).catch(($, e, next) => next(e)) // A file the mod cannot measure (missing, unreadable) goes to Read, which says why.
20}
21hooks/cap.ts 15 lines1// Files Claude reads whole on purpose: images, PDFs, notebooks. Their line count means nothing.
2const SKIP = /\.(png|jpe?g|gif|webp|svg|ico|pdf|ipynb|heic|bmp|tiff?)$/i
3
4export type ReadInput = { file_path: string; offset?: number; limit?: number; pages?: string; allow_large?: boolean }
5
6/** True when this Read would take the whole file and the cap applies to it. */
7export const isWholeRead = (e: ReadInput, respectAllowLarge: boolean) =>
8 e.offset === undefined && e.limit === undefined && e.pages === undefined && !(respectAllowLarge && e.allow_large) && !SKIP.test(e.file_path)
9
10export const countLines = (text: string) => (text.length === 0 ? 0 : text.split('\n').length - (text.endsWith('\n') ? 1 : 0))
11
12export const denial = (file: string, lines: number | null, cap: number) =>
13 `jr-read-cap: ${file} has ${lines === null ? 'more than 4 MB of text' : `${lines.toLocaleString('en-US')} lines`}, over the ${cap.toLocaleString('en-US')}-line cap for whole-file reads. ` +
14 `Search it first (Grep for the symbol or text you need), then Read only that range with offset and limit (for example limit: 300).`
15