by nesaminua
Hooks that force Claude Code to use LSP instead of Grep for code navigation. Saves ~80% tokens
# Add to your Claude Code skills
git clone https://github.com/nesaminua/claude-code-lsp-enforcement-kitStop burning tokens on Grep. Make Claude navigate code like an IDE.
Claude Code defaults to Grep + Read for code navigation. This works, but it's wasteful:
"Where is handleSubmit defined?"
Grep approach:
Grep("handleSubmit") → 23 matches, ~1500 tokens of output
Read file1.tsx (wrong) → 2500 tokens
Read file2.tsx (still wrong) → 2500 tokens
Read file3.tsx (found it) → 2500 tokens
─────────────────────────────────────
Total: ~9,000 tokens, 4 tool calls
LSP approach:
find_definition("handleSubmit") → form-actions.ts:42, ~80 tokens
Read form-actions.ts:35-55 → ~150 tokens
─────────────────────────────────────
Total: ~230 tokens, 2 tool calls
~40x fewer tokens. Same answer.
A rule in CLAUDE.md saying "use LSP" helps ~60% of the time. Hooks make it 100%.
| Task | Grep approach | LSP approach | Saved |
|------|--------------|--------------|-------|
| Find definition of handleSubmit | Grep → 23 matches (~1500 tok) + 2 wrong Reads (~5000 tok) = ~6500 tok | find_definition → file:line (~80 tok) + 1 targeted Read (~500 tok) = ~580 tok | 91% |
| Find all usages of UserService | Grep → 15 matches (~1200 tok), scan results (~300 tok) = ~1500 tok | find_references → 8 file:line pairs (~150 tok) = ~150 tok | 90% |
| Check type of formData | Read full file (~2500 tok), search visually = ~2500 tok | get_hover → type signature (~60 tok) = ~60 tok | |
| Find component | Glob (~200 tok) + Grep (~800 tok) + Read wrong file (~2500 tok) = | → exact location (~100 tok) = | |
| Who calls ? | Grep → noisy results (~1500 tok) + 3 Reads to verify (~6000 tok) = | → caller list (~200 tok) + 1 Read (~500 tok) = | |
No comments yet. Be the first to share your thoughts!
InviteFormfind_workspace_symbolsvalidateTokenget_incoming_callsAggregate from a week of development across 2 TypeScript projects:
| Metric | With LSP | Without LSP (estimated) | |--------|----------|------------------------| | LSP navigation calls | 39 | — | | Grep calls on code symbols | 0 (blocked) | ~120 | | Unique code files Read | 53 | ~180 | | Estimated navigation tokens | ~85k | ~320k | | Tokens saved | | ~235k (~73%) |
How the estimate works:
v2.1 introduces provider-aware block messages. The kit detects which LSP MCP server(s) you have installed and tailors its suggestions accordingly:
typescript-lsp Claude Code plugin. Suggestions use mcp__cclsp__find_definition, find_references, find_workspace_symbols, etc.solidlsp wrapper). Suggestions use mcp__serena__find_symbol, find_referencing_symbols, get_symbols_overview.Detection reads user-level Claude Code config (~/.claude.json, ~/.claude/settings.json) and matches known server names. The shared helper is in hooks/lib/detect-lsp-provider.js — adding a new provider means adding one entry to its PROVIDERS registry, with no changes to the individual hooks.
PreToolUse PostToolUse
────────── ───────────
Grep call ──→ [lsp-first-guard.js] ──→ BLOCK
detects code symbols,
suggests LSP equivalent
Glob call ──→ [lsp-first-glob-guard.js] ──→ BLOCK
blocks *UserService*, **/handleFoo*.ts;
allows *.ts, *subdomain*, src/**
Bash(grep) ──→ [bash-grep-block.js] ──→ BLOCK
catches grep/rg/ag/ack
in shell commands
Read(.tsx) ──→ [lsp-first-read-guard.js] ──→ GATE
5 progressive gates
(warmup → orient → nav → surgical)
Agent(impl) ─→ [lsp-pre-delegation.js] ──→ BLOCK
subagents can't access MCP,
orchestrator must pre-resolve
LSP call ─────────────────────────────────────→ [lsp-usage-tracker.js]
tracks nav_count,
read_count, state
SessionStart
────────────
New session ──→ [lsp-session-reset.js] ──→ WIPE
clears stale nav_count for current cwd,
forces fresh warmup + re-enforces gates
v2 note: versions before v2 had two silent bypass routes that let Claude read code files without ever calling LSP: (1)
Glob("*SymbolName*")had no guard, and (2)nav_countpersisted for 24 h across sessions, so a new session inherited "surgical mode" (unlimited reads) from yesterday's LSP work. Both are closed in v2 bylsp-first-glob-guard.jsandlsp-session-reset.js. If you installed v1, re-runbash install.sh— it merges the new hooks without touching your existing settings.
lsp-first-guard.js — Grep BlockerHook type: PreToolUse | Matcher: Grep
Intercepts every Grep call. Detects code symbols in the pattern. Blocks with a suggestion to use the correct LSP tool.
| Pattern | Detected as | Action |
|---------|------------|--------|
| getUserById | camelCase symbol | BLOCK |
| UserService | PascalCase symbol | BLOCK |
| router.refresh | dotted symbol | BLOCK |
| write_audit_log | snake_case function | BLOCK |
| create-folder-modal | component filename | BLOCK |
| TODO | keyword | allow |
| NEXT_PUBLIC_URL | env var (SCREAMING_SNAKE) | allow |
| flex-col | CSS class | allow |
| *.md, *.json, *.sql | non-code file glob | allow |
| .task/, node_modules/ | non-code path | allow |
Block message example (with both cclsp and Serena detected):
⛔ LSP-FIRST BLOCK: 1 code symbol(s) in Grep — use LSP instead
Symbols: handleSubmit
LSP tools:
handleSubmit:
mcp__cclsp__find_references("handleSubmit") (cclsp)
mcp__serena__find_referencing_symbols("handleSubmit") (Serena)
If only one provider is installed, only that suggestion appears.
lsp-first-glob-guard.js — Glob Symbol BlockerHook type: PreToolUse | Matcher: Glob
Closes the gap where Claude searches for a symbol by filename pattern instead of content. Without this hook, Glob("*UserService*") silently returns the file, Claude reads it, and LSP enforcement never fires.
The guard parses the glob pattern, extracts alphabetic tokens, and blocks if any token looks like a code symbol (PascalCase, camelCase, or snake_case with 3+ parts). Lowercase-only tokens and short generic words are always allowed.
| Pattern | Detected as | Action |
|---------|------------|--------|
| *UserService* | PascalCase symbol | BLOCK |
| **/AuthProvider.tsx | PascalCase in path | BLOCK |
| *createOrder* | camelCase symbol | BLOCK |
| *handleSubmit* | camelCase handler | BLOCK |
| *get_user_sessions* | snake_case function | BLOCK |
| src/**/*.ts | extension pattern | allow |
| *.tsx, **/*.json | extension pattern | allow |
| *subdomain*, *auth* | lowercase concept | allow |
| **/middleware* | file concept | allow |
| tsconfig.json, next.config.ts | framework config | allow |
| README.md | docs | allow |
Allowed by design: lowercase concept searches (*auth*, *subdomain*) are legitimate file discovery by topic. Only symbol-shaped tokens (casing patterns) are blocked, because those should use find_workspace_symbols instead.
bash-grep-block.js — Shell Grep BlockerHook type: PreToolUse | Matcher: Bash
Same detection logic, but for Bash(grep "UserService" src/), Bash(rg handleSubmit), etc. Claude sometimes tries to bypass the Grep hook by shelling out.
Allows: git grep (history search), non-code paths, non-code file type filters.
lsp-first-read-guard.js — Progressive Read GateHook type: PreToolUse | Matcher: Read
The most sophisticated hook. Forces a "navigate first, read targeted" workflow through 5 gates:
Gate 1 — Warmup Required
No LSP state file → BLOCK
Must call get_diagnostics(<any .ts file>) first
Gate 2 — Free Orientation (reads 1-2)
ALLOW — explore freely, no restrictions
Gate 3 — Warning (read 3)
WARN if no LSP nav calls yet
"Next Read will be BLOCKED"
Gate 4 — Navigation Required (reads 4-5)
BLOCK if nav_count < 1
Must use at least 1 LSP navigation call
Gate 5 — Surgical Mode (reads 6+)
BLOCK if nav_count < 2
After 2 nav calls → unlimited reads forever
Session flow:
Session starts
│
├─ Read(page.tsx) → Gate 1 BLOCKS → "warmup required"
│
├─ get_diagnostics(file.ts) → tracker writes warmup_done=true
│
├─ Read(page.tsx) → Gate 2 allows (1 of 2 free)
├─ Read(actions.ts) → Gate 2 allows (2 of 2 free)
├─ Read(types.ts) → Gate 3 WARNS
├─ Read(helpers.ts) → Gate 4 BLOCKS
│
├─ find_workspace_symbols("MyFunc") → tracker: nav_count=1
│
├─ Read(helpers.ts) → unlocked (reads 4-5)
├─ Read(utils.ts) → unlocked
├─ Read(service.ts) → Gate 5 BLOCKS
│
├─ find_references("MyFunc") → tracker: nav_count=2
│
└─ SURGICAL MODE — all Reads unlimited
**Always all