v0.144.0
Upgrading: nothing to do, and two things change on their own.
On a plugin install code-graph-mcp becomes a command your shell can find —
that is the whole of issue #41, and it needs no action from you. No index
change, no config change, no managed-block rewrite: the CLAUDE.md block and
the detail doc keep the ~/.cache/code-graph/bin/… fallback they have carried
since 0.141.0, because Claude Code drops a plugin's PATH entry when the plugin
path contains shell metacharacters, and older versions never added it.
The behaviour change worth knowing about is the second one: a grep with a
; or && tail is no longer denied. It runs whole, and the AST answer that
used to arrive inside the deny now arrives from the PostToolUse hook — except
when your own grep already found the symbol, where nothing is added. That is a
real reduction in steering for about 27% of what used to be denied, bought in
exchange for the command not being cancelled; the section below states the cost
and the measurement. code-graph-mcp stats readers: one follow-up rate shifts,
also described below.
To pin back: npm i -g @sdsrs/code-graph@0.143.0, or cargo install code-graph-mcp --version 0.143.0; plugin users can set the version in the
marketplace entry. Reverting restores the old deny behaviour and removes the
launcher from PATH — nothing migrates in either direction. INDEX_VERSION is
not bumped and no index needs rebuilding.
CODE_GRAPH_NO_INJECT=1 turns off the PostToolUse answer; CODE_GRAPH_NO_BLOCK_GREP=1
turns off the deny tier entirely. Neither is new.
The grep guard's red denies: one that shouldn't fire, and two that lied
Reported as three red error blocks during ordinary work. Two were the same
defect in the PreToolUse grep hook.
A compound command is no longer denied. grep -n "X" tests/foo.rs | head; echo "---"; sed -n '1190,1250p' tests/foo.rs was denied whole. The answer
covered the grep; the sed range read — the thing wanted next — was discarded,
and the deny said so in a NOTE telling you to re-issue it. The hook had detected
that tail since 0.50 and spent it entirely on the apology.
Its own rule already settled this: a grep naming two or more paths is downgraded
to a hint because a first-path-only answer is "an incomplete substitute". A
discarded ; sed is the same incompleteness. And the permission-neutral route
was already built — post-grep-inject exists for compound greps, walks every
top-level segment, and has no head-is-grep exclusion; those commands simply
never reached it because the deny fired first. The asymmetry that produced was
invisible from outside: echo x && grep Sym tests/ ran whole and got its answer
injected, while grep Sym tests/ && echo x went red and lost the echo.
Commands with a ; or && tail now produce no hook output at all. | and ||
still deny — a pipe is one pipeline the answer replaces, and the || branch
would not have run given the answer carried hits. The compound-tail NOTE is
gone with the deny it apologised for.
The "AST-aware equivalent" now is one. grep -rln "applyTierFilter\|tier:" tests/*.mjs asked for a file list from .mjs files. The deny printed
code-graph-mcp grep "applyTierFilter|tier:" tests and delivered 25+ hit lines
from all of tests/: every flag was dropped, and the glob was truncated to its
parent directory. -i -w -F -l -c now carry through, as do the three flags that select which
files are searched — grep's --include and ripgrep's --glob/-g become cg's
-g, ripgrep's -t stays -t, and every occurrence is forwarded rather than
only the first. A globbed path argument becomes the same -g filter instead of
being widened to its parent directory. The argv the child runs is the same array
the printed command is rendered from, so the two cannot disagree again. The
reported command now answers with
code-graph-mcp grep -l applyTierFilter tests -g '*.mjs'.
-F is the one flag that changes the pattern as well as the search: it means
the pattern is literal, so the BRE-to-Rust-regex unescape no longer runs
alongside it. grep -F 'x\|y' searches for x\|y, not x|y.
-c is forwarded but is not identical to grep's: cg prints path:count per
file where GNU grep -c PATTERN file prints a bare number for a single named
file. -n, -r/-R and -H are not forwarded because cg already behaves that
way; -h is deliberately not forwarded, because cg reads it as --help.
What the compound case costs you. The PostToolUse hook that now handles those
commands suppresses itself when your own grep already printed the symbol — that
gate exists because an audit found every such injection was ignored. So for a
compound grep that HITS, you get your grep's output and no AST context, where
before you got the AST answer and no grep. In this repository's own logs that is
27% of denies (23 of 85). The skip is now recorded, so the trade is measurable
rather than asserted. A compound grep that MISSES still gets the answer, and
CODE_GRAPH_NO_INJECT=1 turns that off.
If you read code-graph-mcp stats: that new record makes a grep visible to
the funnel that used to leave no trace, and the funnel scores only the
immediately-next event after an answered deny. Measured on three-line logs
(answered deny → compound grep → follow-up), total is unaffected in every case
because observe is excluded from it, and the follow-up rate depends on what
the compound grep searched for:
- it re-greps the pattern that was denied →
fallthrough_ratestays 1.0. A
verbatim re-search is fall-through however it is spelled. - it searches something else → the window moves onto it and the rate goes to
0.0, because a different query is not evidence the answer failed.
So the rate is comparable across this boundary for the first case and not for
the second.
One case is unchanged and still discards something: grep … | wc -l is denied
whole, and the answer is hits rather than a count. Pipes were never flagged as
dropped tails, so nothing regressed here — but the deny is not equivalent to
what you asked for, and that is now written down rather than implied.
Issue #41, the actual fix
Twice now this project has answered "the plugin prints commands you cannot run"
by rewriting the printed strings. 0.141.0 fixed the CLAUDE.md block and the
SessionStart announcement; 0.142.0 fixed the two printers they missed. The name
itself was never made runnable, so every one of those fixes was an apology.
Claude Code puts <plugin-root>/bin on the Bash tool's PATH for every enabled
plugin — the entry is added whether or not the directory exists. This plugin
shipped no bin/, so the entry pointed at nothing. That is the whole defect:
the door was open the entire time.
There is now a launcher at claude-plugin/bin/code-graph-mcp, so the bare name
resolves on a /plugin install with no npm i -g anywhere. Verified end to end
with only the PATH entry Claude Code already adds: --version, health-check,
the query subcommands, and adopt / unadopt / uninstall / doctor all run
under the bare name.
doctor needed a second fix to get there. It was forwarded to the binary, which
re-execs a doctor.js it looks for beside itself — fine for a source build and
for the npm package, and never true for the install this launcher exists for,
where the binary is the downloaded one in ~/.cache/code-graph/bin/ and answers
doctor.js not found. Looked in: …. It is now answered in JS, through the same
runDoctorCli the other two doctor entry points already share, so the three
cannot drift on flag parsing or exit codes.
The MCP instructions field — INSTRUCTIONS_NOISY and INSTRUCTIONS_QUIET,
injected into the system prompt of every session — was the surface neither
earlier fix touched, and it is the one that leads with
Fastest path is the CLI via Bash … `code-graph-mcp callgraph X` . It now
carries the same fallback sentence the other three surfaces have had since
0.141.0. (1083 bytes; the compile-time truncation budget is 1500.)
Two supporting changes, neither user-visible on its own:
- The npm bin entry and the plugin launcher dispatch through one module,
claude-plugin/scripts/cli-entry.js. The defect 0.142.0 shipped was two
printers of one string drifting apart; two dispatchers foradoptand
uninstallwould be that defect over a destructive path. find-binary.jsno longer accepts anything in the plugin's ownbin/as the
native binary. The launcher has to be namedcode-graph-mcpfor PATH to
find it, which is exactly what the discovery chain's basename test looks for —
both thewhichtier and the bundled-bin/tier would have handed the
launcher back as the binary, and the launcher resolves the binary by asking
that same chain.