{
  "markdown": "# skim\n\n[![CI](https://github.com/helloderekg/skim-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/helloderekg/skim-mcp/actions/workflows/ci.yml)\n[![PyPI](https://img.shields.io/pypi/v/skim-mcp)](https://pypi.org/project/skim-mcp/)\n[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-3776AB?logo=python&logoColor=white)](pyproject.toml)\n[![License: MIT](https://img.shields.io/badge/license-MIT-3fb950)](LICENSE)\n\nToken-efficient **skim-then-expand** I/O for agentic models (Claude Code / desktop Claude).\n\n> Independent community project — **not affiliated with, endorsed by, or sponsored by Anthropic**.\n> No telemetry; runs entirely on your machine. See [Trademarks, privacy & license](#trademarks-privacy--license).\n\nInstead of reading a whole large file into context, the model calls `skim_open(path)` and gets a\ncompact **skeleton** — structure, signatures, and (for logs/data) preserved critical values like ids,\nnumbers, dates, and error codes — plus **anchor ids**. It reads the skeleton cheaply, then calls\n`skim_expand(handle, anchors=[...])` to pull only the exact spans it needs, **verbatim and lossless**.\n\nCompression is *lazy*, not *lossy*: nothing is paraphrased or destroyed, only deferred. Expansion is\nalways one call away — which is what makes it safe for a closed model that can't ingest latent vectors.\n\n![How skim works: a full read of tarfile.py costs 25,180 tokens; skim's skeleton plus one expanded function costs 8,906 tokens, 65% less, with every other span still one expand away](docs/skim-flow.svg)\n\n## What it does that other tools don't\n\nThe mainstream tools are **lossy in context** — repo-maps and `--compress` drop function bodies;\nsummarizers and compression models paraphrase. The closest neighbors either cover code only, or show\nthe model a *transformed* view and keep originals in a side cache. skim's line is stricter: **whatever\nlands in context is verbatim source, and everything not shown is one anchored expand away** — an agent\ncan edit code from what it read without re-reading. That holds everywhere:\n\n- **Files** — Python (`ast`) + ~17 languages (tree-sitter); bodies folded, one `expand` away.\n- **Whole repos** — `skim_repo` builds a ranked, token-budgeted map; expand exact code from any file.\n- **Command output** — `skim_run` compresses verbose test / build / log output, fully recoverable.\n- **Data & logs** — a generic path with a **retention guarantee** (ids, numbers, error codes, negations\n  are promoted into the skeleton, never silently dropped) and **dedup** of repeated blocks.\n\nSee [DESIGN.md](DESIGN.md) for the architecture and verified prior-art positioning.\n\n## Falsifiable, not claimed\n\nA reduction percentage is marketing until you can check it. skim ships the checker:\n\n```bash\nuv run skim-verify path/to/your/gnarliest_file.py     # any file at all; exit code tells CI\n```\n\nFive invariants, verified on **your** files, locally: every non-blank line recoverable, expands\nbyte-exact, anchors in-bounds, reconstruction equal to the decoded file, deterministic output. The\ntest suite enforces the same contract with Hypothesis fuzzing on every path; a reproducible `FAIL`\non any readable file is a bug — please report it. Summarize-first tools cannot ship this command,\nbecause for them the equivalent check fails by design.\n\nThe cost-vs-correctness question has its own open yardstick — [eval/ACCURACY.md](eval/ACCURACY.md)\nprices every eval question under a full read vs skim (including the rows where skim **loses**),\nand any other context tool can be scored under the same protocol.\n\n## Built to be audited\n\n- **Under 2,000 lines of stdlib Python for the whole package** (the reading engine is ~1,200) — no\n  ML models to download, no framework, no telemetry, dependency surface of one (`mcp`, plus\n  optional `tiktoken`/tree-sitter extras). An afternoon's security review covers all of it:\n  [SECURITY.md](SECURITY.md) is the threat model.\n- **A kill switch for every surface that isn't read-only:** `SKIM_RUN_DISABLED=1` removes the\n  shell tool, `SKIM_PATCH_DISABLED=1` removes the file editor; the readers keep working.\n- **Windows is a first-class platform, not a port:** command output decoded as UTF-8 (no cp1252\n  mojibake), process trees actually killed on timeout (`taskkill /F /T`), CRLF preserved by\n  `skim_patch`, and CI runs the full suite on `windows-latest` alongside Ubuntu.\n\n## Try the core (no install)\n\n```bash\npython demo.py path/to/file.py            # measured before/after token counts\npython demo.py                            # demo on examples/sample.log (retention + dedup)\n```\n\n## Results (hard data)\n\nEvery number below is generated **live** by `uv run python benchmarks.py` — full reproducible tables in\n**[BENCHMARKS.md](BENCHMARKS.md)**. Measured on the running interpreter's standard library (real code);\ntoken counts via tiktoken `cl100k_base` (a proxy for Claude's tokenizer; ratios are tokenizer-robust).\n\n**Token savings on real code** — 60 Python stdlib files: **387,911 → 122,660 tokens (68% fewer, 3.16×)**;\nper-file 1.5×–11×; **100% lossless**, deterministic; ~linear runtime (~4 ms / 1k lines); pure CPU, no\nGPU, no network, no model calls.\n\n**Multi-language** (tree-sitter, `[lang]` extra) — the *same* lossless engine on JS / TS / Go / Rust /\nJava / C / C++ / Ruby / PHP / C# and more. Example: a 481-line JavaScript module → **6.6× / 85% saved, lossless**.\n\n**1:1 before/after on a real task (distribution-level)** — *\"read the largest function in this module\"*;\nthe model opens the file, reads the skeleton, expands exactly the one function it needs. **Same answer,\nfull skim cost (skeleton + expand) counted, across 24 large modules:**\n\n| file | lines | function read | full read | skim (skeleton+expand) | saved |\n|---|---:|---|---:|---:|---:|\n| `tarfile.py` | 3,032 | `_proc_pax` (119 lines) | 25,180 | 8,906 | **65%** |\n| `optparse.py` | 1,682 | `parse_args` (37) | 12,830 | 5,610 | **56%** |\n| `pathlib.py` | 1,436 | `walk` (43) | 11,242 | 5,421 | **52%** |\n| `bdb.py` | 921 | `effective` (48) | 7,279 | 3,243 | **55%** |\n| `socketserver.py` | 864 | `serve_forever` (27) | 5,824 | 3,615 | **38%** |\n| **24-file total** | | | **176,897** | **71,877** | **59%** |\n\nPer-task savings: **median 51%, mean 51%, range 30–91%** across 24 tasks — the honest distribution, not a\ncherry-picked best case. The win shrinks when you need most of a file.\n\n**Head-to-head — same 30 files** (`compare.py` + real Repomix via `npx`):\n\n| approach | tokens | % of full | lossless? |\n|---|---:|---:|:--:|\n| full read (Claude `Read`) | 184,277 | 100% | yes |\n| **skim** | **64,043** | **34.8%** | **yes (lazy-expand)** |\n| Repomix `--compress` | 105,963 | 57.5% | no (bodies dropped) |\n| signatures-only (Aider/Basemind mechanism) | 20,276 | 11.0% | no |\n\nskim is the **only lossless** option and uses **40% fewer tokens than Repomix `--compress`**. The repo-map\napproach is ~3× smaller but discards bodies/docstrings/comments irreversibly. (Basemind is pure Rust and\nwasn't installed; its row reproduces the signatures-only mechanism — see [COMPARISON.md](COMPARISON.md).)\n\n**Correctness:** 0 of 54,215 non-blank lines unrecoverable across 80 files; 202 tests / ~80% coverage with\nHypothesis property fuzzing of both paths; an ~18,000-case adversarial campaign (every bug found is fixed\nand regression-locked). Reproduce: `uv run python bench.py` / `pytest` / `benchmarks.py`.\n\n## Run as an MCP server\n\nThe one-liner (installs from PyPI on first run, tree-sitter languages included):\n\n```bash\nclaude mcp add skim -- uvx --from \"skim-mcp[lang,tokens]\" skim-mcp\n```\n\nOr from source:\n\n```bash\ngit clone https://github.com/helloderekg/skim-mcp.git && cd skim-mcp\nuv sync --extra lang                       # MCP SDK + multi-language (tree-sitter); drop --extra lang for Python-only\n```\n\nRegister with Claude Code (use the repo's absolute path; forward slashes work on Windows):\n\n```bash\nclaude mcp add skim -- uv run --directory /abs/path/to/skim-mcp skim-mcp\n```\n\nOr Claude Desktop — add to `claude_desktop_config.json` (`%APPDATA%\\Claude\\` on Windows,\n`~/Library/Application Support/Claude/` on macOS), then restart:\n\n```json\n{\n  \"mcpServers\": {\n    \"skim\": {\n      \"command\": \"uv\",\n      \"args\": [\"run\", \"--directory\", \"/abs/path/to/skim-mcp\", \"skim-mcp\"]\n    }\n  }\n}\n```\n\n(If `uv` isn't found, use its full path from `where uv` / `which uv`.)\n\n### Tools\n\n- `skim_open(path, query=\"\")` → compact skeleton + anchor ids (read the skeleton; ids are in its `expand(\"aN\")`\n  markers). Pass `query` to also get `matches` (line + covering anchor) in the same call, no search round-trip.\n- `skim_expand(handle, anchors=[...])` → exact verbatim spans. Items are anchor ids (`\"a7\"`) or literal\n  line ranges (`\"L120-180\"`) for when a grep already gave you line numbers.\n- `skim_search(handle, query)` → which anchors/lines contain a string, without reading them.\n- `skim_run(command)` → run a shell command, get a compact expandable view of its output (tests / builds / logs).\n- `skim_repo(path, query)` → a lossless, ranked, token-budgeted map of a whole repo; expand exact code from any\n  file. Ranked by query match when you pass one, else by **import-graph centrality** (PageRank over which files\n  import which), so the load-bearing modules surface first.\n- `skim_patch(handle, anchor, new_text)` → replace exactly one expanded span **on disk**, drift-safe: refused\n  if the file changed since `skim_open`, LF/CRLF preserved, result re-verified from disk, fresh handle returned.\n  Because expands are verbatim, an edit built from one applies safely — *read 8% of the file, edit it anyway*.\n  (`SKIM_PATCH_DISABLED=1` turns it off.)\n- Spans are also **MCP resources**: `@skim:skim://doc/<handle>/span/<anchor>` pulls a span by reference.\n\n## What it looks like in practice\n\n**Fix a bug in a big module you barely need.** The task: \"why does `_proc_pax` mishandle pax\nheaders?\" in `tarfile.py` (3,032 lines, 25,180 tokens). Claude calls `skim_open`, reads a\nskeleton with every signature, spots `_proc_pax`, expands that one anchor, and answers from the\nexact 119-line body. Measured cost: 8,906 tokens including the skeleton and the expand. Same\nanswer, 65% fewer tokens (see the [benchmark table](#results-hard-data) — median across 24 such\ntasks is 51%).\n\n**Get your bearings in an unfamiliar repo.** `skim_repo(\".\", query=\"rate limit\")` returns the\nmost relevant files' skeletons, ranked, inside one token budget, each with its own handle. Claude\nreads the map, expands the two functions that matter from two different files, and starts editing\nwith the rest of the repo still unread but reachable.\n\n**A red CI run with 400 lines of noise.** `skim_run(\"pytest -q\")` returns the exit code plus a\ncompact view: repeated boilerplate collapses to \"identical to a3\" pointers, and the retention\nlayer promotes the load-bearing literals (error codes, file paths, counts) into view so the\nfailure is visible without expanding anything. When Claude needs the full traceback, it expands\nthat one block.\n\n**When not to skim (honesty).** A 40-line config file, or a file you're about to rewrite\nwholesale: just read it. The skeleton wrapper costs more than it saves on tiny inputs, and the\n[meter](#see-your-savings-live) will show those rows as negative rather than hiding them. skim's\nwin is the everyday case where you need 40 lines out of 2,000.\n\n## Getting Claude to use skim\n\n**Mounting skim makes the tools available — it does not make Claude use them.** Claude picks a tool per\ntask, and its default for \"read/review this file\" is the built-in **Read**, not `skim_open`. Smaller\nmodels (e.g. Haiku) are especially unlikely to reach for an MCP tool on their own. So skim saves tokens\nonly when it's actually invoked. Two ways to get there:\n\n**1. Ask for it, per task:**\n\n> Use skim to review `src/big_module.py`\n> Map this repo with `skim_repo` before you start\n> Run the tests through `skim_run`\n\n**2. Make it automatic — add a rule to your `CLAUDE.md`** (a project one, or global `~/.claude/CLAUDE.md`)\nso Claude reaches for skim without a reminder every turn:\n\n```\nWhen the skim MCP tools are available, prefer skim_open (files) and skim_repo (directories/repos) over\nthe built-in Read for anything larger than ~300 lines, and skim_run for verbose command output. Expand\nonly the spans you need — skim is lossless, so skimming first is never risky, only cheaper.\n```\n\nskim also ships a \"prefer skim\" instruction to the model, but models don't reliably act on server-level\ninstructions — the `CLAUDE.md` rule is what pulls hard enough, especially for smaller models. You can\nconfirm it's wired up any time: tell a session *\"use skim to open &lt;a big file&gt;\"* and watch the\n[meter](#see-your-savings-live) tick.\n\n**Or let skim do both steps — registration *and* the rule — in one idempotent command:**\n\n```bash\nuv run skim-mcp install                              # from a source checkout (project ./CLAUDE.md rule)\nuvx --from \"skim-mcp[lang,tokens]\" skim-mcp install  # from PyPI\nskim-mcp install --rule global                       # write the rule to ~/.claude/CLAUDE.md instead\nskim-mcp install --print-only                        # show what it would do, change nothing\n```\n\nRe-running never duplicates the rule; if the `claude` CLI isn't on PATH it prints the exact manual\ncommand and the Claude Desktop JSON instead.\n\n## See your savings live\n\nMount skim, then run the meter in a second terminal — a tiny localhost dashboard (pure stdlib, no\nnetwork, no deps) that reads the same `skim_calls.jsonl` the server writes and refreshes every second:\n\n```bash\nuv run skim-meter                        # -> http://127.0.0.1:17321\nuv run skim-meter --once                 # one-shot text snapshot instead of the web view\nuv run skim-meter --price-per-mtok 3     # optional: also show ~dollars saved at YOUR rate\n```\n\nIt shows, in real time: **tokens in** (what reading those files / running those commands in *full* would\nhave cost), **tokens out** (what skim actually put into context — skeletons plus every expand/search),\nand **% saved** = `1 - out/in`, **broken down by session**. It's honest — expands eat into the number,\nand a `skim_run` on a tiny output can even net negative.\n\n![skim live token-savings dashboard: 76.5% saved overall, 261,826 tokens in, 61,452 out, a By-session table (docs-pass 79.4%, api-refactor 74.7%) with last-active times, and a per-call feed](docs/skim-meter.png)\n\n**Per session:** every skim server process stamps a session id on each call, and Claude Code spawns one\nprocess per session — so the dashboard lists **every session** (even before it has used skim), sorted by\n**last activity**. Each row is labeled by your `SKIM_SESSION_LABEL` if you set one, else by the **first\nfile/repo that session touched** (else *idle*), alongside its **start time** and short id so you can tell\nthem apart. A **Clear** button (or `skim-meter --clear`) archives the log to a timestamped *ghost* file and\nresets the meter to zero — nothing is lost. (MCP doesn't tell the server which *sub-agent* issued a call,\nso per-connection/session is the finest split available server-side.)\n\n## Tests\n\n```bash\nuv sync --extra dev\nuv run pytest                  # unit + property-based fuzzing of the invariants (202 tests, ~80% coverage)\nuv run python bench.py         # invariant sweep + compression over the standard library\nuv run python check_invariant.py <file>   # check one file against the invariants\n```\n\nThe suite enforces six invariants for **any** input — lossless, round-trip-exact, reconstruction-exact\n(`full_text` equals the decoded file byte-for-byte), in-bounds, deterministic, never-crash — via\nhand-written edge cases, the real standard library, and Hypothesis fuzzing of both the code and\ngeneric paths.\n\n**Measure the expand-loop yourself:** [eval/QUESTIONS.md](eval/QUESTIONS.md) is a 7-question\nunder-fetch eval (every answer hidden behind an anchor, locked by a test);\n`uv run python eval/score_expand_loop.py` scores a real session's log against it, and\n[eval/ACCURACY.md](eval/ACCURACY.md) prices each question (full read vs skim, negatives included) —\nthe accuracy-vs-cost yardstick other context tools are invited to run against.\n\n## Limitations (honest)\n\n- **Code skeletons cover Python (`ast`) + ~17 languages via tree-sitter** — JS, TS, Go, Rust, Java, C,\n  C++, Ruby, PHP, C#, Kotlin, Swift, Scala, Bash, Lua, R. Install the optional `[lang]` extra; other text\n  falls back to the generic (lossless) block path. The long tail of ~300 tree-sitter grammars is\n  incremental (each language's folding is verified against real parses before it ships).\n- **Dense repetitive logs get Drain-style templates** (`~412x GET /api/<*> took <*>ms`) so the skeleton\n  shows *what* repeats; structured JSON/CSV skeletons are still roadmap, so compression there is modest.\n- **It depends on the model calling `expand`.** If the model answers from the skeleton when it needed a\n  collapsed body, it can be wrong. A steering hint mitigates this; `skim_calls.jsonl` lets you measure it.\n- **Functions defined inside `if` / `try` blocks aren't shown as signatures** (still lossless and expandable).\n- **`skim_run` runs shell commands on your machine** (to capture and compress their output), with the\n  privileges of the server process — the same capability class as an agent's Bash tool. Mount skim only\n  where you'd let an agent run commands; a prompt-injected model could invoke a destructive command.\n  Set `SKIM_RUN_DISABLED=1` to mount skim read-only (the shell tool refuses, the readers keep working).\n  Full threat model in [SECURITY.md](SECURITY.md).\n- **Handles live in the server process's memory.** They last the session (Claude Code runs one skim\n  process per session); after a restart an old handle returns a clean `unknown handle` error — re-open.\n  Handles are content-hashed, so a re-opened *changed* file gets a new handle and stale anchor ids can\n  never silently point at different lines.\n- **Savings depend on usage:** large on big files you read part of, a wash on small files or when you need\n  most of a file. Token counts use tiktoken (a proxy for Claude's tokenizer); ratios are robust.\n\n## Status & roadmap\n\n**Shipped:** Python `ast` + multi-language tree-sitter code skeletons (~17 languages, each verified\nagainst real parses), a generic block path with retention + dedup + Drain-style templating for dense\nlogs, `skim_run` (lossless command/test-output compression with process-tree-safe timeouts),\n`skim_repo` (whole-repo lossless map, ranked by query match or import-graph PageRank), spans as MCP\nresources, an under-fetch eval harness, and `skim-meter` (a live token-savings dashboard) — all behind\nan airtight test suite.\n\n**Roadmap:** a structured-data skeleton for JSON/CSV (schema + value stats), the long tail of\ntree-sitter languages, a hybrid push mode (auto-expand the top relevance-ranked anchor) if the\nunder-fetch eval shows models leaving answers on the table, and runner adapters so other context\ntools can be scored on the [accuracy-vs-cost yardstick](eval/ACCURACY.md).\n\n## Contributing, security, changelog\n\n[CONTRIBUTING.md](CONTRIBUTING.md) has the five invariants every change must keep (lossless,\nround-trip exact, in-bounds, deterministic, never-crash) and how to add a language.\n[SECURITY.md](SECURITY.md) is the threat model and how to report privately.\n[CHANGELOG.md](CHANGELOG.md) tracks releases; [RELEASING.md](RELEASING.md) is the release process.\n\n## Trademarks, privacy & license\n\n**Independent project.** skim is a community open-source project, **not affiliated with, endorsed by, or\nsponsored by Anthropic PBC, OpenAI, or any other company.** \"Claude\", \"Claude Code\", and \"Anthropic\" are\ntrademarks of Anthropic, PBC; \"Model Context Protocol\" / \"MCP\" are used descriptively to indicate protocol\ncompatibility; \"Aider\", \"Basemind\", and \"Repomix\" are trademarks of their respective owners. All marks are\nused nominatively, for identification and comparison only, and imply no endorsement. No third-party logos are used.\n\n**Benchmarks.** Figures in this repo are measurements under the documented conditions (tool versions, flags,\ncorpus, token counter, date), reproducible via the published scripts — not guarantees under other conditions.\nComparative figures for Aider, Basemind, and Repomix were produced with their then-current public releases;\ncorrections welcome via an issue.\n\n**Privacy & telemetry.** skim runs entirely on your machine. It makes **no network requests** and collects,\ntransmits, or sells **no data**. The only data written is a local `skim_calls.jsonl` log (skim tool calls —\nfile paths and span anchors — for your own debugging); it never leaves your computer. Delete it anytime, or\nset `SKIM_LOG_FILE` to redirect it. It may contain paths/snippets from your own files; treat it like any local log.\n\n**License.** [MIT](LICENSE). Runtime deps `mcp` (MIT, © Anthropic PBC) and `tiktoken` (MIT, © OpenAI) — see\n[NOTICE](NOTICE). The dev-only test dep `hypothesis` is MPL-2.0, used unmodified and never bundled.\n\nmcp-name: io.github.helloderekg/skim-mcp\n",
  "bytes": 21082,
  "sha": "d3e70cccde03727181948bd6e04e4e652b7cdb44b5f126949a240aebe2f2074e",
  "repo_slug": "helloderekg/skim-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_helloderekg_skim_mcp_d52b263a/readme"
}