Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Releases: sdsrss/code-graph-mcp

v0.31.0 — Multi-account isolation: honor CLAUDE_CONFIG_DIR

18 May 16:33

Choose a tag to compare

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.json writes 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.js exposes
    claudeHome() (returns process.env.CLAUDE_CONFIG_DIR || ~/.claude,
    re-read on every call).
  • lifecycle.js, auto-update.js, doctor.js, session-init.js,
    adopt.js now route all ~/.claude/... paths through the helper.
  • adopt.js: memoryDir() keeps its (cwd, home) signature for back-compat;
    CLAUDE_CONFIG_DIR simply overrides the home + .claude join.
  • adopt.js: isPluginModeInstall() matches both the legacy
    ~/.claude/plugins/ marker and CLAUDE_CONFIG_DIR/plugins/.

Tests

  • New claude-config.test.js: env-var resolution + empty-string fallback.
  • adopt.test.js: memoryDir + isPluginModeInstall honor the override.
  • lifecycle.e2e.test.js: full install subprocess writes into
    CLAUDE_CONFIG_DIR and 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

15 May 15:54

Choose a tag to compare

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-index now reports file deletions ("N files updated, M files removed").
  • deps <file> distinguishes missing-file from no-imports; surfaces unresolved deps.
  • callgraph no longer prints no-op Resolved 'X' → 'X'.
  • incremental-index in non-git dir explains why it skipped (only in non-quiet mode).
  • map on empty project prints a friendly message instead of dangling "Modules:".

health-check / doctor contract

  • health-check --json emits valid JSON {healthy:false, reason:"no_index", ...} + exit 0 when no index exists.
  • doctor routes "No index found" to the index-empty fix path (auto-runs incremental-index).

MCP empty-string args

  • get_call_graph / find_references / get_ast_node / find_similar_code reject empty/whitespace symbol_name.
  • get_call_graph / find_references treat empty file_path as absent.
  • trace_http_chain(route_path="") and dependency_graph(file_path="") reject upfront.

Path & filesystem hardening

  • module_overview rejects absolute paths, ../ traversal, and Windows drive letters before hitting the DB.
  • snapshot create --out pre-flights target path (dir-as-out, missing-parent) instead of leaking SQLite VACUUM INTO errors.
  • scan_directory tolerates per-entry walk errors — chmod 000 subdir no longer aborts the whole rebuild.

Quality

  • 526 tests pass.
  • cargo +1.95.0 clippy --all-targets -- -D warnings clean 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)

13 May 19:52

Choose a tag to compare

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_type no 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::Path keeps same-file targets per the spec ("same-file matches take precedence")
  • path_filter_candidates now 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: invalid type filter now errors (was silent empty result + exit 0)
  • find_references: invalid relation now errors (was silent fall-back to "all")
  • get_call_graph: symbol_name + route_path conflict 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 inspect rejects truncated SQLite (was returning fake-empty meta success)
  • Concurrent incremental-index gets 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

13 May 17:59

Choose a tag to compare

Full Changelog: v0.27.0...v0.28.0

v0.27.0 — Python call relations + dead-code truncation guard

10 May 22:56

