skim

Stop paying twice for
the same file dump.

A Claude Code plugin that catches oversized Read, wide Grep, and noisy Bash calls before they run and hands back a compact Haiku-produced digest instead — so bulk content never lands in your session's context, and never gets re-sent on every turn after that.

Claude Code plugin Go 1.23 Uses the PreToolUse hook Worker model: Haiku 4.5 Fails open No dependencies MIT license

01The problem

Agents burn a large share of their token budget on work that involves no reasoning: reading big files, scanning wide grep results, absorbing noisy command output. That bulk lands in the main context — and then gets re-sent with every following turn, so one 90KB file read isn't paid for once. It's paid for on every turn until the session ends.

Nothing is lost. The digest tells the model which line ranges matter, and it can always Read again with offset/limit on just the range it needs — or run skim cat <path> to force the full, unmodified file.

02Three interception paths

The only lever skim uses is Claude Code's PreToolUse hook: deny the call, and put a replacement digest in permissionDecisionReason. No argument rewriting, no PostToolUse substitution — one mechanism, stable across versions. When a hook allows a call through, the tool runs completely normally.

Read → file map

Passes through if the call already has offset/limit, matches a passthrough glob, or sits under the size thresholds. Binaries and images always pass through. Otherwise: summary, line-range map, symbol list. At most 2000 lines are ever digested — the cap Claude Code's own Read has, since reading past it cannot save anything. A longer file says so and gives the offset to continue from.

300 lines / 60KB default

Grep → clusters

The hook counts matching lines with ripgrep first, using the call's own flags — -i, multiline, glob, type — so the decision is made on the query you actually asked for. Under the threshold, or already bounded by output_mode: count or a small head_limit, it passes through. Over it: matches grouped by directory and symbol.

60 matches default

Bash → run digest

Noisy commands (cat, curl, big tail, test runners) are redirected to skim run -- <cmd>, which executes them, streams full stdout and stderr to a log file, and returns summary, key lines, exit code and log path.

nothing discarded — just off to the side

Degrade-open guarantee. If the worker exits non-zero, times out, or returns output that doesn't parse, the hook allows the call through so it proceeds exactly as if skim weren't installed — the failure just gets logged. An optimizer breaking must never block you. Every degradation path fails toward unmodified Claude Code behavior, never toward blocking.

03Model-agnostic on the driving side

skim never branches on which model runs your session — Sonnet, Opus, Fable, or whatever ships next. It hooks tool calls, not the model, so detection and substitution are identical for all of them. Only the worker is pinned to a cheap tier.

The value of what's saved scales with your session model's per-token weight: highest on Opus, lowest — and genuinely negative, once worker calls are counted — when the session already runs on Haiku. skim stats settles it in dollars rather than tokens: the saving priced at your session model's rate, against what the worker actually billed.

04No API key required

skim runs on whatever credentials Claude Code already has. There are two ways it can reach the worker model, and it picks the better one available.

TransportWhenFixed cost per call
claude -p CLI The default. Anything Claude Code can authenticate, including a subscription. ~1,300 tokens — a minimal system prompt replaces Claude Code's ~5,500-token default
direct API If ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN happens to be set ~0 — one POST to /v1/messages, no system prompt, no caching of one-shot content

A Claude Code subscription keeps its token in the OS keychain, and skim deliberately does not read it — borrowing a session credential for out-of-band API calls is fragile against refresh and expiry, and a subscription is not an API entitlement. So the CLI transport is what a plugin install actually uses, and it is the one that gets tuned. skim doctor prints which is active.

05Try it, then install it

Single Go binary, no dependencies, no API key — and you can watch it work before installing anything.

Without installing

--plugin-dir loads a plugin for one session only, so this touches no config:

make build
cd /some/project
claude --plugin-dir /path/to/skim/plugin

Then, in the session:

/skim demo

That writes a ~680-line sample into your directory and tells you to Read it. The read is intercepted and you get a ~25-line structural digest instead of the file — and the model can still answer questions about it. For the contrast: /skim off, read it again to get the whole file, then /skim on.

Installing

claude plugin marketplace add init-kaushal/skim
claude plugin install skim@skim

No Go toolchain needed. The file the hooks invoke is a small tracked launcher. On first use it downloads the prebuilt binary for your platform and verifies it against the release's published SHA256SUMS before running it — a download whose hash does not match is discarded, never executed. It is cached, so this happens once per version. Prebuilt for darwin and linux on arm64 and amd64; a local make build always takes precedence. Building from source is a last resort that only applies to a checkout — an installed plugin has no Go module beside it, so for installs the download is the route that matters. If every route fails the hooks exit 0 and do nothing — your tool calls behave exactly as if skim were absent — and /skim doctor says so.

/skim doctor   # verify the install, and which transport is active
/skim stats    # what it saved, and what the worker actually billed
/skim off      # kill switch, takes effect immediately

doctor checks that claude runs, that ripgrep is reachable (and probes it to confirm), that the config is valid, and which worker transport is in use. Binary distribution is a v1 simplification: the plugin ships what make build produces for your host, not multi-platform prebuilt artifacts.

06Configuration

~/.claude/skim/config.json, created with defaults on the first skim doctor run.

KeyDefaultMeaning
disabledfalseMaster kill switch — toggled by /skim off and /skim on.
read_max_lines300Files at or under this line count pass through untouched.
read_max_bytes60000Files at or under this byte size pass through untouched.
grep_max_matches60Grep results at or under this many matching lines pass through untouched.
bash_noisy_patterns[…]Regexes matched against the command string; a match redirects to skim run --.
passthrough_globs["**/*.md", …]Paths that always skip interception, whatever their size.
modelclaude-haiku-4-5-…Passed explicitly to every worker call — never inherited from the session model.
worker_timeout_sec45Worker timeout before the hook degrades open. Capped at 55s, under the 60s the plugin declares for its hooks.

Kill switches, checked in this order

  • SKIM_DISABLED=1 in the environment — forces every hook to a no-op, checked before anything else.
  • "disabled": true in config, toggled by /skim off and /skim on.
  • An invalid config.model — validated once at load, so no interception pays for a doomed worker round-trip.
  • passthrough_globs — a per-path opt-out that applies even when skim is otherwise fully enabled.

Separately, a recursion guard (SKIM_ACTIVE=1, set on every nested worker process) makes every hook a no-op the moment it detects it's running inside skim's own worker call.

07Honest limitations

Worth reading before you install this.

Latency

Every uncached interception adds one worker call — measured at 6–13 seconds from 17KB to 400KB, near-flat, because the cost is the call itself rather than the content. Caching absorbs repeat reads; /skim off is the immediate escape hatch.

Approximate line numbers

File-map line ranges come from a Haiku pass over the content, not an exact parse — nominally ±3 lines. When you need precision, Read with offset/limit, or skim cat.

Bash permission matching shifts

A redirected command is evaluated as skim run -- <original>, not the original string. Narrow Bash allow/deny rules will see the skim run … form — write them accordingly.

Costs are close on small files

The worker is cheap but not free, and it reads content in order to compress it. On Opus a long file pays for itself on the first read; a file just over the threshold needs to stay relevant for a few more turns. On a Haiku session it does not pay off at all. make bench FILE=… measures your case.

Savings shrink with cheaper sessions

The mechanic moves bulk content out of the session model's context, so its value scales with that model's per-token weight. On a Haiku session the trade can go negative once worker calls are counted.

Single-host binary

v1 ships whatever make build produces for your host rather than multi-platform prebuilt artifacts. skim doctor catches an arch mismatch.