vs-token-safer: Instructions file for Claude Code

CLAUDE.md

vs-token-safer CLAUDE.md is an instructions file for Claude Code from JSungMin/vs-token-safer. It costs 19,098 tokens per session, scanned A, original, MIT.

Repository instructions for vs-token-safer, a local tool that searches C++ or C# code through official language-server indexes and returns a compact list of file locations and lines.

In plain words
What is it for?
Use it to orient work on the vs-token-safer project, run its required evaluation, and find source locations through clangd for C++ or Roslyn for C#.
Why use it?
It limits how much source code reaches the model and requires an evaluation check before changes, reducing broad raw-text searches and skipped project rules.

Instructions file for Claude Code

Written for Claude Code: SessionStart hook event. Also seen: reads .claude/ paths; mentions subagents; names the AskUserQuestion tool.

This is JSungMin/vs-token-safer's own configuration. It tells Claude Code how to work on vs-token-safer itself, so it is not a mod to install elsewhere. Copy it as a starting point and replace the rules that are about this project. Everything vs-token-safer configures →

Reuse

Borrowing it

Nothing to install: this file belongs to JSungMin/vs-token-safer. Take a copy, put it at the same path in your own repository, and replace the rules that are about this project with yours.

Copy the file
curl -O https://raw.githubusercontent.com/JSungMin/vs-token-safer/main/CLAUDE.md
Clone the repo
git clone --depth 1 https://github.com/JSungMin/vs-token-safer

Made for: Claude Code.

Wrote this? Show the measurements

A badge with what this costs and how it scanned, read live from this page, so it follows the numbers instead of freezing them. Markdown for a README, HTML for a documentation site or a project page.

agentmods badge for vs-token-safer CLAUDE.md