Choose a tag to compare

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_overview JSON empty path no longer smears anyhow Error: on stderr; . normalizes to project root (matching MCP tool_module_overview).
  • find_dead_code truncation guard: callback args in the cut-off tail of long functions (env CODE_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::install best-effort git invocations redirect stderr to /dev/null; fatal: not a git repository no longer leaks into cargo test output.

Removed:

  • LanguageConfig.call_node_kind field — 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_call
  • src/storage/queries/dead_code.rs::tests::test_find_dead_code_skips_when_caller_content_truncated
  • tests/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 warnings clean 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

10 May 22:01

Choose a tag to compare

v0.26.0 — UserPromptSubmit context push default ON + trigger hints

Changed

  • claude-plugin/scripts/user-prompt-context.js: computeQuietHooks default
    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 in pre-grep-guide.js's 15-day baseline:
    429 raw grep vs 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 + shouldSkip filter keep confirmation
    chatter silent. Escape hatch: CODE_GRAPH_QUIET_HOOKS=1.
  • claude-plugin/scripts/pre-edit-guide.js: caller threshold lowered from
    directCallers < 2 to < 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_map injection (session-init.js) stays default
    OFF
    — that hook is a static dump duplicated by MEMORY.md's decision
    table; this hook is a reactive trigger reminder. The two defaults are
    intentionally asymmetric.

Added

  • src/mcp/server/mod.rs MCP instructions field 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.md is loaded every session, higher priority than the
    invited-memory path in MEMORY.md.
  • claude-plugin/templates/plugin_code_graph_mcp.md clarifies the asymmetric
    hook defaults and lists CODE_GRAPH_QUIET_HOOKS=1 as the context-push
    escape hatch alongside the existing VERBOSE_HOOKS / QUIET_HOOKS=0 flags.

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-time assert!(len <= 1500) on
    NOISY instructions string holds; final length ~772 bytes).
  • node --test claude-plugin/scripts/user-prompt-context.test.js:
    77/77 pass — six computeQuietHooks priority-chain cases rewritten for
    the default-noisy invariant; one e2e check kept on the =1 escape hatch.
  • No change to routing_bench.rs corpus; 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=1 in ~/.claude/settings.json env to opt out.
  • Adopted projects: the plugin_code_graph_mcp.md template auto-refreshes on
    next SessionStart (unless CODE_GRAPH_NO_TEMPLATE_REFRESH=1 is 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

10 May 21:32

Choose a tag to compare

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 --version via existing readBinaryVersion
  • Compares to package.json version via existing compareVersions
  • Stale → callers clearCache() + fall through to the rest of the discovery chain
  • Permissive on unknown values (missing pkg version, unreadable binary --version output) → 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.28 vs pkg 0.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
  • 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

10 May 21:26

Choose a tag to compare

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-mcp while 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:

  1. Command HEAD is grep/rg/ag (NOT piped — cargo test | grep FAILED is an output filter, not a search)
  2. Args include an indexed source-tree path (src/ tests/ lib/ scripts/ claude-plugin/ tools/ pkg/ cmd/ internal/ app/ components/ server/ client/ crates/ packages/)
  3. Not searching only a config/lockfile (Cargo.toml .gitignore *.md *.json *.yml)
  4. Command doesn't already invoke code-graph-mcp (no double-suggest)
  5. .code-graph/index.db exists in CWD
  6. 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.json schema 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_bench unaffected: tool-only mode (forced tool_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

10 May 20:40

Choose a tag to compare

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_bench context-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 case stale 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 clippy clean (no-default-features + all-targets); full cargo test suite green (default + --no-default-features).

Full Changelog: v0.24.0...v0.24.1

v0.24.0 — Bare-name call qualifier (Rust)

10 May 20:02
6d918e7

Choose a tag to compare

Fixed

  • callgraph: Rust qualified calls (Type::method, crate::path::fn, self.method, Self::method, builder chains like OpenOptions::new().create()) no longer route to unrelated project functions sharing the rightmost name. Eliminates phantom callers in impact_analysis and find_dead_code for short-named functions (new/create/open/from).
  • parser: impl crate::path::Type { ... } impl-block type now strips the leading path so qualified_name and SelfRecv payloads match (was producing crate::path::Type.method qualified_names that broke same-type LIKE matching).

Verification

  • impact run_full_index on this repo: 36 → 33 transitive callers; the 3 documented phantoms (decompress_with_cap, try_acquire_index_lock, from_project_root) no longer appear.
  • routing_bench P@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

  • v0.24.0 fix(callgraph): Rust qualifier-aware resolver eliminates phantom callers by @sdsrss in #19

Full Changelog: v0.23.1...v0.24.0