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.
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.
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.
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.
| Transport | When | Fixed 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.
| Key | Default | Meaning |
|---|---|---|
disabled | false | Master kill switch — toggled by /skim off and /skim on. |
read_max_lines | 300 | Files at or under this line count pass through untouched. |
read_max_bytes | 60000 | Files at or under this byte size pass through untouched. |
grep_max_matches | 60 | Grep 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. |
model | claude-haiku-4-5-… | Passed explicitly to every worker call — never inherited from the session model. |
worker_timeout_sec | 45 | Worker 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=1in the environment — forces every hook to a no-op, checked before anything else."disabled": truein config, toggled by/skim offand/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.