Added
ctx_explore— delegated, deterministic repository exploration (gitlab #907).
A new multi-turn explorer — MCP tool #78 plus alean-ctx exploreCLI — that
answers "where does X live / how does Y work" in a single call instead of the
agent's usual read→grep→read loop. It seeds with BM25 lexical retrieval, expands
along a bounded graph BFS grounded in the hit set, then selects citations by
coverage, returning byte-stablepath:start-endranges (with a citation-only
mode for minimal token spend). Wired into the tool registry, the standard and
read-only tool profiles, and the heavy-index warm-need;eval_harnessgains a
SearchArm::Explore(output-token metric plus new "exploration" queries in
rust/eval/search-suite.ndjson) so A/B runs can compare explore vs hybrid vs
bm25 on recall/MRR/tokens. Output is a deterministic function of repo content
(#498).- Codex ChatGPT subscription auth now routes through the proxy (#568).
Completes the #554 fix: instead of skipping config when a Codex ChatGPT login
is detected (which left subscription users at 0 savings),install_codex_env
now writes a mode-specific config. ChatGPT subscription auth is pointed at the
proxy's Codex backend rail (model_provider = leanctx-chatgpt,openai_base_url = …/backend-api/codex,chatgpt_base_url = …/backend-apifor aux calls such as
the codex_apps streamable-HTTP MCP endpoint); API-key Codex keeps the/v1
path. The proxy gained/backend-api/codex/responses(compressed/metered via the
OpenAI Responses path tochatgpt.com) plus credential-preserving passthrough
for non-model/backend-api/*traffic. Header forwarding stays allowlist-based
both ways; a dedicated cookie store persists only Cloudflare anti-bot cookies
(cf_clearance/__cf_bm/cf_chl_*) and drops auth/session cookies; gzip/zstd
request bodies are decoded under a bounded reader (zip-bomb safe) before
compression and re-encoded. Thanks @ousatov-ua. lean-ctx doctorwarns when the MCP server is launched from a directory
without a project root (#547). When an MCP client spawns lean-ctx from an
IDE/agent config dir (.lmstudio,.claude,.codex,.codebuddy) or any
marker-less CWD, every out-of-treectx_readfails with "path escapes project
root". The newMCP server CWDdoctor check (also surfaced in the structured
health report) explains the cause and the fix (cwdin the client config, or
allow_auto_reroot/extra_roots);.lmstudiois now also treated as a
suspicious persisted root. Thanks @albinekb.- Shadow-mode CLI reads/searches now record Context IR lineage (#566).
Follow-up to #550. The MCP dispatcher records a Context IR provenance entry for
every tool call (server/call_tool.rs), but the shadow-mode hook's single-shot
lean-ctx read/grepsubprocess dropped it — soctx_proofand IR exports
were blind to compressed shadow reads.record_file_read/record_searchnow
thread the rendered-output excerpt + measured tool duration into a disk-backed
ContextIrV1load→record→save, mirroring the MCP path (same 200-char
char-boundary excerpt bound;mode/patternride the IRpatternslot; the
read'soriginal_tokensand the search's raw matched-line estimate are the IR
input so the stored compression ratio is accurate — no fabricated values). The
two remaining MCP read side effects from #550 — the in-memory loop/correction
detectors and the bounce/adaptive-threshold signals — are now delivered via
connect-only daemon routing: when an MCP daemon is already running, a
shadow-modelean-ctx read/greproutes the call through it
(/v1/tools/call→call_tool_guarded), so loop detection, correction-loop
auto-degrade, bounce tracking and adaptive thresholds all fire on the daemon's
long-lived state — full MCP parity, for free. The hook child connects only: it
reuses a live daemon but never auto-starts one (a per-call subprocess
auto-starting daemons would proliferate them, the #453 class of bug), falling
back to the enriched standalone path (disk-backed learning sinks + the Context
IR above) when no daemon is reachable or on Windows. This resolves the design
decision #566 was gated on; the connect-only invariant is documented in
daemon_client::try_daemon_tool_call_blockingand regression-guarded by
hook_connect_only_566. - PowerShell-native cmdlets route through lean-ctx (#561). Follow-up to #556:
shadow/harden mode already recognised the Windowspowershellshell tool, covering
the Unix-style PS aliases (cat/ls/rg). The command-rewrite layer now also
maps the PowerShell-native cmdlets and their short aliases —Get-Content/gc
→lean-ctx read(honoring-Path,-TotalCount/-Head/-Firstand
-Tail/-Last),Select-String/sls→lean-ctx grep(-Pattern,-Path),
andGet-ChildItem/gci→lean-ctx ls(-Path). Parameter names are matched
case-insensitively; anything with an unrecognised flag, a pipeline, multiple
operands, or an out-of-project path passes through untouched (same conservative
contract as the Unix rewrites), so determinism and redaction guarantees are
inherited. The PowerShell cmdlets are detected only in the rewrite path and are
deliberately kept out of the POSIX shell-alias surface. - Addon security hardening — trust, policy, signing, sandbox, audit (#863).
Because an addon is executable trust (a stdio addon runs code on your machine;
an http addon receives your context; its output enters the model), the
ecosystem ships with defense-in-depth across three tiers:- Trust tier + risk review. A registry-controlled
addon.verifiedflag
splits the catalog into verified (maintainer-audited) and community, shown
inaddon list/infoand the install preview.core::addons::trust::assess
statically reviews the[mcp]wiring (remote endpoint, non-HTTPS, inline
shell, fetch-and-exec, unpinned upstream, secret-bearing env) at info/warn/
danger severity. The same logic backs a registry CI validator
(registry::validate_entries, run bycargo test): unique slugs, required
provenance for installable entries, no shell/fetch/non-HTTPS/unpinned wiring,
and zero findings for verified entries. - Install policy floor —
[addons]. A global-only config block (never
merged from a project-local file):policy(open/verified_only/
allowlist/locked),allowlist,require_signature,sandbox,
block_risky.policy::gateenforces it ininstallbefore any gateway
mutation. Fully permissive by default; distribute via MDM or pin through the
signed org-policy floor. - Registry signing. A user-override registry can shadow trusted names; with
require_signature = trueit is honoured only if a sidecar
addon_registry.json.sigcarries a valid Ed25519 signature by a trusted org
key (same anchor aspolicy org trust). - Opt-in OS sandbox.
addons.sandbox = auto|strictwraps spawned stdio
servers insandbox-exec(macOS) /bwrap(Linux) at the single spawn point
— outbound-network isolation inauto, read-only fs + refuse-if-no-launcher
instrict. Off by default. - Runtime redaction + audit. Downstream tool output is run through the
shell-layer secret redaction and audit-tagged as untrusted before it reaches
the model (runtime::scrub_output).
New small, unit-tested modulescore::addons::{trust,policy,signing,sandbox, runtime}; binding registry-review checklist inCONTRIBUTING.md.
- Trust tier + risk review. A registry-controlled
Changed
- Leaderboard — no top-50 cap, real pagination, everyone findable. The
community leaderboard previously truncated to the top 50 accounts, so most
contributors never appeared and the headline community energy could silently
drop when the cut-off shifted.GET /api/leaderboardnow paginates
(?page,?per_page, default 50 / max 200) and supports case-insensitive
name search (?q=), while two new fields —total_tokens_savedand
total_cost_avoided_usd— report the uncapped community totals across all
opted-in accounts, independent of the displayed page or any filter. The
server-rendered/leaderboardpage and the website/metricspage gained
matching search + pagination controls; the landing-page hero energy stat and
the in-app cockpit now read the uncapped totals so headline numbers stay
stable. Global ranks are preserved across pages. Pagination, ranking, totals
and search are pure, unit-tested functions (paginate,all_ranked_cards).
(gitlab #868–#871)
Fixed
ctx_impactdropped C# same-namespace blast radius after the first reindex
(#398). A C# class used within its own namespace (nousing, DI-injected)
reappeared as a leaf node after the first background reindex. The private
ctx_impactbuilder wrote precisetype_refedges into thePropertyGraph, but
everyProjectIndex::save()mirrorsgraph_indexover the graph via
clear_code_graph()— andgraph_indexemitted no type-usage edges, so the
reindex silently wiped the blast radius (a dual-writer bug). A new
core::type_ref_edgesmodule is now the single source of truth for C#/Java
consumer→definer file resolution (namespace-aware, failsafe-capped), shared by
both the durablegraph_indexmirror and thectx_impactbuilder;graph_index
now emits these precise edges instead of the old coarse alphabetical
namespace-chain heuristic, so a reindex reproduces the blast radius instead of
dropping it.GRAPH_ENGINE_VERSIONis bumped (2→3) so stale graphs self-heal on
the next query, and the regression tests now run through the index mirror — the
exact gap every prior #398 fix missed. (The grep hook also now redirects only
output_mode=content, passingfiles_with_matches/countthrough untouched,
since the path-swap returned wrong results for those.) (gitlab #915–#918)ctx_readleft an empty[]metadata field on incompressible files (#509).
Theentropy(anddensity) read modes append a[techniques…]tag listing
which compression techniques fired. On a file where none did (high-entropy, no
duplicate blocks) the technique list is empty, so the header rendered a bare
H̄=4.2 []— the same empty-trailing-field waste fixed for
ctx_semantic_search's(rrf: X, )in #511. Atechniques_taghelper now omits
the bracket segment entirely when the list is empty (and keeps the leading space- Pi
AGENTS.mdadvertised renamed tools that no longer exist (#548). The Pi
installer writes a curated statictemplates/PI_AGENTS.md, and its tool-mapping
table still listedctx_grep/ctx_find/ctx_ls— tools renamed long ago to
ctx_search/ctx_glob/ctx_tree. Pi agents that followed the table issued
unknown-tool calls. The template (and the matchingpi.rssetup hint) now use the
canonical names, and a new parity test (tests/rules_template_tool_names.rs) ties
every shipped agent template to the live MCP registry: anyctx_*reference
that is not a registered tool fails the build, so a future rename can never drift
silently again. (First slice of the #548 agent-rules unification — marker/dedup
consolidation, content-aware freshness, andrules.toml↔syncsemantics follow.) - Rule injection skipped content/compression changes when the version was unchanged (#548).
The injector's freshness check was version-only: it compared the on-disk
<!-- version: N -->againstRULES_VERSIONand skipped the rewrite when they
matched. So a change that alters the rendered body without bumping the version —
togglingshadow_mode, switchingcompression_level, or editing a canonical
section between releases — left every agent's rules block stale until the next
version bump.RulesFile::block_matches_rendernow compares the on-disk block
byte-for-byte (whitespace-insensitive) against a fresh render for the active
parameters, and the skip path requires bothis_current()and that content
match. Re-runningsync/inject after a compression-level change now regenerates
the block as expected; an unchanged config stays idempotent. (Second slice of the
#548 agent-rules unification, after the Pi-template parity guard.) rules diff/sync↔.lean-ctx/rules.tomlsemantics, and arules diff
false-positive (#548). Two coupled fixes for the rules-governance commands:sync/diffdo not consumerules.toml— now documented and decoupled.
rules sync/diffregenerate from the canonicalrules_canonicalsource of
truth (preserving user text around the markers) and never readrules.toml,
which is the input forrules lintplus a user-editable inventory fromrules init. This is now stated in theruleshelp, theinitnext-steps, and the
RulesConfig/syncdocs.detect_driftno longer loadsRulesConfigat all,
sorules diffworks without first runningrules init(it previously
failed with "No rules config found") — the dead_configparameter is gone and
the command is infallible.rules diffreported phantom drift after every sync. Drift picked the
shared-vs-dedicated expected block from a content heuristic ("up_to_date and no
'existing user rules'"), which misread freshly synced shared files with no
user text (Copilot CLI, Codex CLI, Gemini/OpenCode in shared mode) as the
dedicated layout and flagged them asDRIFTEDon every run. Drift now compares
each target against the canonical block for its realRulesFormatvia the
newrules_inject::expected_blocks_by_target, keepingsyncanddiffin
agreement. Covered by new tests:detect_drift_without_rules_toml_does_not_ require_initandsync_then_diff_reports_no_drift. (Third slice of the #548
agent-rules unification.)
- Compression block had two disagreeing marker models, so cross-channel dedup
never fired (#548).rules_canonical::renderembedded the output-style
compression prompt inline inside the<!-- lean-ctx-rules -->block with no
delimiters, but the coverage/dedup readers (rules_channel,rules dedup)
detect the payload by a separate<!-- lean-ctx-compression -->…<!-- /lean-ctx-compression -->block. Since the writer never emitted those markers,
cursor_compression_covered/client_autoloads_compressionwere always false on
freshly written rule files — so the MCP per-session instructions kept repeating
the compression block even for Cursor/Codex that already load it from their rule
file (double billing), andrules dedupcould not thin a render-produced shared
AGENTS.md. The two models are now one: theCOMPRESSION_BLOCK_*markers live in
rules_canonical(single marker source, re-exported fromrules_channel), and
renderwraps the compression prompt in them for the persistent carriers
(Dedicated/Shared— every injected rule file). The ephemeralBareMCP
channel stays unmarked by design (its inclusion is governed by carrier coverage,
so a per-session marker would be noise). Content-aware freshness (second slice)
re-propagates the new block on the nextsyncwithout a version bump. Covered by
new tests asserting carriers wrap /Baredoes not /Offemits nothing, and an
end-to-end check that arender-produced Cursor block is now recognised as
compression coverage. (Fourth slice of the #548 agent-rules unification — closes
the "one canonical carrier/marker model" acceptance criterion.) - Shadow-mode hook reads dropped ~75% of the MCP read side effects (#550). When
shadow/harden mode intercepts a nativeview/grepcall it spawnslean-ctx read
as a single-shot subprocess. That CLI path recorded only a fraction of what the MCP
ctx_readpipeline does, and — crucially — never flushed its buffered telemetry
before the process exited, solean-ctx heatmapstayed empty andlean-ctx gain
reported nothing for compressed reads. Three fixes:- One flush set, no drift. A new
tool_lifecycle::flush_all()is the single
source of truth for the buffered-telemetry flush (stats, heatmap, path-mode
memory, auto-mode resolver, edit-quality, mode predictor, feedback, threshold
learning, LiTM calibration). The daemon shutdown, the parent watchdog and every
CLI tool arm (read/grep/ls/find/deps/diff/-c/-t) now call it — the
hand-rolled per-arm copies had drifted (thereadarm flushed onlystats), which
is exactly how the gap went unnoticed. - CLI read learning parity.
record_file_read/record_searchnow run the same
disk-backed learning sinks the MCP background thread does — mode-predictor training,
the per-language compression feedback outcome, and the per-call anomaly metric — so
auto-mode selection, the feedback loop and dashboard signals improve from
shadow-mode reads too (not just direct MCP calls). - Mode predictor actually persists now.
ModePredictorstored its history in a
struct-keyedHashMap<FileSignature, _>, whichserde_jsoncannot serialize
("key must be a string") — somode_stats.jsonwas never written and the
predictor relearned from zero every process. The history now serializes as an entry
list (round-trip tested). The in-memory-only loop/correction detectors and the
bounce/adaptive signals that need routing throughctx_read::handleare tracked as
a follow-up (they cannot be honored from a single-shot subprocess without
cross-process state).
- One flush set, no drift. A new
- Windows PowerShell profile path hardcoded to
~\Documents— broke under OneDrive
redirection (#558).proxy enableand the shell-hook install resolved the
PowerShell profile by hardcodinghome\Documents\PowerShell\…. Windows OneDrive
folder backup (on by default on most installs) redirects Documents to e.g.
…\OneDrive\Documents\…, so lean-ctx wrote to a file PowerShell never reads — the
active$PROFILEwas never updated and the proxy received no traffic in new
terminals. A newresolve_powershell_profile_pathasks PowerShell itself for
$PROFILE.CurrentUserCurrentHost(authoritative under any folder redirection,
preferringpwshthen Windows PowerShell, UTF-8 output) and falls back to the
documented default only when no PowerShell host can be launched. Non-Windows hosts
keep the static~/.config/powershellpath and never spawn a process (#356). - Copilot CLI
view(read) andrg(search) tool calls passed through uncompressed (#562).
handle_redirectdispatched on the tool name but only matchedRead/read/
read_fileandGrep/grep/search/ripgrep, so two documented GitHub Copilot
CLI tool names —view(its read tool) andrg(its search alias) — slipped
through without compression in shadow/harden mode. The dispatch is now a tested
classify_redirecthelper that includesview(→ read) andrg(→ grep); the
Claude/Cursor/CodeBuddy matchers are unchanged because those hosts never emit
those names and Copilot CLI fires the hook for every tool call. - Copilot/VS Code Claude models ignored lean-ctx — no
.github/copilot-instructions.md(#555).
lean-ctx init --agent copilotinstalled the MCP server plus a deliberately
weakAGENTS.mdpointer but never wrote.github/copilot-instructions.md, the
repo-level file VS Code Copilot Chat auto-applies to every request. Claude-
family models (Sonnet/Opus) therefore ignored the tool mapping while GPT-5.x
followed it ~95% of the time.initnow writes the strong dedicated ruleset
into.github/copilot-instructions.mdas an idempotent<!-- lean-ctx-rules -->
block (user content is preserved, never clobbered) and pins
github.copilot.chat.codeGeneration.useInstructionFiles: truein the project
.vscode/settings.jsonas a safety net (an explicit user value is honoured);
uninstall removes the block. - Shadow mode ignored
globand Windowspowershelltool calls (#556).
Shadow/harden mode silently passed two documented Copilot CLI tools straight
through: theglobtool ("find files matching patterns") had no arm in the
redirect hook, and thepowershellshell tool (paired withbashon Windows)
was not recognised as a shell, so command rewrites never fired there.
handle_redirectnow interceptsGlob/glob— warming the sharedctx_glob
core via a newlean-ctx globsubcommand and recording the intercept in
shadow.log, then letting the native path-list result through —is_shell_tool
(now shared by both hook entry points) coversPowerShell/powershell/pwsh,
and the Claude/Cursor/CodeBuddy redirect matchers includeGlobso the hook
fires for it. Copilot CLI already dispatches every tool, so itsglob/
powershellcalls are covered automatically. - Codex proxy never compressed — ChatGPT login bypasses it; the API-key config
was a no-op (#554).lean-ctx proxy enablereported success for Codex yet
Requests/Compressed/Tokens savedstayed at0, for two reasons. (1) A Codex
ChatGPT login (the default) authenticates via OAuth directly against
chatgpt.com/backend-api, so a customopenai_base_urlis ignored and the
proxy never sees the traffic — the Claude Pro/Max situation, but with no
warning. lean-ctx now detects a ChatGPT login (~/.codex/auth.json
auth_mode = "chatgpt", overridable by an explicitOPENAI_API_KEY) and
prints an honest skip notice pointing at the MCP tools instead of writing dead
config. (2) In API-key mode lean-ctx wrote[env] OPENAI_BASE_URLinto
~/.codex/config.toml, which Codex does not read; it now writes the documented
top-levelopenai_base_urlkey (openai/codex#12031), migrates the dead legacy
entry, and preserves any custom remote endpoint. Uninstall/cleanup/preview
handle both forms. lean-ctx index build-semanticcold-starts the embedding model again
(#545). On a machine without the model cached, the build dead-ended with
"embedding model not downloaded — auto-download … failed" even though no
download was ever attempted: the build path checkedis_available()(a pure
file-existence check) and bailed before the download could run — a regression
from the #519 ORT-teardown guard.build_or_updatenow downloads the model
first via a newEmbeddingEngine::ensure_downloaded()(pure network/file IO,
no ORT init) and only loads the ONNX Runtime once the files are present, so the
cold bootstrap works again and the #519 teardown safety is preserved. The
passive search path is unchanged.- Copilot CLI hooks silently no-opped — wrong payload field names and missing
modifiedArgs(#551). Copilot CLI sends camelCasetoolName+toolArgs
(a JSON-encoded string), but the rewrite/redirect/observe handlers only read
snake_casetool_name/tool_input/command, so every Copilot tool call
passed through unchanged; even once parsed, thepreToolUseoutput omitted
Copilot'smodifiedArgsfield, so rewrites/redirects would never have taken
effect. A newhook_handlers::payloadresolves the tool name, args (a
tool_inputobject, atoolArgsobject, or atoolArgsJSON-string) and
command across all handlers,observegains a CopilotpostToolUsebranch so
its telemetry is recorded instead of dropped, and the hook now emits Copilot's
documentedpermissionDecision+modifiedArgscontract alongside Claude's
hookSpecificOutput.updatedInputand Cursor'supdated_inputin a single
response. Snake-case (Claude/Cursor) stays regression-tested. Thanks for the
detailed report. CLAUDE.md/CODEBUDDY.mdpointer block duplicated on everysetup/doctor --fix(#549). The block-detection constants pointed at the wrong marker:
*_MD_BLOCK_START/ENDreferenced the canonical rules marker
(<!-- lean-ctx-rules -->) while the installer writes the AGENTS pointer block
(<!-- lean-ctx -->), soexisting.contains(START)was always false — the
doctor reported the block as missing and every run appended a fresh copy,
accumulating duplicates. The constants now point atAGENTS_BLOCK_START/END
(one fix for both the doctor false-negative and the duplication), a new
remove_all_blocks()collapses any already-accumulated duplicates back to a
single canonical block in the installers andstrip_*_md_block, and the doctor
fixtures seed the real pointer marker.
Upgrade
lean-ctx update # recommended (auto-downloads + refreshes shell hooks)
cargo install lean-ctx # or
npm update -g lean-ctx-bin # or
brew upgrade lean-ctxNote: After upgrading via cargo/npm/brew, run
lean-ctx setupto refresh shell aliases.lean-ctx updatedoes this automatically.
Full Changelog: v3.8.12...v3.8.13