SLOPSHOPPER

jr-read-cap

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…

newguard
★ 2v0.1.0MITupdated 2026-10-15jrlprost/claude-code-mods/plugins/jr-read-cap
A shopper browsing a rack in a slop shop
README

jr-read-cap

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.
  • Applies to Read calls with no offset, limit or pages.
  • Images, PDFs and notebooks are never capped.
  • A read Claude marks as deliberately large (allow_large) goes through, unless you turn that off.
  • A file the mod cannot measure goes straight to Read, which reports the problem itself.

Settings (/config)

OptionDefault
Line cap1000Minimum 50
Let explicit large reads throughon

Install

/plugin install jr-read-cap --marketplace jrlprost/claude-code-mods

Access

Reads the size and text of files Claude is about to read, to count lines. Writes nothing, runs nothing, sends nothing over the network.

Source 2 files
hooks/register.ts 21 lines
1import 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}
21
hooks/cap.ts 15 lines
1// 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