Releases: sdsrss/code-graph-mcp
v0.31.0 — Multi-account isolation: honor CLAUDE_CONFIG_DIR
Closes #20.
Users with multiple Claude Code accounts (personal vs work) set
CLAUDE_CONFIG_DIR to keep their settings.json, plugins/, and
projects/ separate. The plugin previously hardcoded ~/.claude/ across
~15 call sites, so for any account running with the override:
- hook registrations were written to a file Claude Code did not read,
- adoption files / MEMORY.md sentinels landed in the wrong project dir,
- cache cleanup and
installed_plugins.jsonwrites pointed at the default
install, not the configured one.
Net effect: the plugin was effectively broken under multi-account isolation.
Fixed
- New shared helper
claude-plugin/scripts/claude-config.jsexposes
claudeHome()(returnsprocess.env.CLAUDE_CONFIG_DIR || ~/.claude,
re-read on every call). lifecycle.js,auto-update.js,doctor.js,session-init.js,
adopt.jsnow route all~/.claude/...paths through the helper.adopt.js: memoryDir()keeps its(cwd, home)signature for back-compat;
CLAUDE_CONFIG_DIRsimply overrides thehome + .claudejoin.adopt.js: isPluginModeInstall()matches both the legacy
~/.claude/plugins/marker andCLAUDE_CONFIG_DIR/plugins/.
Tests
- New
claude-config.test.js: env-var resolution + empty-string fallback. adopt.test.js:memoryDir+isPluginModeInstallhonor the override.lifecycle.e2e.test.js: full install subprocess writes into
CLAUDE_CONFIG_DIRand never touches the default~/.claude/.
Compatibility
No default-behavior change: when CLAUDE_CONFIG_DIR is unset, every path
resolves exactly as before. 118/118 plugin tests pass.
Full Changelog: v0.30.0...v0.31.0
v0.30.0 — UX pass: 16 silent-failure / misleading-feedback fixes
Four rounds of end-to-end dogfooding (fresh-project workflows, MCP stdio
fuzz, IO edge cases) surfaced 16 places where the tool silently swallowed
errors, gave misleading guidance, or returned empty results indistinguishable
from a successful no-op. No API removals; only additive IndexResult.files_deleted.
CLI feedback honesty
incremental-indexnow reports file deletions ("N files updated, M files removed").deps <file>distinguishes missing-file from no-imports; surfaces unresolved deps.callgraphno longer prints no-opResolved 'X' → 'X'.incremental-indexin non-git dir explains why it skipped (only in non-quiet mode).mapon empty project prints a friendly message instead of dangling "Modules:".
health-check / doctor contract
health-check --jsonemits valid JSON{healthy:false, reason:"no_index", ...}+ exit 0 when no index exists.doctorroutes "No index found" to the index-empty fix path (auto-runsincremental-index).
MCP empty-string args
get_call_graph/find_references/get_ast_node/find_similar_codereject empty/whitespacesymbol_name.get_call_graph/find_referencestreat emptyfile_pathas absent.trace_http_chain(route_path="")anddependency_graph(file_path="")reject upfront.
Path & filesystem hardening
module_overviewrejects absolute paths,../traversal, and Windows drive letters before hitting the DB.snapshot create --outpre-flights target path (dir-as-out, missing-parent) instead of leaking SQLite VACUUM INTO errors.scan_directorytolerates per-entry walk errors —chmod 000subdir no longer aborts the whole rebuild.
Quality
- 526 tests pass.
cargo +1.95.0 clippy --all-targets -- -D warningsclean on both default and--no-default-features.
Full Changelog: v0.29.0...v0.30.0
v0.29.0 — edge resolution precision pass (12 silent-failure fixes)
5 rounds of end-to-end dogfooding surfaced 12 silent-failure / mis-attribution bugs across the parser → resolver → MCP/CLI surface. Net on this project's own index:
- dead-code false positives: 21 → 6 (remaining 6 are documented design limits — receiver method calls, type-as-field references, cross-file constant access; not bugs)
- edges restored: 3030 → 3266+ (+8% real call relations recovered)
Parser
- TypeScript
return_typeno longer leaks": "prefix (was stored": string", now"string") - Rust generic impl trait now emits method-level edges (was 0 because
source_name="Foo<'a, W>"failed exact-match against bare"Foo") - Method-level implements edges no longer fan out to all same-name methods in one file (3 structs × 2 methods used to produce 9 edges instead of 6)
Resolver
CalleeMeta::Pathkeeps same-file targets per the spec ("same-file matches take precedence")path_filter_candidatesnow accepts single-file Rust mods (crate::domain::foo()→src/domain.rs) — this single fix eliminated 14 of 20 dead-code false positives
MCP / CLI input validation
ast_search/semantic_code_search/ CLI: invalidtypefilter now errors (was silent empty result + exit 0)find_references: invalidrelationnow errors (was silent fall-back to"all")get_call_graph:symbol_name+route_pathconflict now errors (was silent route-only)module_overview: empty path now errors ("."still works as documented match-all)
Infrastructure
- FTS5 NOT/AND/OR/NEAR queries no longer leak raw
fts5: syntax error(each token wrapped in"…") snapshot inspectrejects truncated SQLite (was returning fake-empty meta success)- Concurrent
incremental-indexgets friendly busy message + original error preserved for debug
Coverage
+17 regression tests (5 cli_e2e + 8 integration + 2 call_qualifier + 1 snapshot + 1 parser/relations). 587 tests pass; clippy 1.95 clean on --no-default-features and --all-targets under -D warnings.
Known limitations preserved (not bugs)
- Receiver method calls (
obj.method()) — needs Rust type inference - Type-as-field references (
pub foo: SomeStruct) - Cross-file constant access via Path qualifier
Full Changelog: v0.28.0...v0.29.0
v0.28.0
Full Changelog: v0.27.0...v0.28.0
v0.27.0 — Python call relations + dead-code truncation guard
Highlights
P0 fix — Python tier parity: tree-sitter-python uses call nodes, but the relation extractor only matched call_expression + a Ruby-guarded call arm — so every .py file produced 0 call edges despite README documenting Python as Full tier. Reindexing this repo: 0 → 2969 total edges. module_overview, find_dead_code, impact_analysis, get_call_graph, find_references all return correct results for Python now.
P1 fixes bundled:
cmd_overviewJSON empty path no longer smears anyhowError:on stderr;.normalizes to project root (matching MCPtool_module_overview).find_dead_codetruncation guard: callback args in the cut-off tail of long functions (envCODE_GRAPH_MAX_CODE_LEN) no longer false-positive as dead. Two-signal heuristic — trailing...sentinel and declared-span > stored-newline-count by 5+ lines — keeps Python ellipsis stubs unaffected.snapshot::create+snapshot::installbest-effortgitinvocations redirect stderr to/dev/null;fatal: not a git repositoryno longer leaks intocargo testoutput.
Removed:
LanguageConfig.call_node_kindfield — defined but never read by the dispatcher. Kept misleading future contributors into thinking config alone could add a new language; the Python regression was the proof. Replaced with an inline comment at the dispatch site enumerating every language's call node kind.
Surface
Internal-only — no Δ-contract on MCP tool schemas, CLI flags, or SQLite schema.
Regression coverage
5 new test anchors:
src/parser/relations/tests.rs::test_extract_python_bare_call,::test_extract_python_method_callsrc/storage/queries/dead_code.rs::tests::test_find_dead_code_skips_when_caller_content_truncatedtests/cli_e2e.rs::test_cli_overview_dot_means_project_root,::test_cli_overview_json_empty_no_anyhow_prefix
Verification
cargo +1.95.0 clippy --no-default-features --all-targets -- -D warningsclean on both targets- 467 tests pass (351 lib + 64 cli_e2e + 46 integration + 5 snapshot + others)
Provenance: autonomous iteration loop (4 rounds), 1 P0 + 4 P1 + 1 P2 surfaced, all fixed.
Full Changelog: v0.26.0...v0.27.0
v0.26.0 — context push default ON + trigger hints
v0.26.0 — UserPromptSubmit context push default ON + trigger hints
Changed
claude-plugin/scripts/user-prompt-context.js:computeQuietHooksdefault
flipped back to noisy (push ON). The v0.21 opt-in flip cited routing-bench
P@1=100% as evidence the agent already picks tools correctly without push,
but that bench measures triage accuracy once the agent has decided to query
a tool — not the prior question of whether the agent reaches for a tool at
all. The real counter-evidence is inpre-grep-guide.js's 15-day baseline:
429 rawgrepvs 191 functional CLI calls on the same indexed source tree
(~13× pre-training bias toward grep). Push is the corrective. Per-type
cooldowns (impact 30s / overview 5min / callgraph 60s / search 60s) cap
frequency; the 8-char message floor +shouldSkipfilter keep confirmation
chatter silent. Escape hatch:CODE_GRAPH_QUIET_HOOKS=1.claude-plugin/scripts/pre-edit-guide.js: caller threshold lowered from
directCallers < 2to< 1. Editing any function with one or more callers
now surfaces the one-line impact summary; the per-symbol 2-minute cooldown
is unchanged so the noise floor stays the same.- SessionStart
project_mapinjection (session-init.js) stays default
OFF — that hook is a static dump duplicated byMEMORY.md's decision
table; this hook is a reactive trigger reminder. The two defaults are
intentionally asymmetric.
Added
src/mcp/server/mod.rsMCPinstructionsfield gains one line of explicit
scenario triggers:"who calls X?" → get_call_graph; "impact of X?" or before editing a fn → get_ast_node include_impact=true; concept search without an exact symbol → semantic_code_search. Compile-time
assert!(NOISY.len() <= 1500)budget guard unchanged (now 772 / 1500 bytes).- Project
CLAUDE.md"Code Graph Integration" section replaced with a 5-row
trigger table (who calls / impact / module overview / concept search / HTTP
route) —CLAUDE.mdis loaded every session, higher priority than the
invited-memory path inMEMORY.md. claude-plugin/templates/plugin_code_graph_mcp.mdclarifies the asymmetric
hook defaults and listsCODE_GRAPH_QUIET_HOOKS=1as the context-push
escape hatch alongside the existingVERBOSE_HOOKS/QUIET_HOOKS=0flags.
Rationale anchor
- mem #8234 documents that hook content has bounded leverage when the
current bench corpus is saturated (Sonnet 4.5 hits P@1=100%); bench is the
right oracle for tool-description boundary disambiguation, not for
server-prelude / hook-content tuning. This release therefore lands without
a fresh routing-bench cycle — the changes are all hook-content surface.
Verification
cargo check: clean (compile-timeassert!(len <= 1500)on
NOISYinstructions string holds; final length ~772 bytes).node --test claude-plugin/scripts/user-prompt-context.test.js:
77/77 pass — sixcomputeQuietHookspriority-chain cases rewritten for
the default-noisy invariant; one e2e check kept on the=1escape hatch.- No change to
routing_bench.rscorpus; intentionally skipped per mem #8234.
Migration
- Existing users on default env will start seeing
[code-graph:impact| overview|callgraph|search]push lines on intent-matching prompts. Set
CODE_GRAPH_QUIET_HOOKS=1in~/.claude/settings.jsonenv to opt out. - Adopted projects: the
plugin_code_graph_mcp.mdtemplate auto-refreshes on
next SessionStart (unlessCODE_GRAPH_NO_TEMPLATE_REFRESH=1is set). - No data-migration, no schema change, no MCP tool API change.
Full Changelog: v0.25.1...v0.26.0
v0.25.1 — findBinary disk cache version-check
Fixed
find-binary.js disk cache (~/.cache/code-graph/binary-path) now validates the cached binary's --version against the package version before returning it.
The bug
Previously, the cache short-circuit at findBinary() entry only checked isNativeBinary(cached) — file exists with the right basename. Once a stale path was written, it shadowed every newer binary on the system forever, until the user manually rm-ed the cache file.
Asymmetric with the auto-update cache branch at :184-188 which was already version-gated (mem #8187 fixed three install-chain bugs but only landed on ~/.cache/.../bin/). The entry-level disk cache that runs on every hook tick had no equivalent gate.
End-user impact
After any platform-pkg path drift between npm-update cycles — version-pinned hash subdirs, nvm prefix switch, manual rm/reinstall — disk cache kept pointing at a path whose binary had aged. Hooks dispatched to the old binary until manual cache clear.
How it's fixed
New isCachedBinaryFresh(cachedPath, pkgVersion) helper:
- Reads cached binary's
--versionvia existingreadBinaryVersion - Compares to
package.jsonversion via existingcompareVersions - Stale → callers
clearCache()+ fall through to the rest of the discovery chain - Permissive on unknown values (missing pkg version, unreadable binary
--versionoutput) → trust the cache (don't refuse the only path we have)
Verification
node --test find-binary.test.js: 19/19 pass — 11 existing + 8 new cases:- THE BUG reproduction (cached
0.5.28vs pkg0.25.0→ invalidate) - cache ≥ pkg → fresh
- missing pkg version → permissive (trust cache)
- unreadable binary
--version→ permissive - non-existent path / null / undefined → not fresh
- file basename mismatch → not fresh
- THE BUG reproduction (cached
node --test lifecycle.test.js: 12/12.node --test pre-grep-guide.test.js: 39/39 (v0.25.0 hook untouched).cargo +1.95.0 clippy --no-default-features --all-targets -D warnings: 0 findings.
Migration
No user action needed. First findBinary call after upgrade detects stale cache → invalidates → walks fresh. Users on the dev branch with manually-recorded cache paths: rm ~/.cache/code-graph/binary-path triggers the same fresh walk.
🤖 Generated with Claude Code
Full Changelog: v0.25.0...v0.25.1
v0.25.0 — PreToolUse:Bash raw-grep → cg CLI hint
Added
pre-grep-guide.js — new PreToolUse:Bash hook nudging Claude away from raw
grep/rg/ag on the indexed source tree toward code-graph-mcp grep / ast-search / callgraph / show.
Motivation
15-day session telemetry on this repo (78 sessions / 13.5K assistant turns):
- raw grep on source trees: 429 calls
- code-graph CLI/MCP: 437 calls
- ratio overall ~1:1, but severe per-day variance — worst days went 10:0 against
code-graph-mcpwhile best days hit 23:78 in its favor.
Tool descriptions alone route correctly when Claude is already deciding between tools (tests/routing_bench.rs Opus 4.7 P@1=95.5% in tool-only mode), but pre-training bias gives grep -rn pattern src/ an enormous default weight that descriptions can't compete with. This hook closes the loop at the Bash entry point — same shape as the existing PreToolUse:Edit pre-edit-guide.js impact-summary hook.
How it fires
Fires when all of these match:
- Command HEAD is
grep/rg/ag(NOT piped —cargo test | grep FAILEDis an output filter, not a search) - Args include an indexed source-tree path (
src/tests/lib/scripts/claude-plugin/tools/pkg/cmd/internal/app/components/server/client/crates/packages/) - Not searching only a config/lockfile (
Cargo.toml.gitignore*.md*.json*.yml) - Command doesn't already invoke
code-graph-mcp(no double-suggest) .code-graph/index.dbexists in CWD- Same command-hash not hinted within last 60s (per-command cooldown)
Exits silently otherwise — zero noise for build greps, log filters, config lookups, or the rare legitimate raw grep on indexed source.
Kill switch
CODE_GRAPH_QUIET_HOOKS=1 silences this hook (matches the rest of the hook tier).
Verification
node --test pre-grep-guide.test.js: 39/39 pass (8 fire cases + 13 skip cases + 5 regression cases lifted verbatim from 2026-05-11 session telemetry + cooldown hash + kill-switch matrix).lifecycle.test.js: 12/12 —hooks.jsonschema regression-clean after adding the new entry.incremental-index.test.js: 10/10.cargo +1.95.0 clippy --no-default-features --all-targets -D warnings: 0 findings.cargo test --no-default-features --lib: 347/347 pass.- E2E: piped JSON tool_input emits hint once on first match, silent on repeat (cooldown verified), silent on pipe-greps and under
CODE_GRAPH_QUIET_HOOKS=1. routing_benchunaffected: tool-only mode (forcedtool_choice=any); the Bash hook injects into Claude's context, not into the system prompt or tool registry.
Migration
Plugin SessionStart auto-updates the hook registration via ${CLAUDE_PLUGIN_ROOT} path indirection. No .code-graph/index.db in CWD → hook exits silently regardless. Lock manual edits with CODE_GRAPH_NO_TEMPLATE_REFRESH=1 (unaffected; this change is in plugin scripts, not in adopted-memory templates).
🤖 Generated with Claude Code
Full Changelog: v0.24.1...v0.25.0
v0.24.1 — Adoption tag specificity fix
Fixed
adopt: MEMORY.md index-line tags renamed to MCP-tool-aligned multi-word form (impact-analysis, find-references, module-overview, semantic-search, dependency-graph, trace-http-chain, http-route, find-similar-code). Previous single-word tags (impact, refs, overview, semantic, deps, trace, route, similar) collided with release-notes and commit-message prose under the claudemd §11 read-the-file hook regex (word-boundary + 0–2 char declension), producing false-positive denies on legitimate prose. callgraph, ast-search, dead-code retained — already multi-word.
Affects four index-line variants in claude-plugin/scripts/adopt.js (generic + web-* / frontend / rust-go-python-node) and the Rust drift mirror in tests/routing_bench.rs.
Migration
Existing adopted projects auto-refresh on next plugin SessionStart: needsRefresh does bytewise compare against the new desiredBlock, stripSentinelBlock cleans the old block, new block written in place. SENTINEL_BEGIN stays at v1 (bumping it without teaching stripSentinelBlock to also match prior versions would leave orphan v1 + new v2 blocks — covered by a new regression test). Lock manual edits with CODE_GRAPH_NO_TEMPLATE_REFRESH=1.
Verification
routing_benchcontext-rich (sonnet-4.5, domain=all, 3-run majority vote, 382s): Recall 41/42 = 97.6%, FP 0/10 = 0%, Overall 51/52 = 98.1% — zero regression vs v0.17.3+pm-desc-dedup baseline. Backend 22/22, Frontend 19/20 (same residual path-anchored miss as prior baselines, unrelated to this change).adopt.test.js: 66/66 pass, including new regression casestale INDEX_LINE → adopt rewrites in place without duplicating sentinel blocks.- Hook-regex stress prose: 3 OLD FP (
impact,overview,semantics) → 0 NEW FP; legitimate references still match. cargo +1.95.0 clippyclean (no-default-features + all-targets); fullcargo testsuite green (default +--no-default-features).
Full Changelog: v0.24.0...v0.24.1
v0.24.0 — Bare-name call qualifier (Rust)
Fixed
- callgraph: Rust qualified calls (
Type::method,crate::path::fn,self.method,Self::method, builder chains likeOpenOptions::new().create()) no longer route to unrelated project functions sharing the rightmost name. Eliminates phantom callers inimpact_analysisandfind_dead_codefor short-named functions (new/create/open/from). - parser:
impl crate::path::Type { ... }impl-block type now strips the leading path soqualified_nameand SelfRecv payloads match (was producingcrate::path::Type.methodqualified_names that broke same-type LIKE matching).
Verification
impact run_full_indexon this repo: 36 → 33 transitive callers; the 3 documented phantoms (decompress_with_cap,try_acquire_index_lock,from_project_root) no longer appear.routing_benchP@1: 22/22 (no regression).- 558 tests pass with default +
--no-default-features+--all-features. Clippy clean with--all-features.
Migration
Existing .code-graph/ databases keep working (qualifier-aware resolution is a no-op when edges.metadata IS NULL). Run code-graph-mcp index --rebuild to populate qualifier metadata on existing Rust files; incremental indexing picks it up automatically as files change.
Known scope
This release covers Rust only. Other languages (Go, JS/TS, Python, C#, etc.) continue with the existing same-language-fallback behavior. Per-language qualifier capture is tracked as future work — Go has the cleanest signal (explicit imports) and would benefit most.
PR: #19
What's Changed
Full Changelog: v0.23.1...v0.24.0