Saves tokens: when the model reads a large data or log file whole, it gets the first lines and a note, unless it asks for a range itself.

Install: /plugin install big-file-guard --marketplace samuelbfernandes/moddeck
Saves tokens. When the model reads a large data or log file whole, it gets the first lines and a note instead.
maxKb and the call names no offset or limit, the read is cut to the first headLines lines.big-file-guard: showed the first 200 of ~N lines (file is X KB). Ask for a range...offset or limit, other tools, small files, and other or unknown file types pass untouched./big-file-guard settings and tokens saved this session /big-file-guard off pause it (remembered across sessions); /big-file-guard on resumes
The / is required: a bare big-file-guard goes to the model like any prompt.
/big-file-guard off, the enabled setting, or /plugin disable big-file-guard.
enabled (default on), maxKb (default 256), headLines (default 200).
{ plugin: 'big-file-guard', key: 'isOff' } (boolean) and { plugin: 'big-file-guard', key: 'saved' } (number).
hooks/register.tsx 122 lines1// big-file-guard: a Read of a large data or log file with no offset/limit of its own is cut to the first lines,
2// and the model is told so in a note after the result. Never blocks.
3// FAIL OPEN: any error leaves the call exactly as the model made it (or the result exactly as it came back).
4import { atom, read, update } from 'claude-code'
5import type { EngineInterface, Register } from 'claude-code'
6
7type Dollar = EngineInterface
8
9const COMMAND = 'big-file-guard'
10const KINDS = new Set(['csv', 'tsv', 'json', 'jsonl', 'ndjson', 'log', 'txt'])
11
12const saved = atom({ plugin: 'big-file-guard', key: 'saved' } as const, 0)
13const isOff = atom({ plugin: 'big-file-guard', key: 'isOff' } as const, false)
14
15type Cfg = { isEnabled: boolean; maxKb: number; headLines: number }
16
17const num = (v: unknown, fallback: number, min: number): number =>
18 typeof v === 'number' && Number.isFinite(v) && v >= min ? Math.floor(v) : fallback
19
20const extOf = (path: string): string => {
21 const name = path.slice(Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) + 1)
22 const dot = name.lastIndexOf('.')
23 return dot < 0 ? '' : name.slice(dot + 1).toLowerCase()
24}
25
26async function setSwitch($: Dollar, off: boolean): Promise<void> {
27 if (off) await $.store.set('off', true)
28 else await $.store.delete('off')
29 await update($, isOff, () => off)
30}
31
32
33async function isSwitchedOff($: Dollar): Promise<boolean> {
34 try {
35 return await read($, isOff)
36 } catch {
37 return false
38 }
39}
40
41async function status($: Dollar, cfg: Cfg): Promise<string> {
42 const off = await isSwitchedOff($)
43 const state = !cfg.isEnabled ? 'disabled (the enabled setting is false)' : off ? 'off' : 'on'
44 const tokens = await read($, saved)
45 return [
46 `big-file-guard · ${state}`,
47 `Cuts csv, tsv, json, jsonl, ndjson, log and txt files over ${cfg.maxKb} KB to the first ${cfg.headLines} lines`,
48 `Saved this session: about ${tokens} tokens`,
49 ].join('\n')
50}
51
52async function run($: Dollar, cfg: Cfg, arg: string): Promise<string> {
53 const word = arg.trim().toLowerCase()
54 if (word === '' || word === 'status') return status($, cfg)
55 if (word !== 'on' && word !== 'off') return `big-file-guard · unknown option “${word.slice(0, 30)}”\nUse /big-file-guard, /big-file-guard on or /big-file-guard off.`
56 try {
57 await setSwitch($, word === 'off')
58 } catch {
59 return 'big-file-guard · that did not work, nothing changed'
60 }
61 return word === 'on' ? 'big-file-guard · on' : 'big-file-guard · off · /big-file-guard on turns it back on'
62}
63
64/** The size in bytes when this call should be cut, else undefined. Any doubt means undefined. */
65async function sizeToCut($: Dollar, cfg: Cfg, e: Record<string, unknown>): Promise<number | undefined> {
66 try {
67 if (e.tool !== 'Read' || !cfg.isEnabled || (await isSwitchedOff($))) return undefined
68 if (e.offset !== undefined || e.limit !== undefined) return undefined
69 const path = e.file_path
70 if (typeof path !== 'string' || !KINDS.has(extOf(path))) return undefined
71 const stat = await $.fs.stat(path)
72 if (stat.kind !== 'file' || stat.size <= cfg.maxKb * 1024) return undefined
73 return stat.size
74 } catch {
75 return undefined
76 }
77}
78
79export const register: Register = (on, options) => {
80 const cfg: Cfg = { isEnabled: options.enabled !== false, maxKb: num(options.maxKb, 256, 1), headLines: num(options.headLines, 200, 1) }
81
82 on('session.start', async ($, e, next) => {
83 try {
84 const stored = (await $.store.get('off')) === true
85 await update($, isOff, () => stored)
86 } catch {
87 // reads as on
88 }
89 try {
90 await $.command.register({ name: COMMAND, description: 'Show or switch big-file-guard: /big-file-guard [on|off|status]', argumentHint: '[on|off|status]' })
91 } catch {
92 // fail-open
93 }
94 return next(e)
95 })
96
97 on('command.run', { command: COMMAND }, async ($, e) => ({ text: await run($, cfg, e.args) })).catch(() => ({
98 text: 'big-file-guard · could not read its own state',
99 }))
100
101 on('tool.call', { tool: 'Read' }, async ($, e, next) => {
102 const size = await sizeToCut($, cfg, e as unknown as Record<string, unknown>)
103 if (size === undefined) return next(e)
104
105 const ran = await next({ ...e, limit: cfg.headLines } as typeof e)
106 try {
107 if (ran.deny !== undefined || ran.isError === true) return ran
108 const file = (ran.result as { file?: { content?: unknown; numLines?: unknown; totalLines?: unknown } } | undefined)?.file
109 const shownBytes = typeof file?.content === 'string' ? file.content.length : (ran.text ?? '').length
110 const shownLines = typeof file?.numLines === 'number' ? file.numLines : cfg.headLines
111 const total = typeof file?.totalLines === 'number' && file.totalLines > 0 ? `${file.totalLines}` : `~${Math.max(shownLines, Math.round(size / Math.max(1, shownBytes / Math.max(1, shownLines))))}`
112 const kb = Math.round(size / 1024)
113 const note = `big-file-guard: showed the first ${shownLines} of ${total} lines (file is ${kb} KB). Ask for a range (offset and limit), or use head/wc/summary tools for the rest.`
114 const gain = Math.max(0, Math.floor(size / 4) - Math.floor(shownBytes / 4))
115 await update($, saved, n => n + gain)
116 return { ...ran, context: [...(ran.context ?? []), note] } as typeof ran
117 } catch {
118 return ran
119 }
120 }).catch(($, e, next) => next(e))
121}
122types/index.d.ts 11 lines1declare module 'claude-code' {
2 interface PluginState {
3 'big-file-guard': {
4 /** Approximate tokens kept out of the conversation this session (bytes / 4, minus what was shown). */
5 saved: number
6 /** The persisted on/off switch (`$.store` 'off'), mirrored so the moddeck side bar can read it. */
7 isOff: boolean
8 }
9 }
10}
11