Added
lean-ctx update <version>pins a specific release (#447) —updatenow
takes an optional version (lean-ctx update 3.8.5,v-prefix optional) and
installs that exact tagged GitHub release instead of the latest, so you can
roll back or A/B an older build. It reuses the normal update path —
SHA256-verified download, atomic binary swap,post_update_rewire— so the
same checksum guarantee applies and no data, config or logs are touched
(only the binary is swapped; downgrades read your existing data as-is).
Invalid versions are rejected before any network call;--checkreports
whether the pinned version differs. The auto-update scheduler is unchanged
(still tracks latest).- R2 benchmark faithful-arm preflight (#361) —
bench/agent-task/r2/preflight.mjs
proves, before any priced run, that the pi arm routes shell throughctx_shell
(nativebashsuppressed) and actually compresses it — the "green preflight =
running as designed" gate the tokbench reviewer asked for, ruling out R1's
102 native bash / 0 ctx_shell. The shell-suppression decision is now the
single, unit-tested invariantresolveSuppressedBuiltins
(packages/pi-lean-ctx), so the routing fix can never silently regress. - Proxy accepts a trusted non-loopback HTTP upstream behind an opt-in (#440) —
Codex and other clients that sit in front of the proxy need to point it at an
upstream likehttp://host.docker.internal:2455, butvalidate_upstream_url
rejected every non-loopbackhttp://URL with a misleading "must use HTTPS"
error and no escape hatch. A trusted plaintext upstream is now allowed via
LEAN_CTX_ALLOW_INSECURE_HTTP_UPSTREAM=1or
[proxy] allow_insecure_http_upstream = true; the startup banner anddoctor
flag the plaintext hop so it stays a conscious choice. Documented end-to-end in
docs/reference/05-advanced.md, including thesupports_websockets = false
Codex HTTP/SSE setup as an alternative to the native WebSocket transport below. - Native WebSocket
/responsestransport for Codex (#440) — Codex CLI and the
OpenAI SDK default to a persistent WebSocket connection (ws://…/responses,
oneresponse.createevent per turn), so the HTTP-only proxy forced clients to
setsupports_websockets = false. The proxy now speaks the Responses WebSocket
protocol natively:GET /responses(and/v1/responses) upgrades to a
WebSocket, eachresponse.createturn is bridged to the configured HTTP/SSE
upstream with lean-ctx's tool-output compression applied, and every upstream
SSE event is relayed back verbatim as a WebSocket frame. Method routing keeps
POSTon the HTTP/SSE forwarder, so both transports share one upstream, auth
path and compression logic (proxy::openai_responses_ws). Codex works as a
drop-in now without disabling WebSockets.
Changed
- Rust crate migrated to edition 2024 (#438) —
cargo fix --editionplus
manual fixes for#[cfg(windows)]FFI (unsafe extern "system") and
feature-gated pathscargo fixcannot reach on a single host. Newly-unsafe
std::env::set_var/remove_varcalls are fully documented: the 13 production
sites carry exact// SAFETY:justifications (all single-threaded CLI/startup
paths), while the ~390 test sites route through one audited helper,
crate::test_env, instead of repeating the same comment at every call. Profile
switching no longer mutates the environment from the multi-threaded MCP server —
set_active_profilerecords the active profile in a thread-safe in-process cell,
removing a latentenv::set_vardata race. Nestedif/if letcollapsed to
edition-2024 let-chains tree-wide. No behavioural change. Thanks @dasTholo for
the original migration PR (#438). - OpenCode plugin no longer double-registers the built-in overrides (#441) —
the plugin exposedctx_read/ctx_search/ctx_glob/ctx_edit/ctx_shell
both as static replacements of the nativeread/grep/glob/edit/bash
tools and again under theirctx_*names via dynamic MCP registration, so the
model saw two copies of each and paid for the duplicate schemas. The five
already-overridden tools are now filtered out of the dynamic set; every other
ctx_*tool is still registered dynamically. Thanks @omar-mohamed-khallaf. - Default shell allowlist now includes the C/C++ compilers (#361) — under
mode=replace,ctx_shellenforces the allowlist, butgcc/cc/clang/
g++/c++/clang++were missing even thoughrustc/go/javacwere, so a
coding agent could not compile an ad-hoc reproducer (gcc repro.c) without an
explicit opt-in (reported by the tokbench review, which set
LEAN_CTX_ALLOWLIST_WARN_ONLY=1to work around it). They are compile-only —
executing the produced binary stays gated like any other path — so the security
boundary is unchanged.
Fixed
gaindashboard shows the per-day lean-ctx version again (#307) — the
"richer theme rendering" pass replaced the per-day version column in the
RECENT DAYS section with a gradient bar, solean-ctx gainandgain --deep
silently stopped attributing each day's compression rate to a release
(regressing the feature added in v3.7.1). The version is still recorded on
every day's stats and thegain --dailytable still showed it — only the
dashboard renderer dropped it. The bar is kept (now padded to a fixed width so
the column lines up) and the version is re-appended (v{x.y.z},—for
pre-tracking days).- Secret redaction stops corrupting type annotations and drops its duplicate rules (#430) —
ctx_editcarried a second copy of the redaction regex set that never got the
non-secret-literal guard added for #430; worse, its generic-long-secret branch
kept the matched value before the[REDACTED]tag, so a real key could leak into
diff evidence. Diff masking now goes through the singlecore::redactionsource
of truth. That guard is also widened: right-hand sides that are type expressions —
password: Promise<string>,apiKey: Record<string, unknown>,token: string[]—
are recognized as non-secrets (real keys never contain<>|()[]{}), so reading
TypeScript throughctx_readno longer maskspassword: undefined-style
annotations as API keys. ctx_readexposes the same schema in Pi as in Codex / MCP (#432) — the Pi
adapter hand-wrote actx_readschema with onlypath/offset/limit/
mode, so an agent running in Pi never sawfreshorstart_lineeven though
the canonical MCP schema (and the Pi handler internally) already supported them —
making cross-harness instructions likectx_read(mode="full", fresh=true)look
invalid in Pi only. The Pi schema now matches the registry:start_line(with
offsetkept as a back-compat alias) andfreshare exposed and wired through
both the MCP-bridge and CLI read paths.proxy enablenow also routes Pi / forge through the proxy (#361) — Pi and
forge resolve their endpoint from~/.pi/agent/models.json
(providers.<name>.baseUrl) + OAuth, not fromANTHROPIC_BASE_URL/
OPENAI_BASE_URL, so the shell and Claude/Codex env wiring silently bypassed
them (the tokbench review had to hand-editmodels.json).proxy enable/
disablenow wire Pi'santhropic(bare origin) andopenai(/v1-suffixed)
providers when~/.pi/agentexists, preserving any custom remote endpoint
unless--forceand reverting only the endpoints it set. Pi's OAuth keeps
working because the proxy forwards the credential verbatim to the real upstream.config init --fullno longer resets the existing config to defaults (#443) —
the command rebuilt the file fromConfig::default()and saved that over the
user'sconfig.toml. Because the TOML merge writes every default value, this
silently reverted custom settings (proxy port, compression level, provider
setup, …) on everyinit --full. The command now loads the existing config and
re-serializes that (falling back to defaults only when no file exists),
preserving user values while still materializing the fully-commented template;
an unparseable file aborts with a clear message instead of being overwritten.- OpenCode (and 18 other agents) now get the
ctx_*usage rules injected (#442) —
rule injection was gated onrules_already_present(), a hand-maintained list
that only knew about five agents. For everyone else it returnedfalse, so with
auto_inject_rulesunset the setup skipped injection and the model never saw
the "preferctx_*tools" guidance — defeating the whole point of MCP-only
mode. Detection is now derived from the singlebuild_rules_targetscatalog
(rules_inject::any_rules_marker_present), so every supported agent is covered
and can never drift from the writer again. The OpenCode hook additionally
injects the rules intoAGENTS.mdwhen running MCP-only (shadow mode off) and
MCP is registered, so the guidance lands even without the interception plugin. - Impact graph self-heals after an upgrade so C# same-namespace edges apply (#398) —
the v3.8.3 fix addedtype_refedges for C#/Java types consumed without a
using/import (same-namespace/package visibility), but those edges only exist
in a freshly built graph.ctx_impactrebuilt the property graph only when it
was completely empty, so after upgrading, an existing graph (built before the
edges existed) was served unchanged — leaving the consumed class a
false-negative leaf that reported "no impact". The property graph now records
the engine generation that produced it (engine_version+built_within
graph.meta.json), andctx_impact analyze/diff/chaindetect a graph
built by an older engine and transparently rebuild it once before querying.
Combined with the XDG resolver fixes (#436/#439) — which keep the graph and
config.tomlin a single stable location — a stale or misplaced graph can no
longer mask the real blast radius. Thanks @nigeldun. - Direct writers stop re-creating
~/.lean-ctxafter migration (#439) — the
resolver fix (#436) flips the data tree to XDG, but several feature-specific
writers still hard-coded~/.lean-ctxand re-created it post-split regardless
of where the resolver pointed: multi-agentshared_knowledge.json
(core::agents), Jira OAuth credentials (core::providers::jira_oauth), the
personal-cloud cache/knowledge readers (cloud_client/cloud_sync), the
LaunchAgent proxy logs and scheduled-update logs (proxy_autostart/
update_scheduler), the A2A task store (core::a2a::task), the cloud
mode_statsreader (cli::cloud) and the ctxpkg publisher signing key
(core::context_package::keys). All now route through the typed
data_dir()/state_dir()resolvers — the same categoriesdoctor --fix
migrates them to — so a post-migration session reads and writes the XDG dirs,
while legacy single-dir installs still resolve in place. The source-level
legacy-path firewall (rust/tests) was tightened to catch both the multi-line
dirs::home_dir()…join(".lean-ctx")chains and thejoin(".lean-ctx/…")
subpath form it previously missed, so the tracked-debt allowlist can only shrink. doctorshows~instead of the absolute home path (#437) — dozens of
checks printed the full/Users/<name>/…(or/home/<name>/…) path, leaking
the username and adding noise. Two chokepoint helpers indoctor/common.rs
(tildify_homefor formatted lines,display_user_pathfor raw paths, with
component-boundary safety so a sibling like…/<name>-backupis never mangled)
collapse the home dir back to~at the central output sinks, sodoctorand
doctor integrationsno longer print an absolute home path.- Data dir no longer re-adopts a marker-free
~/.lean-ctx(#436) — the data
resolver returned the legacy~/.lean-ctxwhenever that directory merely
existed, even afterdoctor --fixhad moved every data marker to the XDG
dirs. Config/state/cache had already flipped to$XDG_*in that case, so data
silently diverged from its siblings and editor sessions kept writing
active_transcript.json/context_radar.jsonlback into~/.lean-ctx. The
legacy/mixed decision now lives in a single source of truth
(paths::single_dir_override): a legacy dir wins only while it still holds data
markers, so once split, data flips to$XDG_DATA_HOME/lean-ctxlike the rest.
A cross-category contract test plus a source-level legacy-path firewall
(rust/tests) lock the invariant in so it can never silently regress. doctor --fixnow empties a residual~/.lean-ctx(#434) — after the data
moved to XDG, leftover reports (doctor/,setup/,status/) and the empty
directory lingered, so the next run re-detected the old location and the fix
report itself was written back into the legacy dir.--fixnow drains any
remaining non-runtime entries into the typed XDG dirs and removes the empty
directory (xdg_migrate::reclaim_legacy), and the report lands in XDG.doctorreports the realconfig.tomllocation after a split (#435) — the
config.tomlcheck and the path-jail hint were hardcoded to~/.lean-ctx, so
after the XDG splitdoctorpointed users at a stale path. Both now resolve
throughConfig::path()/config_dir()and show where the file actually lives.doctorscore matches the checks it prints (#433) —passed/totalwere
two hand-maintained counters that drifted: rendered ✗ checks ("XDG layout",
"data dir split") were shown but never counted, so the summary overstated
health. Every check now flows through one accumulator that counts exactly what
it renders; advisory lines (LSP, providers, MCP bridges) are rendered but
explicitly excluded from the score, so display and tally can no longer diverge.- Secret redaction no longer mangles source files read via
ctx_read(#430) —
the key/value secret pattern matched TypeScript type annotations and language
literals such aspassword: undefined,secret: stringandtoken: null,
replacing the value with[REDACTED:API key param]and corrupting files read
throughctx_read. The redactor now skips a denylist of obvious non-secret
literals (undefined/null/none/true/false/string/number/boolean/…). The same
pass fixed two latent under-redaction bugs: AWS keys and generic long
secrets were annotated in place (the secret kept,[REDACTED]merely
appended) instead of removed. The shell tee redactor and thectx_read
redactor now share one implementation (core::redaction), so the two layers
can never drift apart again. - Dashboard tool profile "Lean" no longer reverts to "Power" (#431) —
selecting Lean persistedtool_profile = "lean", but the config loader didn't
recognise it (loggingUnknown tool_profile 'lean'and falling back to Power)
and the settings API reported the effective profile (Power) rather than the
unpinned state.lean/lazy/resetare now understood everywhere as the
unpin sentinel (centralised intool_profiles::is_unpinned_alias), the loader
self-heals silently, and the dashboard reports — and round-trips — Lean (the
toggle is labelled "Lean (default)"). - Dashboard settings page no longer times out on load (#431) — route
handlers ran synchronously on the small async worker pool, so one slow
endpoint (e.g. a graph/index build) could starve a trivialGET /api/settings
for minutes on few-core machines. Handlers now run on the blocking thread
pool, keeping light endpoints responsive, and any handler crossing 1s is
logged for diagnosis. ctx_readacceptsoffset/limitaliases (#432) — agents trained on the
native Read tool sendoffset/limit, but the schema only documented
start_line, so those range reads were silently ignored.offsetis now an
alias forstart_lineandlimitbounds the window (lines:N-M); the aliases
are advertised in the tool schema and the generated manifest/reference docs,
withPI_AGENTS.mdaligned.- macOS "access your Documents" prompt eliminated structurally (#356) — the
daemon, proxy and auto-updater run as LaunchAgents (their own TCC identity,
ppid 1), so any access they make under~/Documents,~/Desktopor
~/Downloadspops the privacy prompt in lean-ctx's name — and because every
release re-signs the binary, the grant is voided on each update, re-prompting
forever. The earlier opt-out path guards (v3.8.0–v3.8.7) were per-call-site
and fragile, and the stable code-signing identity only made one "Allow"
stick — neither satisfied users who refuse Documents access outright. The
three LaunchAgents are now wrapped insandbox-execwith a minimal Seatbelt
profile (allow default;deny file-read*/file-write*under the three
protected home dirs —rust/src/core/tcc_guard_sandbox.rs), so the kernel
refuses any such access silently withEPERM: TCC is never consulted and the
prompt can no longer appear, with no "Allow" required. Everything else stays
permitted, so functionality is intact; the path guards and stable signing
remain as defense-in-depth. The profile is smoke-tested before use (no
KeepAlivecrash-loop on a malformed profile), existing installs adopt the
wrapper automatically on the nextlean-ctx update, and a new regression
(rust/tests/tcc_sandbox.sh) boots the daemon under the production wrapper.
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.8...v3.8.8