README.md
[![agentmods](https://agentmods.dev/badge/instructions/jsungmin/vs-token-safer/claude-md.svg)](https://agentmods.dev/instructions/jsungmin/vs-token-safer/claude-md)
Your own site
<a href="https://agentmods.dev/instructions/jsungmin/vs-token-safer/claude-md"><img src="https://agentmods.dev/badge/instructions/jsungmin/vs-token-safer/claude-md.svg" alt="Measured on agentmods" height="20"></a>
Per session 19,098 This file is loaded in full into every session.
When invoked 19,098 The same file — it is already loaded in full.
Security scan A 1 finding. A grade says what 26 rules found in the file — not that it is safe.
Origin original No closer match found in the catalogue.
Token cost

What it costs to keep this loaded

Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.

ModelPer sessionOnce invoked
Fable 5.1 $0.19098 $0.19098
Opus 5 $0.09549 $0.09549
Sonnet 5 $0.03820 $0.03820
Haiku 4.5 $0.01910 $0.01910

Measured 3d ago against content hash bb5f5f400135, method: parsed. Prices are Anthropic first-party input rates as of 2026-09-07, from the pricing page.

Security

Grade A, and why

vs-token-safer CLAUDE.md scanned grade A with 1 finding against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured 3d ago.

A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.

Runs shell commandslowCapability

Expected in a hook, worth knowing in a rule or an instructions file.

- **No console windows (Windows).** EVERY `spawn`/`execFile*`/`execSync` in `server/` MUST pass `windowsHide: true`.
CLAUDE.md · 662 lines

How it starts

The opening of the file, as written. The whole thing — 662 lines — stays where its author put it; the contents beside it link to each section on GitHub.

vs-token-safer — Claude rules

Force code search through an official language server's index (clangd for C++, a Roslyn-based C# LSP) instead of Bash grep, and token-cap the result to a compact file:line list. The Visual-Studio / IDE-agnostic sibling of rider-mcp-enforcer. Local-only. Ships as MCP server + CLI (vts). npm package + plugin name: vs-token-safer.

First, orient (every session)

  1. Read this file, then node eval/run.mjs — must print EVAL PASSED (41/41) before you change anything.
  2. Resume context lives in: this file · the wiki (wiki_query "vs-token-safer", pages under .omc/wiki/) · memory anchor project-vs-token-safer. The wiki Status and TODO page is the live checklist.

What's true

  • Engine = official, glue = ours. clangd (LLVM) / Roslyn (MS) do the analysis; we only write the LSP↔MCP glue. Never reuse a 3rd-party MCP server over source; never reimplement Roslyn.
  • Local-only, zero transmission. Same trust model as the other plugins. The token-cap returns file:line (no bodies) → less raw source reaches the model than grep-and-paste.
  • Async. runTool is async (LSP is async). MCP/CLI adapters must await and disposeClients().
  • Naming umbrella. "token-safer" is deliberately broad — more token-saving features/backends can be added under this name beyond C++/C# search.

Layout

  • server/lsp.js — generic LSP client (JSON-RPC/stdio). The one new, careful piece. didOpen is open-or-refresh: first call → didOpen(v1), a re-call on an already-open doc → didChange (bumped version, current disk text) so a file changed after warm-up isn't answered from a stale buffer; a since-deleted file → didClose. Position tools re-call didOpen before each query, so hover/goto/ outline/rename always re-read the file. The LSP engine keeps UNOPENED files fresh itself (clangd file-watch + background re-index); our warmset caches self-invalidate (include-graph by mtime+size composite key + an FNV-1a content hash [warmset.js fnv1a, zero-dep — codebase-memory-mcp XXH3 parity; reuses cached includes when bytes are unchanged despite mtime/size jitter, catches a real change a mtime-only key would miss], query-history by re-record; _censusCache is process-lifetime → restart/re-setup to refresh). LSP-spec conformance: server→client requests get shape-correct replies (_serverRequestReply: workspace/configuration→array, workspace/applyEdit{applied:false}, window/showDocument{success:false}, void reqs→null, unknown→MethodNotFound -32601); a timed-out request sends $/cancelRequest; client declares synchronization + workspace.configuration capabilities.
  • server/scope.js — INDEXING SCOPE (cold-latency attack): index a SUBTREE not the whole monorepo. Config scope / VTS_SCOPE (comma-list of dirs rel to root); vts setup --scope "MyGame,Plugins" persists it. scopeDirs/inScope/scopedCdb (writes a FILTERED compile_commands.json of only in-scope TUs to the out-of-tree dir → clangd --compile-commands-dir points there → it background-indexes far fewer TUs; live UE5: VTS_SCOPE=MyGame = 3,377 of 26,488 TUs (13%), ~7.8× cut) / scopeStats. UNIVERSAL: every backend's afterInit warm walk is scope-filtered too (no tsconfig/sln edit). backends/index.js effectiveCdbDir(root) = scoped CDB when scope set, else resolveCdbDir; scopeDirsFor(root). clangd STATIC PREINDEX: clangd-indexer (full LLVM release bundles it; VTS_CLANGD_INDEXER_CMD/next-to-clangd/ PATH) builds a monolithic .idx over the scoped CDB → clangd loads it via --index-file (LOCAL file, no remote server) for an instant project-wide index; buildStaticIndex/hasClangdIndexer/staticIndexPath, absent → warm-pass fallback + advisory to install full LLVM. Ops vts_scope (show scope + TU stats + top-level dirs) / vts_preindex (build ahead: static index if indexer present, else warm pass); CLI vts scope/vts preindex, folded into vts_admin. Eval guard 79. Env: VTS_SCOPE, VTS_CLANGD_INDEXER_CMD, VTS_INDEXER_TIMEOUT_MS (1800000). PREINDEX GATING: vts preindex DEFAULT = fast scoped background warm; the clangd-indexer STATIC --index-file (parses every in-scope TU, tens of min on a big scope) is OPT-IN via static=true/--static — never auto-run just because the indexer exists (an existing vts-static.idx is still auto-loaded, cheap). within-scope cert: completenessCert({scoped}) qualifies a semantic COMPLETE/0 as "within the configured indexing scope" (search_symbol/find_references), and clangdIndexAdvisory counts TUs/shards against the EFFECTIVE (scoped) CDB.
  • server/policy.js — UNIFIED TOOL-ROUTING POLICY (vts COMPLEMENTS Claude Code, not competes). shouldSuppressSteer(file) stays SILENT where CC-native is clearly better — generated/build-output paths (Intermediate|Binaries|Saved| DerivedDataCache|node_modules|build|dist|out|obj|.git, *.generated.*/*.g.cs/*.min.js); wired into the edit-steer hook (a whole-decl edit there isn't nagged AND isn't counted against adoption). VTS_SUPPRESS=0 off. routingDigest() = the SINGLE SessionStart message: a when-to-use-what decision tree (semantic→vts, whole-decl→ symbol-edit, doc/just-edited/sub-decl→CC-native Read/Grep/Edit, big-tree→scope+preindex) + live adoption posture + adaptive-controller state — replaces the adoption-only nudge in hooks/edit-report.js (one coherent policy, not scattered nudges). Eval guard 80.
  • server/treesitter.js + server/symindex.js — SYNTACTIC TIER (zero-setup fallback BETWEEN the semantic LSP and the literal text scan; the answer to "tree-sitter/embedding rivals are popular because they need no toolchain"). treesitter.js: lazy wasm tree-sitter (web-tree-sitter runtime + tree-sitter-wasms prebuilt grammars, both optionalDependencies — NO native build, Windows-safe; resolved via the sdk.js-style createRequire anchors). tsFileSymbols(abs) walks an AST → real DECLARATIONS (name+kind+line). 36 grammars ship; decl extraction is configured for ~17 languages — 10 via a hand-tuned node-type walk (C/C++/C#/JS/TS/Py/Go/ Java/Rust/Ruby; nameOf drills C/C++ declarator chains) + 7 via canonical server/tags/<grammar>.scm TAGS QUERIES (php/swift/kotlin/scala/dart/zig/bash) — a grammar with neither degrades to a GENERIC walk, never dark. The tags tier is the EXTENSION POINT: a TAGS sentinel config in EXT_MAP + a .scm with canonical @definition.<kind>/@name/@reference.* captures = a new language with NO JS (defTagsQueryFor/ extractTagDefs/extractTagRefs, validated against the bundled grammar; query-construct failure → graceful fallback). References too: TAGS langs read @reference.* from the same .scm, others use inline REF_QUERIES. tsSearchSymbols(root,q) ranks exact-before-substring across a scope (time+file-box). Charter-pure: tree-sitter is an OFFICIAL standard parser (GitHub/neovim), not a reimplement (the tags-query DSL is its own official interface, glue = ours); output stays token-capped file:line; nothing transmitted; SYNTACTIC means it locates decls but does NOT resolve refs/overloads/types (the LSP's job — so it's BELOW the semantic tier). symindex.js: COMMITTABLE index (Codeix-inspired) — vts index writes a portable, git-committable, team-shareable .vts-index/symbols.jsonl (one record/decl, paths RELATIVE) via tree-sitter; searchSymIndex answers search_symbol INSTANTLY on a toolchain-less machine or before clangd's index builds (the 369s→51s cold problem). INCREMENTAL rebuild: the header carries a per-file manifest h:{rel:{mt,sz,h}} (mtime+size fast-path → warmset.fnv1a content hash); a rebuild REUSES unchanged files verbatim (no read, no re-parse — parsing is the cost), reads+hashes only stat-changed files, re-parses only on a real content change, drops deleted files. So vts index after editing a few files re-parses only those (returns reused/reparsed; shown in the op output). core.js syntacticSymbols (committed index → live tree-sitter, else literal scan) feeds the search_symbol no-backend / empty-result branches; completenessCert({syntactic}) labels it. Op vts_index{status} (CLI vts index [--status], folded into vts_admin). Eval guard 81; benchmark arm C (zero-setup: 150-file symbol search grep 4917 → tree-sitter 53 tok = 98.9%, no toolchain).
  • server/concept.js — FUZZY retrieval WITHOUT embeddings (approach "B"; the charter-pure answer to Code Context Engine's exact-vs-fuzzy critique — "how does the auth flow work" when you can't name the symbol). THE REPO IS ITS OWN THESAURUS: identifiers + the comments beside them are a distributional signal already in the source — tokens that NAME THE SAME THING co-occur. PURE/zero-dep module: splitIdent/tokenize (CamelCase/snake/digit split, drop digit+stop+len1), tokMatch (exact 1.0 / prefix≥4 0.7 — no stemmer), buildConceptModel(units,{maxUnitTokens:14}) (df + co-occurrence over per-decl token bags = name subtokens + leading-docstring subtokens; the UNIT must be TIGHT — a long header comment attached to the first decl makes a giant unit where everything co-occurs → junk, so cap it), assoc (PMI-lite c·N/(df·df)), idf, expandQuery({k,minAssoc:1.5,minCooc:2}) (gate single-shot noise: cooc≥2 AND df≥2), scoreSymbol (enriched·idf·bestMatch + comment channel ×0.5). HOT MCP tool concept_search (core.js: tokenize q → conceptIndexFor(root) [cached tree-sitter tsFileDeclDocs walk, scope-filtered, bounded] → expand → score [kind weight demotes const/var locals] → top-N with a relative floor VTS_CONCEPT_FLOOR 0.2 + VTS_CONCEPT_MAX 15; flow=true expands the top seed along the call graph via find_references direction). CLI vts concept --q "auth login flow" [--flow]. HONEST: A(subtoken)+D(comment) reliable, B(co-occurrence) recovers domain synonyms when vocab clusters (compile database→clang/ubt/generate) but noisy on cross-cutting generics; pure- synonym-no-lexical-bridge residual genuinely needs embeddings (stated). treesitter.js tsFileDeclDocs = decl + attached leading comment (gap≤3, skip header blocks ≥4 lines, cap 200ch) feeds the concept units. NO embeddings, nothing transmitted, output token-capped file:line. Eval guard 83; follow-up paper paper/fuzzy-concept- dictionary.tex (companion to the Token-Safer paper, motivated by the CCE correspondence). Env: VTS_CONCEPT_*. SCORING = 3 deterministic channels (name > path > comment; scoreSymbol) + 2nd-pass structural-proximity boosts off TWO neighbour graphs (same LARGER anchor gate, reranks the matched set, never invents a match): (a) import-graph (importSpecifiers → within-repo basename adjacency; lifted by VTS_CONCEPT_IMPORT_FACTOR 0.3 × the neighbour's score) and (b) git CO-CHANGE (server/cochange.js cochangeNeighbors — files committed together in the last VTS_COCHANGE_MAX_COMMITS 500 commits are coupled; mega-commits > VTS_COCHANGE_MAX_FILES_PER_COMMIT 30 skipped as merge/format noise; ≥ VTS_COCHANGE_MIN_WEIGHT 2 co-commits to count; lifted by VTS_CONCEPT_COCHANGE_FACTOR 0.25, BELOW imports — a softer signal). Co-change is the M1 migration toward the Cursor/Augment "what clusters semantically" axis, embedding-free: the repo's own history is the cluster signal. PURE parseCoChange (git-log text → pair weights, both directions) + thin git log read, cached in conceptIndexFor; absent git → empty map (boost no-ops). VTS_CONCEPT_COCHANGE=0 off. Eval guard 89; live-verified on the vts repo (core.js → 49 co-change neighbours: eval/run.mjs, policy.js, …). A click-feedback loop was CRITIC-REJECTED (self-confirming via position bias, non-deterministic, unmeasurable, erodes inspectability); the charter-pure adaptation paths are these code-mined structural signals + a committable synonym file (DONE): a team-curated, git-committable <root>/.vts-index/concept-synonyms.json ({ "term": ["syn", …] }) — concept.js parseSynonyms (tokenises keys+values) feeds expandQuery({synonyms}), injecting a curated bridge at weight 0.95 (below an exact 1.0, above a mined neighbour); additive (absent/malformed → mined model alone), inspectable, deterministic, no drift. Eval guard 83. TWO PAPER MIGRATIONS (charter-pure, no embeddings): (1) LARGER confidence-gate (concept.js anchorConfident / VTS_CONCEPT_ANCHOR_MIN 0.5) — the import-graph proximity boost fires ONLY from high-confidence anchors (a neighbour lifts a symbol only if its own base clears ratio×topBase), so a weak/cross-cutting neighbour can't drag its imports up (live-verified: cross-repo gamedev/uiLang noise dropped); (2) RM3 PRF (concept.js prfTerms / VTS_CONCEPT_PRF*) — a 2nd-pass mines feedback terms from the TOP-k pass-1 results' OWN vocabulary (name+comment subtokens, ≥2-doc consensus, idf-ranked, capped) and re-scores, bridging a synonym the query missed ("warm"→warming/dominant, "reachable"→fixpoint/cascades); the climb SEED stays the PRE-PRF intrinsic exact match (base0) so PRF widens recall without drifting the seed. Eval guard 83 (#c anchor, #d prf). PRECISION-LADDER NAV (VTS_CONCEPT_STEER): search_symbol(exact)+multi-word-miss → steers DOWN to concept_search; concept_search → points UP to find_references/goto. See [[identity-and-roadmap]].
  • server/textstruct.js — STRUCTURE tier for prose/config files (the naming-umbrella extension: token-safer for DOCS, not just code). A text file's "symbol tree" = its SECTION hierarchy, so the EXISTING name-addressed tools work on it: document_symbols → token-capped table of contents, read_symbol → ONE section (not the whole file), replace_symbol_body/insert_symbol/safe_delete → edit a section BY ITS HEADING/KEY (no whole-file Read + line-count). EXTENSIBLE provider registry (PROVIDERS: ext→parser): markdown/mdx (ATX+ setext, fence-aware), asciidoc, reStructuredText, toml/ini ([section]), yaml (indent-nested keys), json (pretty-printed keys), txt (heuristic), css/scss/less (parseCss: top-level selectors / at-rules (@media/@keyframes) at L1, SCSS-nested rules deeper, each with an EXACT brace-matched span via the shared htmlNetBraces scan — a stylesheet's "symbol tree" is its RULE hierarchy, so read/replace_symbol target ONE rule), html/htm/xhtml (parseHtml: <h1-6> + <style>/<script> blocks + id-landmarks at L1, and WITHIN style/script the top-level CSS selectors / JS FUNCTIONS at L2 via a brace-depth scan htmlNetBraces/ htmlJsDecl — so read/replace_symbol target a rule or function BY NAME; dogfooded on dashboard.html, a function read at ~153×). The heuristic embedded JS/CSS decls are tagged embedded so they can be REPLACED by exact tree-sitter ranges: tree-sitter INJECTION is DONE (was the deferred robustness upgrade) — treesitter.js htmlEmbeddedDecls(text) re-parses each <script>/<style> with the real javascript/css grammar for EXACT decl ranges, recovering decls the heuristic misses (a MINIFIED one-line script, two CSS rules on one line, and — crucially — a function inside a top-level IIFE (function(){…})(), the dashboard.html pattern: the heuristic's depth-0 brace scan misses it, the injection's maxBlockDepth≤1 walk recovers it). textstruct stays PURE (no fs/async/tree-sitter) — structOutlineInjected(file,text,injector) takes the parser from core.js (which owns tree-sitter) and falls back to the heuristic when it returns null (deps absent); resolveInOutline resolves against the already-computed (refined) outline. Each provider emits [{level,title,line[,endLine]}]; shared computeSpans sets the section span (a provider endLine — brace-matched — wins over the to-next-heading heuristic), resolveSection/resolveInOutline (exact-then-substring, line disambig) + fmtOutline are format-agnostic — add a format = add one parser. core.js: STRUCT_TOOLS + structTool (synthesises an LSP-shaped range from a section span → reuses symbolEditResult/applyEditsToText; computes the injected outline ONCE, then resolves against it); an isStructFile(a.path) SHORT-CIRCUIT runs BEFORE backend resolution (a .md/.toml/.css has no language server); document_symbols + read_symbol carry a completenessCert({section}) (the SECTION rung). NO new MCP tools (the 5 existing symbol tools just work on text files — zero tool-budget cost). Zero-dep core, PURE, local, token-capped. Eval guards 84 (CSS provider) + 81 (HTML injection, under the tree-sitter block). vts symbols/read-symbol/replace-symbol/insert/safe-delete --path X.{md,css,html}.
  • server/backends/index.js — clangd/roslyn/typescript/pyright spawn configs + pickBackend(root) (detect order: compile_commands→clangd > .sln/.csproj→roslyn > tsconfig/package.json→typescript > pyproject/*.py→pyright; strongest build-artifact first). MIXED-REPO FIX: a query that TARGETS a file uses backendForPath(a.path) (core.js — ext→backend: .py→pyright, .ts/.js→typescript, .cpp/.h→clangd, .cs→ roslyn) BEFORE pickBackend(root), so a .py/.ts file inside a clangd-rooted UE/C++ tree gets pyright/ typescript instead of clangd (else the query hits the wrong LSP, finds nothing, model abandons vts). Precedence (preferBackend, core.js): explicit a.backend > the path's OWN backend WHEN it CONFLICTS with a forced backend (one global server serves every repo, so a backend:"clangd" pinned for a C++ project must NOT be sent this repo's .js/.cs/.py → clangd answers -32001 invalid AST; live-found dogfooding goto on the vts repo while config pinned clangd for a UE tree) > forced VTS_BACKEND/config backend > backendForPath(a.path) > pickBackend(root). A path-less query (search_symbol by name) keeps the forced backend. Eval guard 55. CENSUS FALLBACK (core.js censusFallbackBackends): a path-less query keeping the forced/root backend is the mixed-repo hole — clangd (root) answers 0 for a Python symbol and the model abandons vts. So when the primary backend's search_symbol comes back EMPTY on a path-less, non-explicit query, retry against the OTHER backends the languageCensus shows have files (most-code-first) BEFORE the syntactic/literal fallback — still the EXACT rung, just from the right LSP. Gated: !a.path && !a.backend, count ≥ VTS_CENSUS_FALLBACK_MIN (1), VTS_CENSUS_FALLBACK=0 off; no cost on a single-language repo (census returns no other candidate). Eval guard 90. Override via VTS_CLANGD_CMD/ARGS, VTS_ROSLYN_CMD/ARGS, VTS_TS_CMD/ARGS, VTS_PY_CMD/ARGS. winShell flag spawns the npm .cmd shims (ts/pyright) through a shell on Windows. langIdForPath (lsp.js) maps file ext → LSP languageId. findProjectRoot(start) — bounded walk UP from a file to the nearest project marker (compile_commands/ *.uproject/.sln/.csproj/tsconfig/package.json/pyproject/…/.git as the repo-boundary fallback; nearest dir wins, never climbs past a .git). Feeds resolveRoot (core.js) so a per-call path pins the right repo on a globally-installed server.
  • server/core.jsrunTool() dispatch, token-cap formatters, savings ledger. Tools: search_symbol, find_references (accepts EITHER a 0-based path+line+character position OR a symbol NAME — the code-modification primitive: by-name resolves the decl via c.symbol [exact-name-then-path-endsWith ranking], didOpens it, queries references at location.range.start; no indexed decl → scanTextUnder literal-usage fallback. Discover showed name-driven usage hunts = the top bypass; this collapses the locate→position→refs dance that pushed the model to grep. CALL-HIERARCHY FOLD: a direction=callers|callees param turns the SAME tool into a MULTI-HOP call hierarchy [transitive callers = blast radius before an edit / callees] to depth hops [VTS_TRACE_MAX_DEPTH 5, node cap VTS_TRACE_MAX_NODES 80] via lsp.js prepareCallHierarchy→incoming/outgoingCalls [graceful -32601→[], traceFrom DFS w/ cycle+dedup guard, indented file:line tree]; codebase-memory-mcp trace_path parity but on the OFFICIAL LSP [zero-transmission, real semantic edges] and folded INTO find_references — NOT a new tool [no fixed-surface cost, reuses the symbol→pos resolution]. vts trace-calls CLI = references --direction callers. Eval guard 70; live-verified on the vts repo itself. NAV STEER (refNavSteer): a LARGE flat ref result (> cap or ≥VTS_REF_NAV_MIN 25) with no detail= appends a one-line nudge to the CHEAPER views of the same set — detail=file/dir (per-file blast-radius summary) or direction=callers (transitive caller tree); VTS_REF_NAV=0 hides), goto_definition (a kind param folds in type_definition/implementation/declaration via lsp.js gotoByKind → 3 more LSP nav requests, NO new MCP tools), hover, document_symbols, diagnostics (compiler/linter errors+warnings for a file as a token-capped file:line:col severity [code]: msg list, sorted error→hint + count summary — the compact alternative to reading raw build output; lsp.js diagnosticsFor stores publishDiagnostics PER-uri since notified only keeps the last, waits briefly for the first publish after didOpen; eval guard 63), rename (LSP; preview by default, apply=true writes); SYMBOL-LEVEL EDITING (Serena-parity, the mutating set — all preview-by-default, apply=true writes): replace_symbol_body / insert_symbol (position=after[default]|before — the after/before inserts MERGED into one tool to shrink the surface) / safe_deleteresolveSymbolForEdit (core.js) resolves a declaration by NAME via the LSP outline (documentSymbol's .range = whole body, .selectionRange = name; path pins the file else the index resolves it, optional line disambiguates), then splices text at the span via applyEditsToText (symbolEditResult shared preview/apply, reuses the rename read-only/Perforce note). safe_delete refuses while the symbol is still referenced (refs at the name) unless force=true. Token win: edit by naming a symbol instead of Read-ing the whole file + line-counting for an exact-match Edit. Eval guard 52. detect_changes (REVIEW BY IMPACT — a SURFACE not a rung: git diff [working tree / staged / base=<ref>, via runExternal] → parseDiffHunks changed line ranges → innermost enclosing decl per hunk [documentSymbol, tree-sitter fallback] → blast radius [buildCallGraph callers, depth-bounded] + a DETERMINISTIC risk band [server/detect-changes.js scoreRisk: 4 code-mined channels — blast / cascade-depth / git co-change coupling-GAP (cochangeNeighbors: partners historically co-changed but ABSENT from this diff) / test-reach dampener; fixed weights, NO learned artifact, no click-feedback, hand-recomputable]; EXACT rung when the blast came from the LSP, SYNTACTIC when a file fell back to tree-sitter [blast unknown, said so]; capped [risk] symbol file:line, no bodies. The charter-pure answer to code-review-graph — official LSP for the blast, your own git history for coupling, no persistent DB, no embeddings. VTS_DETECT_MAX_SYMBOLS/VTS_RISK_*; CLI vts detect-changes; eval guard 93); find_files, search_text (filesystem — sanctioned find/grep replacements, no backend needed; search_text TARGETING: path=<file> searches one named file / glob=<pat> matching files — naming it AUTO-INCLUDES that extension (a .md etc), no docs flag; docs=true (no path/glob) widens the project-wide sweep to README/docs/config exts — default stays code-only. The grep-block hook reroutes a file-targeted text grep [grep X README.md] → vts text --path README.md via buildDocsGrepRewrite, rewrite-only never blocks); vts_git, vts_p4 (OUTPUT COMPACTION, not index — run the real git/p4 and group/dedup/cap the result via server/compact.js: git status→by change-type+dir, log→one line/commit, diff→per-file +/- diffstat; p4 opened/status/reconcile→by action+depot-dir, changes→terse. The rtk slice under our roof + ledger; the grep-block hook reroutes a single read-only git status|log|diff / p4 opened|status|changes|reconcile here via buildVcsRewrite — never blocks, VTS_COMPACT_VCS=0 disables. git grep stays a CODE search. CLI vts git/p4 are full arg passthrough → run in cwd, no --projectPath). MCP-SURFACE FOLD: the 9 cold admin/meta tools (vts_git/vts_p4/vts_setup/vts_config/vts_savings/vts_savings_reset/vts_discover/ vts_warmup/vts_gen_compile_db) are NO LONGER advertised individually — they're folded behind ONE vts_admin{op,params} MCP tool (index.js maps vts_adminrunTool("vts_"+op,params); hot search/nav/edit tools stay first-class so the model still reaches for them). core.js runTool + the CLI keep the individual vts_* names UNCHANGED (the grep-block hook still reroutes git/p4 to the CLI, not this tool); eval guard 62. FOOTPRINT SLIM (v0.37.2 — vts's OWN MCP usage measured at ~24%, mostly the per-request tool schema): the self-evident common params (projectPath/backend/maxResults) carry NO description (shared ROOT/ BACKEND/CAP consts in tools.js), tool descriptions trimmed (adoption "USE INSTEAD OF" + routing cues KEPT) → tool-list schema 3455→2723 tok (−21%, recurring every API call); guard 62 cap 3500→2900. Also the per-RESULT completenessCert rung lines + EMPTY_HINT/LOG_STEER and the SessionStart routingDigest (policy.js) are trimmed ~40-50% (rung keywords + the one actionable command kept; guards 76/16/digest green). The folded ops: vts_warmup, vts_setup, vts_config, vts_savings (RTK-gain-style: graph/daily/history + est. USD over timestamped day buckets; ALSO FOLDS IN the bundled gamedev-log-analyzer's ledger [~/.gamedev-log-analyzer/savings.json, VTS_GAMEDEV_SAVINGS_FILE override] → a + gamedev-log-analyzer (logs) line + a COMBINED total, since its log-compaction saves toward the same goal; the dashboard /data does the same via savings.sources. Local file read only), vts_savings_reset, vts_discover (scans ~/.claude/projects/*.jsonl for code searches that BYPASSED vts → missed-token report + catch-rate; learn=true feeds their result files into the warm-set; ALSO MEASURES THE EDIT HABIT — classifyDeclEdit (server/edit-detect.js, SHARED with the enforcement hook) flags a built-in Edit/MultiEdit whose old_string is a whole declaration (replace → replaceDecl) OR whose new_string is (add → insertDecl) on a code file (≥VTS_EDIT_MIN_LINES+decl cue). CONTROL-FLOW EXCLUSION (dogfood-found FP): a ) { opener also matches if/for/while/switch/catch (…) {, so isWholeDecl now only counts the opener when the callee identifier is NOT a reserved control-flow keyword (else a multi-line if(…){…} block edited inside a body was flagged a whole decl → suggested replace_symbol_body symbol="if", not a named symbol); the hook's declSymbolName likewise refuses a reserved keyword as the symbol name. v0.26.2 GENERALIZED it: the construct is decided by the chunk's FIRST meaningful line — a CTRL_FLOW_FIRST header short-circuits to false BEFORE the DECL_KW check, so an if(…){ (void)x; … } / if(…){ static int n; … } block (DECL_KW void/static in the BODY) no longer false-positives (the v0.26.1 callee guard only covered the signature-opener branch). Eval guard 59. It attributes that file's PRIOR Read tokens [reads/readUse Read↔Edit correlation in scanBypasses, read counted ONCE] = the read a symbol-edit would've skipped → edit habit: line; ALSO editUnreached = how many had NO prior vts search on that file [searchUse/searchedBn basename match] = the fraction the search-result steer CAN'T reach. Measured 30d: 1284 whole-decl edits, ~468k tok read-first, 1194/1284 (93%) search-unreachable). STEER is THREE layers, soft→hard (Edit-rewrite impossible: cross-tool updatedInput can't switch Edit→MCP, and the read is sunk by Edit time so a block recovers nothing — only a LEARNING signal): (B) EDIT_STEER on a FOCUSED search_symbol (≤VTS_EDIT_STEER_MAX 10) / goto_definition result (VTS_EDIT_STEER=0 hides); (L1) the grep-block hook now also matches Edit|MultiEdit — a whole-decl replace/insert gets a MODEL-VISIBLE emitWarn with a READY symbol-edit call (replace_symbol_body/insert_symbol, declSymbolName best-effort names it), VTS_EDIT_WARN=0 off; (L1-Bash) the hook ALSO catches a code-file edit done via BASH — sed -i, an awk inplace/redirect, or a python/perl heredoc that opens a code file for write (isBashCodeEdit: a code-ext path AND an explicit write/in-place signal must BOTH be present, so a read-only sed pipe or a python build.py isn't nagged) — warn-only toward replace_symbol_body/ insert_symbol; the Edit-tool steer alone MISSED this (a python brace-match splice bypasses it — live-found on a large irregular-indent function), and Bash file-surgery is a big slice of the low symbol-edit adoption; (L2) OPT-IN escalation, VTS_EDIT_BLOCK_AFTER DEFAULT 0=OFF — set ≥1 and once the adoption ledger's ignore-streak hits it, a SAFE insert (insertDecl && !replaceDeclinsert_symbol can't corrupt) is BLOCKED ONCE (exit 2) then resetStreak() (fire-once, NOT a wall — a permanent block TRAPPED the agent: it fought the wall with Edit retries / code contortions instead of switching, and each blocked attempt re-escalated the streak; live-reproduced on US editing edit-ledger.js); a replace stays warn. ADOPTION LEDGER (server/edit-ledger.js, ~/.vs-token-safer/ edit-adoption.json, VTS_EDIT_LEDGER override): hook records builtin-warn (streak++), core.js records symbol-edit on every symbol-edit dispatch (streak→0); hooks/edit-report.js (SessionStart) re-injects the adoption % as a goal = the SkillOpt-style measure→re-inject loop (static skill can't self-improve, a re-injected live metric can). Eval guards 53 (steer+discover) + 54 (L1/L2 hook); eval/test-edit-steer.mjs. find_files/search_text write a recovery TEE file (VTS_TEE_DIR, default on-truncate) when a result is capped so the full set is recoverable without re-running; a capped search_symbol/find_references ("… N more") tees too (teeOverflow — the rows are already in memory, no re-query). The ledger aggregates PER TOOL (by tool: line in vts savings) so you can see where the win comes from. BOOT AUTO-LEARN (index.js, VTS_AUTO_LEARN default on when projectPath set): 3s after boot, autoLearn(root, 7) (core.js, shares scanBypasses with discover) harvests bypassed-search result files into query-history — the self-improvement loop runs unattended every server start.
  • agents/code-locator.md — context-isolated locator subagent (delegates a lookup, returns only file:line).
  • server/compact.js — PURE output-compaction fns (compactGit/compactP4, string→string, no spawn) for the vts_git/vts_p4 wrappers. Eval exercises them on canned input (deterministic). (No grep compaction here — grep reroutes to search_text, which scans + token-caps itself; there is no raw grep output to compact.)
  • server/viz.js + server/serve.js + server/dashboard.html + server/vendor/ — LOCAL DASHBOARD (vts serve, cbm-style viz but local-only/zero-transmission). viz.js buildVizData(root) assembles the savings ledger + language census + include-graph cache into one model; renderDashboardHtml() reads the SELF-CONTAINED dashboard.html (CSS/JS inlined; 3D graph via Three.js rendered WebGL). NO CDN — Three.js is VENDORED at server/vendor/three.module.min.js (MIT, r160) and served SAME-ORIGIN (/vendor/...); the page imports it relative, so nothing leaves the host. serve.js is node:http ONLY (no express/ws), binds 127.0.0.1 (never 0.0.0.0), routes /→html · /data→JSON (include graph) · /callgraph?symbol=&direction=&depth=→JSON (ON-DEMAND call graph via core.js buildCallGraph = LSP callHierarchy live, NOT a persistent semantic DB — the cbm-parity "call graph" view our charter allows; nodes carry calls/calledBy/repo, edges a call-site count [fromRanges], + totalCallSites) · /symbols?q=→JSON (core.js listSymbols = workspace/symbol autocomplete for the search box) · /vendor/<allowlisted file>. core.js repoLabelFor (findProjectRoot → basename) tags every node with its repository. /symbolgraph→JSON (core.js buildSymbolGraph = TREE-SITTER symbol graph: nodes=code files (weight=decl count), edges=within-repo import adjacency [importSpecifiers, text-based; ext WITHOUT the leading dot], SYNTACTIC + scope-filtered + bounded [VTS_VIZ_MAX_NODES], min.js/ vendor excluded, labeled syntactic so it's never read as the semantic call graph — the zero-toolchain viz twin of the clangd include-graph, works on any of the 17 tree-sitter langs; eval guard 94). The 3D viz: THREE modes (include / call-graph-by-symbol with live symbol autocomplete dropdown / tree-sitter symbol-graph), spherical-SHELL layout (radius ∝ node count+footprint, radius-aware collision so orbs don't clump/overlap), color: groups (union-find connected components) / repo (per-repository hue + legend) / heat, click-to-drill-into-a-group (Esc/Backspace pops out), focus/maximize + keyboard camera (WASD/arrows/+-/R/Esc), distance-scaled labels, highlight filter, metrics overlay (incl. call counts). OPT-IN + CLI-ONLY: started only by vts serve (cli.js special-cases it — long-running, --open launches the browser, --stop/SIGINT stop via a pidfile), NEVER by the MCP server, so the steady-state package stays a thin stdio client. Easy open/close via the skills/vs-viz skill + commands/viz.md / commands/viz-stop.md (/vs-token-safer:viz[-stop]). VTS_VIZ_MAX_NODES (200) bounds the graph. Eval guards 72 (dashboard + server) + 73 (buildCallGraph + /callgraph).
  • server/cli.jsvts <cmd>. server/index.js — MCP server (async handler → await runTool). PER-CALL ROOT: resolveRoot(a) (core.js) replaces the old single-pin a.projectPath || PROJECT_PATH || cwd for every query — precedence: explicit projectPath > a path's enclosing project (findProjectRoot, only when OUTSIDE every known root so an inside-path keeps clangd's compile-DB rooting) > an MCP workspace root

    PROJECT_PATH > cwd. resolveCwdRoot(a) for git/p4 (MCP root beats server cwd; pin still ignored). One global server now serves every repo a session touches, not just the pinned one. index.js does the MCP roots handshake (getClientCapabilitieslistRootssetMcpRoots, re-fetched on roots/list_changed, undefined-safe on old SDKs); no roots advertised → collapses to the old PROJECT_PATH || cwd. Boot prewarm/auto-learn use PROJECT_PATH || first MCP root so a config-less install warms the current workspace (ONE root only). BACKEND POOL (memory guard for dynamic roots): the clients map is BOUNDED — VTS_MAX_BACKENDS (2) LRU-evicts the least-recently-used idle client past the cap, VTS_BACKEND_IDLE_MS (300000, 0=off) reaps idle clients via an unref'd sweep; a client with an in-flight request (pending.size) is never evicted/reaped. Steady state ≈ 1 warm backend; bouncing 2 repos keeps both warm; a 3rd evicts LRU. __pool test surface + eval guards 45 (pool) / 46 (root resolution).

  • server/sdk.js — createRequire MCP-SDK resolution. server/ensure-deps.mjs — SessionStart installer.
  • server/warmset.js — prewarm ORDERING: orderForWarm (query-history > working-now [git status / p4 opened] > git-log recency > include-centrality [adaptive: prefix-read + VTS_CENTRALITY_BUDGET_MS
    • persistent include-graph cache that grows across warmups; VTS_CENTRALITY_MAX bounds the loop] > mtime) + recordQueryResults. Steers clangd's open-set so the warm window hits likely queries; git + Perforce. Used by backends/index.js afterInit + core.js (records result files per search). Also LANGUAGE-MIX warm sizing: languageCensus(root) (cached file-count per backend lang, skips node_modules/build/...), warmCap(root,backend,env,base) (per-backend open-cap scales to that lang's file count × VTS_WARM_CAP_RATIO, clamped [base,VTS_WARM_CAP_MAX]; explicit VTS_*_OPEN_CAP wins), and prewarmBackends(root,picked) (VTS_PREWARM_BACKENDS auto→[dominant] / all→every detected lang dominant-first / comma-list). index.js boot warms each selected backend with its adaptive cap → a multi-lang repo warms in language proportion.
  • server/psearch.js + the PowerShell matcher entry — THE SECOND SHELL. This environment exposes a PowerShell tool ALONGSIDE Bash; the PreToolUse matcher listed only Bash|Grep|Glob|Edit|MultiEdit|Read, so Select-String -Path <src> -Pattern "A|B|C" ran fully unenforced AND uncounted (discover's "share routed through vts" was computed over a channel set that excluded it — it read high for the wrong reason). Found live on a UE tree. classifyPowerShellSearch(cmd) is PURE (shared by the hook and matchBypass, like shell-split.js) → null | {kind:"content"|"files", pattern, target, symbol}. It never rewrites — a naive PowerShell parser (backtick escapes, $d interpolation, -Pat prefix binding, positional args) would silently rewrite into a DIFFERENT search, which is worse than not intervening; the hook emits a READY search_symbol/search_text/find_files call instead. Quiet by construction: a piped … | Select-String (filtering another command's output), a log/doc/config target, any mutating cmdlet in the command (Copy-Item/Remove-Item/… — a file list feeding a file-op must never be steered to a CAPPED list), a non-recursive Get-ChildItem, -NotMatch (an inverted match — any call we suggest would return the COMPLEMENT of what was asked), and every SCRIPTED-VALUE shape (-Quiet, .LineNumber/.Count, $x = (Select-String …), an if (…) test) — those put no bulk text in context, so there is nothing to save, and no vts tool can return a value into a PowerShell variable. The pattern must BE a symbol, not merely CONTAIN one ("// TODO: FixMe later" is prose; symbolIn requires every | alternative to be a whole identifier / Foo::Bar / struct Foo) — the Grep tool's whole-pattern rule. WARN-ONLY by default (VTS_PS_BLOCK=1 opts into blocking a symbol hunt): the channel was invisible until now so there is no conversion data, and this project already learned that a wall the agent can't satisfy makes it fight rather than switch (VTS_EDIT_BLOCK_AFTER defaults to 0 for the same reason). With qvts installed it still blocks — there a working alternative demonstrably exists. Eval guard 98 asserts BOTH halves — the classifier AND that hooks.json is actually wired to the tool (the wiring is the half that failed silently and no behavioural test sees it). Known, accepted: Get-Content F.cpp | Select-String X is piped, so ignored by design.
  • hooks/block-code-grep.js + hooks.json — grep-block. A Bash code search (grep/rg/ack/ag/findstr/ git grep/find -name) that is a SINGLE safe segment is REWRITTEN to the equivalent vts CLI command via PreToolUse updatedInput (token-capped, flow unbroken); anything ambiguous (pipeline, unsafe pattern, quote in the root) falls back to the exit-2 block. Segment splitting is QUOTE-AWARE (splitSegments in server/shell-split.js, SHARED with vts discover so enforcement and measurement agree): a | inside quotes is pattern, not pipeline — so grep "FooA|FooB" / grep "^#include" (the top bypass shapes per vts discover) rewrite to vts text (regex); inside double quotes \" is an escaped literal; SAFE_TEXT allows | ^ # (always double-quoted; $/space/backslash still rejected). grepNudgeFor embeds a READY-TO-USE equivalent call (identifier→search_symbol, regex→search_text) in every Grep nudge/block. GREP-TOOL enforcement v2 (A+) + v2.1: a clear SYMBOL HUNT is BLOCKED (exit 2) and routed to search_symbol/ search_text per isSymbolHuntGrep — (1) a bare identifier, (2) a regex with a code-structural cue (:: / literal ( / void·class·struct·enum·template), OR (3) v2.1 an ALTERNATION (A|B|C) carrying a CamelCase/snake identifier (MaxWalkSpeed|MaxExcessSpeed, get_value|set_value) — the top measured bypass (UE type/symbol enumeration). KEPT as warn (false-positive-safe): freeform single tokens, AND keyword alternations (TODO|FIXME/GET|POST — ALL-CAPS, no lower→upper transition, so no CamelCase signal). The reroute is search_text (same regex, token-capped) → no wrong/missing results, just friction. VTS_GREP_BLOCK=0 reverts all of it to warn-only. Measured: v2 block ~172k tok/30d; v2.1 adds ~319k (CamelCase alternation). GLOB/Search TOOL (filename search) v2.2: a CONCRETE code-file glob (*.cpp / Foo.h / **/Bar.* per isBlockableGlob) is BLOCKED → find_files (which is a DIFFERENT tool — can't updatedInput-rewrite a Glob — so it's a block with a ready-to-use find_files q=… projectPath=<dir hint from the glob/path>); a bare */**/* or code-DIR glob stays a warn. The warn alone was IGNORED — the model kept Glob-ing a giant UE tree and narrowing the path instead of switching (live dogfood). find_files/search_text are walk-BOUNDED: shared SKIP_DIRS (node_modules/Intermediate/Binaries/Saved/build/… ) + a 4s time box so a huge tree can't hang them. FIND-DIR FIX (v2.2): a Bash find <dir> -name X rewrite now HONORS <dir> as the find_files root (extractFindDir) — it was dropped, so find /abs/UE/path -name X searched the configured vts repo and falsely reported "No files" (a live correctness bug on a UE worktree). vts discover also counts the Glob tool as a find_files bypass. FILE-OPS FIND FP FIX (v0.33.14): a find doing FILE-OPS — its own -exec/-delete/ -type d (isFindFileOps), or alongside a file-op exec in the same command (hasFileOpsContext: cp/mv/tar/rsync/xargs/zip/du/… — a backup/copy du …; find … -name "*.cpp") — is NOT a code search → never blocked AND never rerouted to a (token-CAPPED) find_files, which would silently drop files from a copy/delete. A genuine code-file find -name "*.cpp" with no file-op still rewrites to find_files. Live-found: a UE-depot backup find got blocked + the capped reroute would corrupt the backup. grep stays strict (a literal grep in a pipeline is usually content filtering). Eval guard 17b. VTS_REWRITE=0 → block instead of rewrite; excludeCommands (config) / VTS_EXCLUDE_COMMANDS (csv) opt a command out; escape hatch VTS_ENFORCE=0. Messages i18n'd (uiLang(): Korean when VTS_LANG/config lang=ko OR OS locale ko-*, else English; VTS_LANG=en|ko forces). Copy is AGENT-DIRECTED — the actionable part instructs the assistant ("re-run with the vts tool matching the intent" + the concrete call), with a brief human-facing reassurance that the red box is a redirect ("hold on"), not a failure — the hook output is consumed by the MODEL, which is the one that re-runs, not a human picking from a menu.
  • skills/vs-search/SKILL.md — routing. commands/{setup,savings,update}.md. commands/update.md = /vs-token-safer:update — one-command REFRESH of a STALE committable .vts-index (op vts_admin{op:index} = incremental buildSymIndex, re-parses only stat/hash-changed files). Surfaced two ways: the SYNTACTIC · STALE cert (core.js) now names /vs-token-safer:update, and hooks/edit-report.js (SessionStart) emits a policy.js stalenessLine(indexFreshness(root)) cue INDEPENDENT of the adoption gate (a stale index matters on a fresh session too; en/ko; only when a committable index exists + it's stale). Eval guard 95. VTS_STALE_CHECK=0 off.
  • eval/run.mjs + eval/_mock-lsp.mjs — mock-LSP eval (no toolchain). Add a guard for every new path.
  • Config dir ~/.vs-token-safer, env prefix VTS_. MCP server name vs-search.

Read the full file on GitHub · 662 lines

Changes

What this file has done since we first saw it

Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.

  1. 3d ago Changed · +52 lines · +1,466 tokens per session bb5f5f400135
  2. 8d ago First seen · 610 lines · 17,632 tokens per session scan A 43c13f80a10a

Subscribe to this mod's changes

vs-token-safer CLAUDE.md is an instructions file published in the GitHub repository JSungMin/vs-token-safer (11 stars, last pushed today), licensed MIT. It adds 19,098 tokens to every session, about $0.0955 per session on Opus 5. A static security scan graded it A with 1 finding (runs shell commands). No closer match exists in the catalogue, so it is treated as the original; first seen 2026-08-30.