Hooks that force Claude Code to use LSP instead of Grep for code navigation. Saves ~80% tokens
Repo: nesaminua/claude-code-lsp-enforcement-kit
What's inside
When Claude tries to Grep for a code symbol, the hook blocks with a copy-pasteable LSP command:
โ LSP-FIRST BLOCK: Pattern contains code symbol(s) โ use LSP instead
Symbols: handleSubmit, UserService
LSP tools:
handleSubmit:
mcp__cclsp__find_references("handleSubmit") (cclsp)
UserService:
mcp__cclsp__find_workspace_symbols("UserService") (cclsp)
When Claude tries to Read a code file without warming up LSP, the progressive gate blocks:
๐ก๏ธ LSP-FIRST READ GATE โ Gate 1: warmup required
Call one of these first:
mcp__cclsp__get_diagnostics("src/page.tsx") (cclsp)
CONCRETE CALL FOR THIS FILE (works in any project):
mcp__cclsp__get_diagnostics("src/page.tsx")
After warmup: 2 free Reads, then need LSP navigation.
No generic advice. Every block message is parametrized by the actual file Claude tried to touch.
git clone https://github.com/nesaminua/claude-code-lsp-enforcement-kit.git
cd claude-code-lsp-enforcement-kit
bash install.sh
# Windows: pwsh ./install.ps1
Restart Claude Code. Done. The installer is idempotent โ safe to re-run on upgrades.
Verify:
bash scripts/lsp-status.sh
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 | 98% |
Find component InviteForm | Glob (~200 tok) + Grep (~800 tok) + Read wrong file (~2500 tok) = ~3500 tok | find_workspace_symbols โ exact location (~100 tok) = ~100 tok | 97% |
Who calls validateToken? | Grep โ noisy results (~1500 tok) + 3 Reads to verify (~6000 tok) = ~7500 tok | get_incoming_calls โ caller list (~200 tok) + 1 Read (~500 tok) = ~700 tok | 91% |
Aggregate 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:
FAQ
claude-code-lsp-enforcement-kit is a Claude Code plugin with hand-picked skills for development work, indexed on Flowy. Install it with the command on its page. Its skills do not fire on their own yet. Request auto-invocation to have Flowy route them as you prompt. Free and open source.
Is this plugin yours?
Claim it with GitHubSubmit a pluginPromote it