Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Version 5.x
[Unreleased] - ReleaseDate
Version 4.x
[Unreleased] - ReleaseDate
4.0 is a small major so far. There is one breaking change — session_log's boolean shape (file: true, verbose: true) is no longer translated to the per-sink mapping behind the scenes, and is now a parse error that names its replacement (#744). Alongside it, one restriction is loosened: native.claude_code.permissions is accepted rather than rejected, layering additively so the catch-all can carry Claude-Code-only permission keys llmenv doesn't model — without giving a native: block a way to weaken what the renderer produced (#750).
Everything shipping on the 3.x line is inherited; those entries live in CHANGELOG-3.md (Version 3.x on the docs site).
Removed
- Breaking: the boolean
session_logshape is gone.session_log: { file: true, transcript: false, verbose: true }parsed until now by being translated to the per-sink form behind the scenes; it is rejected outright in 4.0. Each sink is a mapping —file: { enabled, level },transcript: { enabled, level }— andverbose: truebecomeslevel: debugon whichever sink should capture prompts and tool calls, which means the two sinks can now differ. The parse error names the replacement; it does not rewrite your config. See Configuration (#744)
Added
features.launch_proxy:llmenv launch claude_codecan start a local HTTP proxy for the session that rewrites outbound Anthropic API requests (headers and JSON body) per declarative rules before forwarding them — e.g. trimming Claude Code's injected system prompt, or setting a field (likethinking) the request would otherwise omit. No TLS interception is needed, since Claude Code already respectsANTHROPIC_BASE_URL; an existing value there (a corporate gateway, say) is chained through rather than clobbered. Each rule can be gated by AND-combined conditions (header/body presence, absence, equality, or regex/substring match) before applyingset(upsert),remove, orstrip— and a rule whose target no longer exists, because Claude Code's request shape changed, is skipped with a logged warning instead of breaking the session. Off by default, Claude Code only. See Commands (#1289)llmenv launch <engine>resolves the environment the wayexportdoes and then runs the engine (claude,codex,crush, oropencode, by binary name or engine id) as a supervised child process — inherited stdio, the resolved environment layered on top, and an exit code mirroring the engine's (128 + signumwhen it dies by signal). SIGINT/SIGTERM/SIGHUP are ignored byllmenvitself so it can't exit ahead of the engine and orphan it mid-shutdown. No shell integration required, so it behaves the same from an interactive shell, a script, CI, or an IDE task;exportandhookstay available for callers that only want the variables. Unix only. See Commands (#1056)- A Codex adapter, PATH-gated like the others: with
codexonPATH, llmenv materializesconfig.tomlinto acodex/subtree and exportsCODEX_HOME. It renders MCP servers, the mergedAGENTS.md(viamodel_instructions_file), and the lifecycle hooks you declare incapabilities.hooks— Codex takes the same nested matcher-group shape as Claude Code and uses the same event names, so they map across without translation. A hook on an event Codex lacks (Notification) or using anmcp_toolhandler is skipped with a warning rather than written somewhere Codex ignores. llmenv also wires its own hooks for Codex — the config-source context, the managed-cache write guard, read-once dedup, the ICM memory and session-log lifecycle events, and throttle when configured — which work because Codex reads the samehookSpecificOutput/additionalContextshape Claude Code does. Permissions, statusline, auth, plugins, and doctor checks are tracked separately and are listed with their issue numbers in the docs rather than failing quietly. An MCP server using the SSE transport is skipped for Codex with a warning — Codex speaks stdio and streamable HTTP only, and rendering SSE as a URL would produce a server it silently treats as streamable HTTP and then can't talk to. See Engines (#233) - The Codex adapter applies
init.seeded_settingstoconfig.tomlthe same way Claude Code applies it tosettings.json— a key you've elected to seed is merged in once and left alone on every re-render, never overwriting a value the folder or Codex itself already set. A security-sensitive Codex key (approval_policy,sandbox_mode,sandbox_workspace_write,trusted_projects,shell_environment_policy) is refused with a warning rather than seeded, since llmenv doesn't model Codex permissions yet (#1102) and a seeded value there could silently run Codex less restrictively thancapabilities.permissionsestablishes on every other engine. Codex needs no install-method seed the way Claude Code does, since it detects its own install method (brew,npm, standalone, …) in-process from its own executable path; and it has no statusline seed either, becausetui.status_lineis a fixed list of built-in item identifiers rendered natively, not an external-command hook the way Claude Code'sstatusLineis — there's nothing forllmenv statuslineto attach to. See Engines (#1104, #1107) - The Codex adapter inherits session transcripts (
sessions/,archived_sessions/), prompt-recall history (history.jsonl), and the cached login (auth.json) acrossCODEX_HOMEhash changes — the same durable-state relocation Claude Code already has forprojects//session-logs//history.jsonl, so a config edit or version bump no longer empties/resumehistory or forces a fresh login.history.jsonlis copied in once and never overwritten;auth.jsongets a newest-mtime-wins capture instead, so a re-login or token rotation propagates rather than pinning the first-ever credential forever. Every copied file's permissions are forced to owner-only regardless of the source's mode, sincestd::fs::copyotherwise carries the source's mode — including a looser one — into the destination. Of the six SQLite databases Codex also writes into$CODEX_HOME, three —goals_1.sqlite,memories_1.sqlite,queue_1.sqlite— get the same treatment: symlinked into the durable store along with their-wal/-shmsidecars, since each holds data (per-thread goals, generated memory content, pending queued messages) with no other durable source. The other three (state_5.sqlite,logs_2.sqlite,thread_history_1.sqlite) are deliberately left alone — two rebuild themselves fromsessions/on their own, and the third is a 10-day-retention diagnostic log (#1420). See Engines (#1105, #1420) - The Codex adapter renders
capabilities.permissionsas a Codex permission profile, filesystem access only:Read/Edit/Write/MultiEditpath rules become[permissions.llmenv.filesystem]entries (read/write/deny, withdenywinning overwritewinning overreadat the same path, matching Codex's own precedence), and the profile is activated withdefault_permissions = "llmenv"— writing the profile alone would otherwise be dead config, since Codex only applies a named profile once one is selected. This is all-or-nothing per config: aBash,WebFetch, orask-tier rule has no Codex equivalent at all (no per-command allowlist, no per-rule "ask" posture — only the globalapproval_policy/sandbox_mode), and rendering the mappable subset while dropping the rest would produce a profile that looks more complete than it is, so a single such rule refuses the whole profile rather than rendering part of it.network.domainsstays unmodeled — meaningfully rendering it also requires modelingnetwork.enabled/network.mode, a bigger, separate sandbox/network-vocabulary gap. See Engines (#1102) llmenv doctorreports Codex-specific diagnostics when Codex is an installed adapter: whether the#1102permission profile will render or was refused (and why), any MCP server using a transport Codex can't speak (SSE), and whether an already-materializedconfig.tomlis valid TOML — all without requiring anexport/regeneraterun first. See Commands (#1100)- The Codex adapter folds
capabilities.rulesbodies (frontmatter stripped, each with a provenance comment) into the sameAGENTS.md/model_instructions_filecontent instead of dropping them, since Codex has norules/*.md-with-glob-frontmatter convention the way Claude Code and opencode do — a lossy transform (a rule's path-scoped, conditional application becomes unconditional prose), but the only target Codex has. See Engines (#1103) - The Codex adapter writes first-class skills (
capabilities.skills) and the built-inllmenvskill the same way Claude Code does, then registers each one with an explicit[[skills.config]]entry naming its materialized path — Codex has no auto-discovery forskills/, so a skill written but not registered would never be seen. Plugin-installation metadata and LSP config are verified-absent from Codex's own source rather than deferred: there is no analogue ofinstalled_plugins.jsonand no[lsp]/Lspconfig surface to render. See Engines (#1106) llmenv launchaccepts--scope,--tag, and--compress, which mean exactly what they do forexport— including the warning when a requested scope isn't active.launchalways resolved the scopes the working directory and environment made active, so anyone using--scopeto pick between environments could do it withexportbut not with the command that supersedes it. The flags may appear either side of the engine name; everything after--is still the engine's, so an engine with its own--scopeis reachable there. See Commands (#1384)native.claude_code.permissionsis accepted instead of rejected, making the catch-all the escape hatch for Claude-Code-only permission keys llmenv doesn't model (additionalDirectories,disableBypassPermissionsMode, and whatever ships next) without waiting on a neutral-schema field. It's safe to accept because the merge is additive:allow/ask/denyappend to what was rendered rather than replacing it,deny > ask > allowauthority is re-applied afterwards, rule strings get the sameWrite→Editnormalization asnative_permissions, and every other key overwrites.defaultModeis rejected here — it's modeled ascapabilities.permissions.default_mode, and allowing it would let anything that can author anative:block setbypassPermissionsand switch the permission system off. A fragment can tighten permissions or add unmodeled keys; it cannot loosen what the renderer produced — an omitted ornulldenyleaves the rendered one intact.native.claude_code.hooksstill hard-errors, since an array of matcher groups has no unambiguous additive merge. See Engines (#750)llmenv launchrelaunches the engine after a crash, reusing the already-resolved environment instead of re-running resolution.--auto-restartskips the confirmation prompt; either way, restarts are capped at 3 attempts within a rolling 5-minute window so a crash loop doesn't loop forever. See Commands (#1284)llmenv launchnotices whenconfig.yamlor a bundle changes while the session is running and surfaces a warning in the agent's own context on its next turn, since the ambient shell hook that used to catch this on the next prompt is gone underlaunch. Detection only — it never re-materializes or restarts on its own account, and it works for every enginelaunchsupports. See Commands (#1286)llmenv launchnotices when the cached Claude Code OAuth credential is close to expiry (or already expired with no live refresh token) and surfaces a warning the same way, pointing atllmenv login. Detection and notice only — llmenv doesn't perform the refresh itself; Claude Code does that on its own, and llmenv only caches the result. See Commands (#1285)features.sandbox:llmenv launch <engine>can run the engine in a container (docker/podman run --rm) instead of directly on the host, wrapped by the same crash/restart supervision as a host launch.runtime: autoprobesPATHforpodmanthendocker;--container/--no-containeroverridefeatures.sandbox.enabledfor one invocation. The project tree is bind-mounted read-write at/workspace(and set as the working directory),SSH_AUTH_SOCKis bind-mounted read-only when present, and the resolved/materialized environment is written to an owner-only--env-filerather than a live mount of~/.config/llmenv(or-eflags, which would put every value ondocker/podman's own argv). The container runs with--cap-drop=ALL,--security-opt=no-new-privileges, and--user <uid>:<gid>by default. For Claude Code with anANTHROPIC_API_KEYpresent, the raw key is sealed via a local icebreaker proxy instead of being forwarded into the container;features.sandboxandfeatures.launch_proxycan't both be enabled on the same launch yet. Off by default. See Commands (#1080, #1648, #1649, #1650, #1651)features.sandbox.forward_ssh_agent(defaulttrue) turns off theSSH_AUTH_SOCKbind-mount sandbox mode otherwise does unconditionally. A running SSH agent socket is a full signing oracle for that identity, not a reduced-privilege view of it, so a user who wants the sandbox's filesystem/host isolation without also handing the container that reach can now decline it per project. See Commands (#1671)llmenv launchprints a one-line stderr notice at launch time whenever sandbox mode actually bind-mounts the host'sSSH_AUTH_SOCKinto the container — previously the exposure was documented but silent at launch. Mirrors the existing unsealed-credential warning (#1669) and namesfeatures.sandbox.forward_ssh_agent: falseas the opt-out. See Commands (#1687)features.sandbox.image: nullnow resolves to llmenv's own published default sandbox image (ghcr.io/phaedrus1992/llmenv-sandbox) instead of failing the launch — previously sandbox mode needed a user-supplied image with no default at all. The image is deliberately minimal, libc and CA certificates only, with no engine binary baked in; llmenv bind-mounts the resolved host engine binary into the container and execs it directly, so any image with a compatible libc works regardless of what's on its ownPATH. See Commands (#1653)- A sandboxed
llmenv launch claude_codenow bind-mounts the materialized Claude Code config directory (CLAUDE_CONFIG_DIR) into the container read-only — previously it wasn't mounted at all, somcpServers, skills, plugins, and settings were invisible to the containerized engine. When ICM's MCP server address is a loopback address (the common case, since it usually names this same machine), a patched copy of.claude.jsonwith the address rewritten to the container's gateway host is overlaid on top, mirroring the existing icebreaker credential-proxy rewrite — otherwise127.0.0.1inside the container would mean the container itself, not the host running ICM. See Commands (#1652) llmenv doctorreports whetherfeatures.sandboxcan actually run: the configured container runtime (docker/podman) is onPATH, theicebreakerbinary is onPATH, and the configured image is pullable — checked withmanifest inspectagainst the registry directly, not a realpull, so it costs no bandwidth or disk on every doctor run. Previously a missing runtime or unreachable image only surfaced as a launch-time failure. See Commands (#1654)- The config-dir mount #1652 added for a sandboxed
llmenv launch claude_codenow applies to crush, opencode, and codex too — each adapter's materialized config directory is bind-mounted into the container, and any MCP-server URL in it pointing at loopback is rewritten to the container's gateway host the same way ICM's was for Claude Code (JSONmcpServers/mcpfor Claude Code/Crush/opencode, TOMLmcp_serversfor Codex). Generalizing it surfaced a gap the Claude-Code-only scope had been masking: Codex keeps a runtime-written OAuth credential (auth.json) outside its own materialized config the same way Claude Code keeps.credentials.json, and mounting the directory without masking it would have handed the container a live credential llmenv never wrote — it now gets the same/dev/nulloverlay.credentials.jsonalready had; Crush and opencode have no such file to mask. See Commands (#1698)
Changed
- The Codex adapter now delivers the session-start memory block too (
[ICM MEMORY CONTEXT (session start)]). Codex readsSessionStartadditionalContextas model context, and llmenv used to drop it. Seehook-run(#2142) --scope IDnaming a scope that isn't active now resolves nothing — no tags, no bundles — instead of falling back to every active scope. The warning has always said "no bundles will fire for this scope"; the code did the opposite of narrowing. Applies toexportandlaunchalike, and both still warn and exit 0 rather than failing. It matters most underlaunch, which injects the result straight into an agent whose interface clears the screen a moment later, so a typo'd scope handed it every active scope's MCP endpoints and credentials with nothing visible to say so. If you relied on an unmatched--scopeexporting everything, drop the flag. See Commands (#1399)PATHresolution skips every non-absolute entry, not just the empty one. A shell resolves.,bin, and an empty entry alike against the working directory, soPATH=".:/usr/local/bin"let llmenv pick./claudeover the installed one — the hijack the empty-entry guard exists to prevent, one character apart. Nothing llmenv looks up (claude,codex,crush,opencode,mcp-proxy,uvx,icm) is expected at a relative path; if yours is, give it an absolutePATHentry (#1400)
Fixed
llmenv login,llmenv setup's engine handoff, andllmenv editsupervise their child the waylaunchdoes, instead of waiting under the default signal disposition. A signal aimed atllmenvalone — a supervisor, a script,kill <pid>— used to kill it and leave the child running:claude auth loginwriting a credential into a temp directory nothing would read, or a full-screen editor with the shell drawing a prompt over it. llmenv now keeps waiting and reports the child's own status. A terminal Ctrl-C still reaches the child directly, since the terminal delivers it to the whole foreground process group. See Commands (#1385)llmenv hook-run --engine <unknown>fails, naming the valid engine ids, instead of running the hook against whatever adapter the environment looked like. The fallback announced itself only through awarn!, which llmenv's ERROR-only default log filter discards, so a typo'd or stale--enginesilently read a different engine's config. Omitting the flag is unchanged. See Commands (#1386)llmenv launchforwards SIGTERM and SIGHUP to the supervised engine instead of swallowing them. Ignoring every signal was right for a terminal Ctrl-C — the whole foreground process group already gets it — but wrong whenever a supervisor targetsllmenv's pid alone (docker stopsignalling PID 1, systemdKillMode=mixed, a CI runner or IDE task doingkill <pid>): the engine never learned to shut down and nothing exited until that caller's SIGKILL deadline. SIGINT is still deliberately not forwarded, since the engine already has its own copy and a second one reads as a double Ctrl-C, which many agents treat as "force quit".launchstill never exits on its own account, so the status you get is always the engine's. See Commands (#1383)- A rejected environment variable name whose first character is multi-byte no longer reports a character that isn't in the name. The check read the leading byte and printed it as a character, so
llmenvblamed e.g.Ãfor a name starting withé. The name was rejected either way — only the message was wrong (#1387) - Codex wires the per-turn lifecycle hooks it was missing:
turn_startwhen a memory backend resolved for the scope,stopfor the task tracker and slippage self-critique, the slippage rule-reinjection hook, and the session-log turn capture set. OnlySessionStart/SessionEndwere ever registered, so a Codex scope withfeatures.memorygot the MCP server but never the recall that fires each turn, and the task tracker's Stop reminder never ran at all. The session-log set is Claude Code's minusNotification, which Codex has no event for. Both adapters now read the same gates rather than each deriving its own, andllmenv doctor's lifecycle-hooks report covers Codex too instead of only Claude Code. See Engines (#1435) llmenv doctorno longer reports a bundle directory it cannot stat as one that does not exist. A permission-deniedbundles/<name>came out as "declared but directory does not exist", which points at the wrong fix; the underlying error is now surfaced instead. The adapter cache version-skew scan got the same treatment — it warns that it could not check rather than silently reporting the adapter has no cached builds. Four more spots that folded a stat error into a wrong answer went with them: a marketplace clone that cannot be read no longer reads as "not synced" or skips its pin check silently, a plugin whoseskills/directory cannot be stat'd no longer contributes zero skills without saying so, and an unreadableSKILL.mdis no longer reported as missing (#1436)- opencode wires the same per-turn lifecycle hooks Claude Code and Codex do:
turn_startfor per-turn memory recall,stopfor the task tracker and slippage self-critique, the slippage rule-reinjection hook, and the session-log turn capture set (narrowed to the four events opencode actually dispatches — it has noNotification,SubagentStop, orPreCompact). An opencode scope withfeatures.memoryconfigured got the MCP server but never the recall that fires each turn, the same gap #1435 closed for Codex.llmenv doctor's lifecycle-hooks report now covers all three adapters. See Engines (#1439) - A
Readtool call no longer fireshook-run pre_tool_usetwice on Claude Code and Codex. Read-once dedup's matcher-scoped registration and session-log capture's unmatched registration both matched the same call, sorepeat_detect's loop-breaker tripped at half its configured threshold andread_oncereported a file's first read as already read (#1442) llmenv doctor's cache-directory-writable check no longer reports a stat error — permission-denied on a parent directory, say — as the cache directory simply not existing. Same class of fix as #1436, extended to the one spot it didn't already cover (#1445)- Engine detection resolves
PATHdirectly instead of shelling out towhich, so an installed engine no longer looks missing on an image that ships withoutwhich— routine for distroless and minimal containers. Previously both "not installed" and "couldn't runwhich" produced the same answer, whichllmenv launchreported as a flat "not found on PATH — install it" for an engine that was present and runnable (#1382) - Folding a pre-existing Codex SQLite DB (
goals_1.sqlite,memories_1.sqlite,queue_1.sqlite) into the durable state dir now decides fold-or-keep once from the base file and applies it to the-wal/-shmsidecars together, instead of letting each sidecar pick its own winner and risk folding a base file from one point in time alongside a WAL sidecar from another. See Engines (#1449) - That same fold is now skipped, with a retry on the next
export, when a DB's base file still looks unmigrated and its-shmsidecar exists — evidence a Codex process may have it open in WAL mode. Folding a live DB risked copying a torn snapshot or stranding the running process's writes on an orphaned inode after the symlink swap. See Engines (#1448) - That same skip-when-live check now covers the whole SQLite family (base file,
-wal,-shm), not just the base file. A prior fold that partially failed — symlinking the base but not a WAL sidecar — left the base looking fully migrated, so the check on it alone missed the still-real, potentially-live sidecar and folded it with no liveness protection at all. See Engines (#1450) - Capturing a folder's
auth.jsonback into the durable store no longer risks persisting a torn credential. Codex writesauth.jsonby truncating and rewriting it in place rather than atomically, so a capture racing a token refresh could read a partial file and corrupt every folder's inherited credential until the next login. The read is now bracketed by a stability check plus a JSON-validity backstop, and discarded (with a retry on the nextexport) if either fails. See Engines (#1451) - The statusline's
scopeswidget no longer shows a stale tag set after$LLMENV_EXTRA_TAGSchanges mid-session. The widget read tags from the materializedllmenv-status.jsonsnapshot, written only byllmenv regenerate— and even a regenerate didn't help within the same shell, since the materialized cache folder is keyed by the active tag set and a running session'sCLAUDE_CONFIG_DIRstays fixed to the old folder until the shell restarts.scopesnow re-reads$LLMENV_EXTRA_TAGSlive on every render and unions it onto the snapshot's tags; every other tag source (host, user, OS, network, project, content scopes) still only refreshes on the next regenerate. See Configuration (#1538) - The statusline's
plugins,mcps, andthrottlewidgets no longer trust a stale, tag-gated count after$LLMENV_EXTRA_TAGSchanges mid-session — the same root cause #1538 fixed forscopes.pluginsresolves purely from top-level config, so it now recomputes live on every render, same asscopes.mcpsandthrottleneed the merged manifest's bundle-contributed data to re-resolve, which isn't cheaply available at render time, so they instead show a staleness marker (⚙️ by default) whenever a live tag isn't already in the last regenerate's snapshot. See Configuration (#1547) - The redundant-
context-mode:context-mode-declaration warning no longer logs on everyllmenv statuslinerender. #1547 moved thepluginswidget's resolver onto the render hot path, so awarn!meant for a one-time config nudge fired on every render instead; it now logs atdebug!there, matching this file's other best-effort collectors, whilellmenv regenerate's own resolve — interactive and user-invoked — still logs it atwarn!. See Configuration (#1551) - A tilde-prefixed config path (
~/...) is expanded before it's loaded, instead of only being rejected by a debug-only assertion that a release build silently skipped, leaving the path unexpanded and the load failing with a confusing "file not found" (#1577) - A project name (
.llmenv.yaml'sname:field) containing control characters — an ANSI escape or an embedded newline — no longer reaches session-log search content unescaped, closing a gap where tags and bundles were already sanitized but the free-form project name was not (#1578)
Security
llmenv-task'ssave_task/load_tasknow validate a task's slug before building a filesystem path from it, the same checkresolve_identifieralready applied. Extractingtaskinto its own published crate (#1461) madeTask::sluga public field, so code holding aTaskcould set it to a path-traversal payload (../../etc/passwd) and reach outside the task store; the one caller insidellmenvitself already validated first, so this closes the gap for any other caller rather than an exploit shipped so far (#1465)llmenv launchwarns on stderr when sandbox mode is active, the resolved environment carries a credential-shaped variable (*_API_KEY/*_TOKEN/*_SECRET), and the engine isn't Claude Code. icebreaker's sealed-token protection is scoped to Claude Code only, so every other engine still receives that raw credential inside the container as-is — the warning at least makes the gap visible instead of silent. See Commands (#1669)- Releases now carry a signed SLSA build provenance attestation, verifiable with
gh attestation verify <binary> --repo phaedrus1992/llmenv. Previously each release shipped a<asset>.intoto.jsonlwritten by a hand-rolled step in the release workflow: it had no signature or certificate chain, so anyone who could write a release asset could rewrite it, and it named aslsa-github-generatorbuilder that was never invoked. Theslsa-verifiercommand printed in every release's notes could not succeed against it. The unsigned file is gone, and the notes now print a command that works. Releases up to v3.11.0 still carry the old file — ignore it (#1412) llmenv upgradeverifies the downloaded binary against the SHA-256 published in the release'schecksums.txtbefore installing it, and fails closed — a release with no checksums, or none for this platform, aborts the upgrade rather than installing something unverified. Previously the only checks were "the HTTP request succeeded" and "the installed binary runs--version", neither of which a tampered or truncated download would fail. The checksum alone does not prove the release pipeline was honest — whoever can replace the binary can replace the checksum beside it — which is what the signed provenance above is for; teachingupgradeto verify that automatically is #1411. See Commands (#1040)- llmenv no longer runs a binary out of its working directory when
PATHhas an empty entry — a leading, trailing, or doubled:, whichPATH="$MAYBE_UNSET:$PATH"in a shell profile produces. Two things were wrong. Themcp-proxy/uvxlookup was a second copy of llmenv'sPATHresolver that never picked up the guard the engine-detection copy got, so it accepted./mcp-proxyand disagreed with whatllmenv doctorreported for the same binary; there is one resolver now. And every spawn passed a bare program name, whichexecvpre-searches under its own rules — POSIX has it treat an empty entry as the current directory — so the guard was defeated at exec time even where the check was right:llmenv launchwould run./claudeand layer the resolved environment (MCP endpoints, tokens) onto it.launch,login,setup's engine handoff, and the proxy now resolve once and spawn the absolute path, presenting the bare name asargv[0]the way a shell does (#1390) llmenv launch's mid-session notice socket now checks the connecting peer's uid and rejects anything not running as the same user, a second, independent layer on top of the socket's directory and file already being owner-only: a different user is rejected even if that permission enforcement were somehow bypassed. It does not, and cannot, distinguish one same-user process from another — that gap is a separate, harder problem, tracked as a follow-up rather than claimed as closed here. No new dependency —tokio::net::UnixStream::peer_cred()andrustix::process::geteuid()were both already in use. See Commands (#1483)llmenv launchnow generates a per-session shared secret and requires it on every request to the mid-session notice socket, closing the follow-up #1483 named but didn't fix: a uid check alone cannot tell the real engine's descendant from another process — a compromised dependency, another local tool — running as the same user. The secret is 32 random bytes from the OS CSPRNG, hex-encoded into a newLLMENV_LAUNCH_TOKENenvironment variable the supervised engine (and everything it spawns) inherits alongsideLLMENV_LAUNCH_SOCKET, and compared in constant time so a mismatch can't be narrowed down one byte at a time by timing. This is a bar-raise, not a hard guarantee: on Linux,/proc/<pid>/environis readable by the same uid by default, so a same-uid attacker who locateslaunch's pid can still read the token from there. See Commands (#1484)- Closes the gap #1484's own shared secret left open: that secret traveled as a plain bearer credential, so anything that could redirect
LLMENV_LAUNCH_SOCKETto an attacker-controlled socket — a poisoned engineenvblock, a wrapper script — could harvest it the moment a client connected, before the client had any way to know it dialed the wrong endpoint.launchand the connecting client now run an HMAC-SHA256 challenge-response before exchanging a request, and the response carrying the notice is itself proofed too, so a relay that faithfully forwards the handshake without ever learning the secret still can't substitute its own text for the real notice. Each of the three proofs is bound to a distinct role label plus both connection nonces, closing a reflection attack an earlier, undifferentiated version of this fix was vulnerable to (opening a second connection to get the server to sign, with zero knowledge of the secret, exactly the value the first connection's request needed) — caught before merge by this repo's own pre-PR security review, not shipped. The secret itself never appears on the wire in either direction. Same known limitation as #1484 — this hardens the socket protocol, not/proc/<pid>/environ's readability by the same uid. See Commands (#1487) - Materializing bundle files into an existing materialized folder, and
llmenv upgradebacking up the running binary before installing a new one, no longer write through a pre-existing symlink at the destination — a plain file copy opens the destination withO_TRUNC, which follows a symlink there and overwrites whatever it points at instead of replacing the link. Affectedllmenv export/regenerate's re-render of bundle content for Claude Code and every other adapter that shareswrite_in_place, plusupgrade's fixed, predictable.llmenv-upgrade.bakpath. Each copy now lands in a same-directory temp file first, then atomically renames over the destination — POSIX's rename semantics replace the destination entry regardless of what it is, without ever following it (#1422, #1423) - Extends the fix above to directory components, not just the final file:
write_in_place, the Claude Code adapter'smaterialize, and the opencode adapter's rules-file writer no longer follow a symlinked subdirectory anywhere along a bundle file's relative path.create_dir_all(parent)happily resolves through a symlinked intermediate directory, which is a strictly stronger primitive than the leaf-only bug — write content into any directory the invoking user can write, under a name of the attacker's choosing — so all three now walk each directory component viaopenat-relative descent (the same primitivellmenv prune's stranded-transcript cleanup already uses) instead of resolving the whole path at once.write_owner_onlyitself still has a related, separate gap at the leaf — tracked in #1429 since it's used far more broadly than these three call sites (#1427) write_owner_only— the single most widely-used config/state writer, called from every adapter forsettings.json,AGENTS.md/CLAUDE.md, hook JSON, and more — no longer writes through a pre-existing symlink at its destination. Same root cause as the two entries above (.create(true).truncate(true)follows a symlink and truncates its target). Now shareswrite_owner_only_atomic's temp-file-plus-renamemechanism instead of a direct exclusive-create, sincerename's replace-regardless-of-what's-there semantics close the gap without a retry loop that doesn't scale under real concurrent writers to the same path (#1429)- The session-log JSONL sink no longer appends through a pre-existing symlink at its destination — the same class of gap as the entries above, but append-mode can't simply replace the destination the way the others do (that would discard the log history an append is meant to preserve), so it's refused outright instead via a new shared check. The MCP proxy's own log writer already had this check; it's now shared rather than duplicated (#1431)
- Codex's cached credential (
auth.json) no longer sits in freed-but-unscrubbed heap memory after a capture. The read that pulls it into memory used a plainVec<u8>, dropped without zeroing; it's now aZeroizing<Vec<u8>>, scrubbed on drop. The JSON well-formedness check right after that same read had the identical problem one line later — parsing into aserde_json::Valuecopies the token into another unscrubbed buffer — closed with a validity check that confirms the JSON parses without keeping any of it around (#1456) - The Claude Code auth cache (
.claude.json's identity block and.credentials.json's OAuth token) gets the same memory-hygiene treatment as Codex'sauth.jsonabove, across every read and write path that touches it — including the macOS keychain, the default backend on that platform, whose token used to pass through an unscrubbedStringconversion (#1469) features.launch_proxy's local HTTP proxy now requires a per-session peer-auth token on every request, the same gap #1483/#1484 closed for the mid-session notice socket: loopback-only binding blocks off-host access but not a different local user on the same host, once the proxy's port is discoverable (the engine's own environment,/proc/<pid>/environ,lsof). The token is injected viaANTHROPIC_CUSTOM_HEADERS— appended to any value already set there, never overwriting it — so Claude Code sends it as a header on every request with no client-side change of its own; a missing or wrong token gets401, compared in constant time, and the header itself is stripped before the request reaches the real upstream. Unlike the notice socket's HMAC challenge-response, this is a static bearer token:ANTHROPIC_CUSTOM_HEADERScarries a fixed value for the whole session, so there's no per-request handshake to layer on top. See Commands (#1632)- llmenv's own published default sandbox image (
ghcr.io/phaedrus1992/llmenv-sandbox, #1653) is now pinned by content digest instead of the mutable:v1tag. A mutable tag let anyone able to push tomain/release/*and triggersandbox-image.yml— or anyone who compromised GHCR push credentials — silently repoint what every sandboxed launch with nofeatures.sandbox.imageconfigured would pull next. See Commands (#1703) docker/sandbox/Dockerfile's base image is now pinned by content digest instead of the mutabledebian:bookworm-slimtag, closing the same class of gap as the entry above one layer deeper: a Debian security update or a registry-side swap could otherwise change what the published sandbox image builds from with no corresponding change in this repo (#1704)- Sandbox mode's per-launch temp files (the resolved
--env-file, and the patched.claude.jsonoverlay used to reach a loopback ICM server from inside the container) no longer sit in the shared OS temp dir. Each file was already owner-only, but the directory itself was shared and world-writable, and a launch that crashed before cleanup left its file there indefinitely; both now live under an owner-only subdirectory of llmenv's own state dir, swept on the next sandboxed launch of anything left by a prior process that's no longer running — not by age, since the patched.claude.jsonoverlay stays bind-mounted for a session's whole lifetime and aging it out would delete a live session's mount source out from under it. See Commands (#1705) .github/workflows/sandbox-image.ymlnow runs a Trivy vulnerability scan against the published sandbox image and fails the build on a HIGH- or CRITICAL-severity finding. A finding is suppressed only through a reviewed entry in the newdocker/sandbox/.trivyignore, never a blanket severity drop. See Commands (#1721)- The published sandbox image now carries an SPDX SBOM, attached alongside the build-provenance attestation the workflow already produced — inspectable with
docker buildx imagetools inspectorgh attestation verifythe same way the provenance attestation is. See Commands (#1722) - The published sandbox image is now signed with keyless cosign (Sigstore, using the publishing workflow's own GitHub Actions OIDC identity — no key material to manage), so a consumer can verify the image's origin independent of the attestations above, the same way release binaries are already verifiable with
slsa-verifier(#1412). See Commands (#1723) DEFAULT_SANDBOX_IMAGE's digest pin (#1703) is now tracked by a Renovate custom manager, closing the gap the digest pin itself opened: nothing previously noticed when a Dockerfile base-image bump, or a manual rebuild, republished the same tag under a new digest. See Commands (#1725)llmenv launch --containernow verifies a pulled sandbox image's build-provenance attestation withgh attestation verify --signer-workflowbefore running it, and refuses to run an image that fails verification — only for llmenv's own published sandbox image; afeatures.sandbox.imageoverride to any other image skips the check, since that image was never attested by this repo. A machine with nogh(GitHub CLI) installed, no network path to GitHub, or one that reaches GitHub but gets an inconclusive answer (rate limiting, a registry/API outage), still launches — verification is skipped with a one-line stderr notice rather than blocking, since only aghthat reached a definitive answer and reported a failure is treated as a real signal. See Commands (#1719).github/workflows/sandbox-image.yml's Trivy scan now gates publishing instead of running after the fact, and covers both platforms the image builds for. Previouslydocker/build-push-actionpushed the image under itsvN/sha-<short>tags before Trivy ran, so a HIGH/CRITICAL-vulnerable image was briefly pullable under those tags before the workflow failed — and the scan only ever checked the CI runner's own platform (linux/amd64), neverlinux/arm64, despite the image being multi-arch. Each platform is now built and pushed to GHCR by digest only, scanned directly, and merged into the release tags — with no rebuild — only once every platform has passed. The job that runs the scan also no longer holds Sigstore-signing/attestation privilege; that stays isolated to the job that runs after the scan gate. See Commands (#1732, #1733)llmenv launchwarns on stderr when sandbox mode is active and a Crush or opencode provider's materializedapi_keylooks like a literal secret rather than a"$VAR_NAME"env-var reference — the same class of gap #1669 already covers for a credential-shaped environment variable, just for a value sitting in the MCP-config file #1698's mount now exposes to the container instead. Claude Code has nomodel_providersconcept, and Codex doesn't renderapi_keyinto its own config yet, so neither is affected today. See Commands (#1764)
Version 3.x
[Unreleased] - ReleaseDate
Security
- The umans usage request connects only to the addresses that passed the private-network check. It no longer follows redirects or uses proxy settings, so a DNS rebind, a redirect, or a proxy can no longer send the Bearer token to a private or metadata address (#2518) [fix:throttle]
[3.12.1] - 2026-10-07
3.12.1 fixes six bugs in the 3.12.0 release.
Hooks and task tracker. The Stop hook no longer repeats the same task reminder every turn (#2511). Features that only a bundle sets now work in the hooks (#2460). A bare task slug resolves in the caller's open session first (#2501).
Memory and MCP.
Memory prune rejects a record with a nan or inf weight.
The SessionStart memory health check works with a .local memory host that has link-local addresses (#2512).
Plugins.
llmenv plugin-sync skips a path marketplace that is missing on this host and syncs the rest (#2513).
Fixed
- A feature that only a bundle's
bundle.yamlsets now works in the hooks. The hooks andllmenv config-contextread the rootfeatures:block only, sotask_tracker,read_once,repeat_detect,slippage, andcd_guardregistered their hooks and then ran them switched off. They now resolve each one the way the adapter does (#2460) [fix:hook] - A bare task slug now resolves in the caller's open session first. Before, a done task from a finished session kept its slug, and
llmenv task startordoneon that slug hit the old task with no warning. See Commands (#2501) [fix:task] - Memory prune no longer treats a record with a
nanorinfweight as a valid record. [fix:memory] - The Stop hook no longer repeats the same task reminder every turn until Claude Code ends the loop. A Stop that a Stop hook caused gets no reminder, an unchanged reminder is shown once, and the reminder names only the sessions of the conversation that stops. See Stop reminder rules. (#2511) [fix:hook]
- The SessionStart memory health check no longer reports memory as dead when the memory host is a
.localname that also resolves to link-local IPv6 addresses. llmenv drops the link-local addresses and connects to the rest. See MCP. (#2512) [fix:mcp] llmenv plugin-syncno longer stops at a path marketplace whose directory is missing on this host. It warns, skips that marketplace, and syncs the rest. See Commands. (#2513) [fix:plugins]
[3.12.0] - 2026-10-06
3.12.0 makes the agent's working state durable and the background plumbing honest.
The task tracker, ICM memory, and MCP servers now report failures, survive a dead child process, and stop trusting input they cannot check.
It also adds a few Claude Code settings and doctor checks for the 2.1.28x line.
Task tracker.
Tasks gain sub-tasks and a queue: top-level tasks run in order, sub-tasks run in parallel, and a parent closes only after its children (#2455).
task reopen undoes a done by mistake (#2471), and task done and session finish refuse to close work that never started or is still open (#2416).
Sessions record resume context (notes, issue, branch, plan docs), so a fresh agent can pick up cold (#2339).
SessionStart now injects a fixed statement of the tracking rules, and nudges fire while work happens, including a deny on the first commit with no task (#2456, #2457).
Memory (ICM).
Recall is adaptive: each memory goes out once per context, and prompts, failed tool calls, and subagents pull what matches them (#2249).
The session wake-up pack now reaches the model, and recall calls name the session's project, so a remote ICM server no longer answers from an unrelated project (#2251, #2253, #2313).
Post-session consolidation runs on SessionEnd, reads only the current project, and skips rules it already stored (#2355, #2387).
Background work writes a checkpoint and resumes at the next session start, and a request id keeps a retried store from writing twice (#2396, #2397).
memory prune is safer: it refuses when it cannot apply retention, reads one project only, and ignores forged record headers (#2386).
MCP and codebase-memory.
Session start probes the memory server and codebase-memory-mcp with a real initialize, restarts a stopped ICM proxy, and names any server that stays down (#2358).
features.codebase_memory[] adds allowed_roots and mem_budget_mb, and index_repository calls outside the allowed roots are denied (#2406, #2154).
The MCP client now fails on an isError reply, follows no redirect, and refuses plain http:// to a public address, at run time and at config render (#2476, #2483).
Claude Code adapter and config.
capabilities.model_effort sets effort per model, and effort_level now reaches Opus 5.5 (#2144).
capabilities.advisor_size becomes advisor_model (#2409), and always_load keeps chosen MCP tools out of tool-search deferral (#2356).
claude.ai account skill and plugin sync is off by default, so llmenv's scope rules apply (#2146).
Each session writes an agent-config document that a PostModelSwitch hook keeps current (#2398).
A corrupt settings.json is kept as settings.json.corrupt, and a stale rendered MCP key no longer survives a re-render (#2376).
Plugins and scopes.
Marketplace plugins can use git-subdir sources and sha pins, and the github source form with no url now syncs (#2441, #2440).
Network scopes match on match.cidr and match.ssid, which were accepted before but ignored, and macOS arp MAC addresses now match (#1051, #2487).
llmenv doctor.
New checks cover retired Claude Code settings, the ICM server version, the size of always-loaded instructions, MCP text that Claude Code cuts at 2,048 characters, the codebase-memory index result, and unfinished background work.
doctor --restart-memory-proxy replaces the pkill advice, which stopped every session's proxy (#2417).
Breaking changes.
advisor_size is removed, effort_level rejects values Claude Code does not accept, and task add --no-parent does nothing now.
task done and task session finish refuse without --force or --abandon-open, and git:// and <helper>:: plugin sources are rejected.
Each one carries a **BREAKING:** prefix below.
Added
- ICM recall now adapts to the session instead of sending the same memory block on every prompt. Each memory goes out once per context (again after a compaction or
/clear), and each prompt pulls memories that match what you're asking and what the session just touched, plus related topics; a failed tool call gets memories about that error, and a new subagent gets memories for its task. On by default; setfeatures.memory[].adaptive_recall: falseto get the old behavior. See Configuration (#2249) [feat:hook-run] capabilities.model_effortsets the start effort and an effort cap per Claude model, rendered into Claude Code'smodelSettings. Levels you save with/effortfor other models survivellmenv regenerate. See Configuration (#2144) [feat:config]llmenv doctorwarns about retired Claude Code settings, environment variables, permission tools, and MCP server types in the rendered config, and names what to use instead. See Troubleshooting (#2145) [feat:doctor]llmenv doctorreports the ICM server version on the host that serves memory, and warns below 0.10.60 (adaptive recall returns little) and 0.10.64 (weaker ranking). See Troubleshooting (#2261) [feat:doctor]llmenv task start --reopenmoves adonetask back toopenand starts it, for a step closed before its work was finished. Seetask(#2338) [feat:task]- Session start checks that the memory server and
codebase-memory-mcpanswer an MCPinitialize, restarts a stopped ICM proxy on the host that serves memory, and names any server that stays down, with the fix.llmenv doctorruns the same check underMCP servers:. See MCP (#2358) [feat:hook] llmenv task session startrecords resume context: notes, issues, branch, base, memory topics, and plan docs, withsession editandsession noteto change it. The SessionStart reminder,session show, andsession summaryprint it, and llmenv fills the branch and issue from git. Seetask(#2339) [feat:task]llmenv task add --detailandtask edit --detailstore what someone needs to do a task cold. Seetask(#2339) [feat:task]mcp[].always_loadandfeatures.memory[].always_loadrender Claude Code's per-serveralwaysLoad, which keeps a server's tools out of tool-search deferral. Unset renders nothing. See Configuration (#2356) [feat:adapter]- Claude Code sessions write an agent-config document (engine, model, effort, project, tags, config hash) to the state dir, and a
PostModelSwitchhook keeps the model current. A resumed or compacted session starts with a one-line[llmenv session]summary of it, andllmenv task session summaryshows arunning asline. See Commands (#2398) [feat:hook] llmenv doctorreports the size of the always-loaded instruction text (CLAUDE.mdand rules without apaths:filter), names the largest bundles, and warns over Claude Code's large-file limit. See Commands (#2357) [feat:doctor]llmenv doctormeasures the instructions and tool descriptions of each MCP server and warns when Claude Code would cut them at its 2,048-character limit.llmenv doctor --probe-mcpalso measures stdio servers. See Commands (#2148) [feat:doctor]- Marketplace plugin entries with a
git-subdirsource install the plugin from the named folder of the repo, and ashapins the exact commit forgithub,url, andgit-subdirsources.archiveandcommandsources are skipped with a warning that names the kind. See Plugins (#2441) [feat:plugins] features.codebase_memory[].mem_budget_mbsetsCBM_MEM_BUDGET_MBfor the server and the SessionStart index.llmenv doctorreports the result of the last codebase-memory index, including the budget to set after an over-budget stop. See MCP (#2154) [feat:mcp]features.codebase_memory[].allowed_rootsadds folders that codebase-memory-mcp may index. llmenv records the project root, its own folders, the code-explorer cache, and these entries at SessionStart, warns about a root the server refuses, and denies anindex_repositorycall outside them. See MCP (#2406) [feat:mcp]llmenv doctor --restart-memory-proxystops the local memory proxy that the pidfile names and starts it again. Each proxy start writes a line tomcp-proxy.logwith the spawner (export,session-start, orrestart). See Troubleshooting (#2417) [fix:mcp]llmenv task add --child-ofmakes a sub-task: sub-tasks run in parallel and the parent cannot be marked done before they are.--paralleltakes a task out of the queue,--afterrecords a task that must finish first, andtask session start --taskadds the first tasks with the session. See Commands (#2455) [feat:task]- The task tracker nudges while work happens: a reminder after a workflow skill starts, a nudge after several tool calls with no task, a reminder to park a task when the agent asks the user a question, and a one-time deny of the first
git commitorgh pr createwith no task in progress. An open session with no tasks is now reported. Switches:features.task_tracker.nudgesandenforce_commit. See Commands (#2456) [feat:task] - While the task tracker is on, SessionStart injects a fixed statement of the tracking rules with the exact
llmenv taskcommands, so an empty personal config gets them.llmenv doctorwarns about instruction text that says the task tools are blocked or forbidsllmenv task. See Commands (#2457) [feat:task] llmenv task reopen <slug>...moves one or moredonetasks back toopen, keeping their notes and parent, and refuses the whole call if any task is notdone. This is the undo for a task closed by mistake. Seetask(#2471) [feat:task]
Changed
- The Claude Code adapter now disables claude.ai account skill and plugin sync by default (
syncClaudeAiSkills/syncClaudeAiPlugins: false). Since Claude Code 2.1.275, a signed-in session downloads the skills and plugins enabled on that account and loads them into every scope, going around llmenv's own scope rules. Setnative.claude_code.syncClaudeAiSkills: true(and the plugins equivalent) to opt back in; on the first render after upgrade, Claude Code moves already-synced items intoskills/.trash//plugins/.trash/rather than deleting them. See What the Claude Code adapter emits (#2146) [feat:adapter] - With
adaptive_recall: false, the per-prompt natural-language recall now names the session's project. It used to be filtered by whatever directory theicm serveprocess happened to run in, which on a remote ICM server meant memories from an unrelated project (#2253) [fix:hooks] - The session-start memory block in Claude Code and opencode now starts with
[ICM MEMORY CONTEXT (session start)], so the model can tell it apart from per-prompt recall. A resumed or forked session no longer fetches and injects a second wake-up pack, since its conversation already holds the first one. opencode also receives the block now, in its first message; llmenv used to drop it. Seehook-run(#2142) [fix:adapter] - BREAKING:
effort_level(incapabilitiesandfeatures.slippage) now fails validation unless it islow,medium,high, orxhigh, the values Claude Code accepts in settings. It used to take any string. Formaxorultracode, the error names the setting to use instead. See Configuration (#2144) [feat:config] llmenv doctorno longer recommendsBASH_MAX_OUTPUT_LENGTHwhenbashOutputMaxCharsis set, since Claude Code ignores the variable then, and no longer warns about an unset prompt-cache TTL, since subscription plans get 1 hour automatically.CLAUDE_CODE_PROMPT_CACHE_TTL=1hnow passes the check. See Troubleshooting (#2145) [feat:doctor]- The opencode adapter now warns for each agent frontmatter field it drops, such as Claude Code's
omitClaudeMd, the same way it already does for command frontmatter. See Engines (#2150) [feat:adapter] - The consolidation
claude -pcall no longer waits for MCP servers to connect (CLAUDE_CODE_MCP_STARTUP_WAIT_MS=0), so it starts faster (#2148) [perf:consolidation] llmenv task show --currentprints each session's resume context on stderr. Its JSON on stdout does not change (#2339) [feat:task]capabilities.advisor_sizeis nowcapabilities.advisor_modeland renders Claude Code'sadvisorModel. Usefable,opus,sonnet, or a model ID. See Configuration (#2409) [fix:config]- BREAKING:
llmenv task donerefuses a task that was never started, andllmenv task session finishrefuses while a task isopen,wip, orwaiting. Both exit non-zero with the fix.task done --forceandtask session finish --abandon-openoverride them, and the native-tool redirect never forces. See Commands (#2416) [fix:task] - The ICM tools now load with the prompt in Claude Code, so the model no longer calls tool search before a recall or a store. Set
features.memory[].always_load: falseto defer them again (#2356) [feat:adapter] - BREAKING: Plugin and marketplace sources that use
git://or a<helper>::remote helper are rejected, likeext::andhttp://before. A pin (shaorref) that is not a string is an error, not an unpinned clone (#2441) [fix:plugins] - The session-start notice for a memory proxy that does not answer names
llmenv doctor --restart-memory-proxyinstead ofpkill, which stopped the proxy of every session (#2417) [fix:mcp] - BREAKING: A new task no longer chains onto the previous one. Top-level tasks form a queue:
llmenv task startrefuses a task while the task ahead of it is not done or waiting, and--forceoverrides. The warning for an undone parent is gone, andtask add --no-parentnow does nothing. See Commands (#2455) [feat:task] - A session started in
$HOME, in a parent of$HOME, or in/is no longer recorded as an allowed codebase-memory root, because the server keeps a root for good.llmenv doctorand the SessionStart notice say so. See Configuration [fix:mcp] llmenv memory prunereads only the memories of the current project and refuses to run when it cannot tell the project, where it used to read whatever ICM's own working folder held. Thememory prune,hook-run, andcheck-stalehelp text now matches what the commands do. Seememory[fix:cli]- The first Stop-hook idle reminder now offers
llmenv task wait <slug> "<reason>"next tollmenv task start <slug>, so an agent whose next step needs the user is not told to start work that cannot start. See Task nudges (#2468) [feat:task]
Removed
- BREAKING:
capabilities.advisor_size. It rendered anadvisorSizekey that Claude Code never read. A config that still sets it fails validation and namesadvisor_model, andllmenv doctorwarns about a staleadvisorSizein a renderedsettings.json(#2409) [fix:config]
Fixed
- The session-start wake-up pack now reaches the model in Claude Code. llmenv fetched it from ICM at every session start and then threw it away, because the output for
SessionStartwas suppressed along withSessionEnd. The wake-up call now also names the session's project, so a remote ICM server returns this project's context instead of whichever project its own working directory points at (#2251) [fix:adapter] - Failures of the hooks that run in the background (state folder, recall ledger, agent-config document, spawn and pipe errors) log at error level, so they show at the default log level. [fix:hook-run]
llmenv task startno longer refuses a task that's blocked on the task added right before it once that blocker is done.task addparents each new task under the previous one, so the block check was waiting on the blocked task itself and could never pass without--force(#2300) [fix:task]- On the ICM server host,
icm servenow always serves the store your ICM config names. Since ICM 0.10.64 it picks<git root>/.icm/memories.dbfrom its working directory, so a proxy that first started inside such a repo served that one project's database to every session on the host. llmenv now starts it in the filesystem root, which has no git root. See Memory backend (#2262) [fix:mcp] - An agent's own
icm_memory_recalloricm_wake_upcall with noprojectargument now searches every project instead of just preference memories. ICM names its default project filter after theicm serveworking directory, which was llmenv'sicm-servestate dir, a project nothing is stored under.icm servenow runs in/with inheritedGIT_DIR/GIT_WORK_TREEremoved, and llmenv refuses to start it with a relativeICM_DB. See Memory backend (#2313) [fix:mcp] - A
config.yamlwhosecapabilitiesblock set onlyoutput_stylesno longer loses them. The merge skipped that block as empty because the emptiness check left outoutput_styles[fix:config] capabilities.effort_levelnow reaches Opus 5.5 in Claude Code. Opus 5.5 ignores the top-leveleffortLevelin the user settings file, which is the file llmenv renders, so the setting (and slippage control'seffort_level) did nothing on the default model. llmenv now also writes it tomodelSettings. See Configuration (#2144) [fix:adapter]llmenv doctorshows theLifecycle hooks (claude_code):section again. Since v3.11.0 it compared the adapter's display nameclaude-codetoclaude_code, so the section never printed. [fix:doctor]- The task-tracker Stop hook now speaks up when a session has open tasks and nothing in progress. It used to list
wiptasks only, so an agent that added its steps and never started any got no reminder at all.llmenv task donealso notes when a task goes straight fromopentodone. Seetask(#2338) [fix:task] llmenv task addworks again afterllmenv task session start --new. With two sessions open it refused to guess, even for the session you just started. A session now remembers the Claude Code conversation that started or resumed it, andtask addandtask session finish|show|summarypick that one. Thesession startcheckpoint also says when an open session is yours from before a/clear. Seetask(#2365) [fix:task]llmenv exportnow starts the localmcp-proxywhen a bundle declares this host as the memory server. It only looked atconfig.yaml, so hooks pointed at a proxy nothing had started. See Memory backend (#2344) [fix:export]- Post-session consolidation actually runs now, on Claude Code's
SessionEnd. It waited for an event no adapter sends, soconsolidation.enabled: truedid nothing. Theclaude -pit runs no longer loads llmenv's hooks and MCP servers, and can't kick off consolidation again from its own session end. A bundle-declared memory entry's consolidation settings count too. See Configuration (#2355) [fix:consolidation] - A bundle-declared memory entry's
default_typeanddefault_importancenow tag stored memories. Onlyconfig.yamlentries applied them before. See Configuration (#2370) [fix:memory] - A removed
envpin no longer survives in.claude.json. When llmenv stopped writingCBM_ALLOWED_ROOTfor codebase-memory-mcp, the old value stayed in the server entry on every re-render, so the server kept refusing paths outside the project. A server entry llmenv owns now dropscommand,args,env,url,headers, andtimeoutthat the new render does not write. Keys Claude Code adds itself still survive. (#2376) [fix:adapter] llmenv memory pruneandauto_pruneno longer delete fresh medium-importance memories whenmemory.retentionis set. ICM's recall output has no record age or type, so the durations cannot apply; prune now refuses with an error and forgets nothing. See Commands (#2386) [fix:memory]- Post-session consolidation reads only the current project's memories, and skips a rule that matches one already stored. Before, it saw every project and stored near-identical high-importance rules again each session. Rules now go to the topic
llmenv-consolidation-<project>, so they surface for their own project; rules stored earlier stay underllmenv-consolidation. See Configuration (#2387) [fix:consolidation] - Claude Code's read-once, read-before-edit, and repeat-call hooks forget what a session has seen after
/clearor a compaction. Before, read-once indenymode blocked the re-read thatCLAUDE.mdasks for, and read-before-edit let an edit through on the strength of a read the model no longer had. (#2381) [fix:hook] llmenv doctorno longer recommendsCLAUDE_AUTOCOMPACT_PCT_OVERRIDEwhenautoCompactEnabledisfalse, and it names theautoCompactWindowthat the percentage applies to (#2345) [fix:doctor]llmenv doctorwarns whenCLAUDE_AUTOCOMPACT_PCT_OVERRIDEis above 100, and reports a state folder that it cannot resolve. [fix:doctor]- The Stop reminder for a session with open tasks no longer offers
llmenv task session finish, which now refuses until the tasks are finished (#2416) [fix:task] llmenv task session finishand the session resume commands no longer report a corrupt or unreadable session file as "no session found". They name the file and the cause (#2424) [fix:task]- Background work is no longer lost when a child dies or ICM is down. Consolidation, the memory store of a web fetch, the transcript records, and the codebase-memory index write a checkpoint, and the next session start runs unfinished jobs again.
llmenv doctorlists what is left. A request id keeps a resumed or retried store from writing the same memory twice. See Troubleshooting (#2396, #2397) [feat:hook] - A detached background job keeps its input when the parent fails to pipe all of it: the job reads the checkpoint instead of losing the event. A checkpoint folder or file that cannot be read shows in
llmenv doctor. [fix:hook] - A marketplace plugin whose
sourceis{"source": "github", "repo": "owner/name"}with nourlnow syncs, and itsrefselects the branch or tag. Before, llmenv skipped it as "not found in marketplace manifest". See Plugins (#2440) [fix:plugins] llmenv plugin-syncre-clones a pinned plugin payload, so a changedrefin the marketplace manifest takes effect. Before, it kept the old checkout and reported success (#2442) [fix:plugins]- A
refon aurlplugin source is honored. Before, it was ignored (#2441) [fix:plugins] - A codebase-memory tool error is no longer taken for success. An MCP reply with
isError: truefails the call, so a failed ICM store is not marked as stored and is retried. [fix:hook-run] llmenv memory prunechecksretentionon the merged active memory entry, so a bundle-declared entry counts, and a failed auto-prune prints a warning. [fix:memory]features.codebase_memory[].allowed_rootsentries are checked with the same$NAMEand${NAME}rules that expand them, so$or${alone failsllmenv validate. An entry that cannot expand, and an entry that is not a folder, are reported inllmenv doctorand the SessionStart notice. Theindex_repositoryguard denies when it cannot read its inputs, and its deny text says when the recorded roots could not be read. [fix:config]- A corrupt
settings.jsonis moved tosettings.json.corruptbefore llmenv writes a new one, instead of being overwritten. A skipped skill symlink, a plugin without a manifest, an unwritable hooks record, and a bundle hook path that cannot be moved to the cache folder now print a warning at the default log level. [fix:adapter] llmenv memory pruneno longer takes a line inside a memory's text for a record header, so stored text cannot choose which other memory is forgotten. [fix:memory]- A bundle hook with an inline shell command no longer triggers the "path ... is not in the bundle files" warning because of a
/in jq//,2>/dev/null, or a URL. The warning now fires for script paths only, names the bundle and the path, and prints once for each bundle and path. A token such as2>/dev/nullis also no longer rewritten into a cache path. See Troubleshooting (#2466) [fix:adapter] - Network scopes now match on macOS, where
arpprints a MAC with the leading zero of an octet dropped (1c:b:8b:e4:5f:94). llmenv pads both the detected and the configuredgateway_macbefore it compares them (#2487) [fix:scope] - Network scopes now match on
match.cidr(any local interface address inside the IPv4 or IPv6 block) andmatch.ssid(the Wi-Fi name; on Linux it comes from the activenmcliconnection). Loopback and link-local addresses never match acidr. Before, both fields were accepted and silently ignored, so a scope that used only them never fired. A scope that sets several fields needs all of them to match.llmenv doctorwarns when the platform hides the SSID, as macOS 15 does. See Network match fields (#1051) [feat:scope] - The session-start MCP health check no longer skips the memory server when another
mcpentry fails to resolve. It also reports a probe that crashed, a closed pipe, and a health check that cannot run, and it bounds the reason it shows the agent (#2358) [fix:hook]
Security
- The MCP HTTP client follows no redirect. Before, it did when no header was set, and a redirect could reach an address that the SSRF check had not approved. [fix:hook-run]
- A session id that is not a plain name is rejected by the
llmenv task sessioncommands, so../xcannot reach a file outside the session folder. [fix:task] - Bidirectional-override and zero-width characters in text from files or the network are escaped before they reach the terminal. [fix:util]
- The MCP HTTP client refuses a plain
http://URL that resolves to a public address, so a memory payload or a header token never crosses the internet in clear text. Loopback, private, and Tailscale addresses still work overhttp://. See MCP security (#2476) [fix:mcp] llmenv export,regenerate, anddoctornow refuse anhttp://MCP URL or memoryhost:address that is a public IP, and say to usehttps://. Before, the agent connected in clear text, with its bearer headers, to an address that llmenv's own client refused.doctoralso warns when anhttp://hostname resolves to a public address. See MCP (#2483) [fix:mcp]- The MCP client's address check now sees an IPv4 address wrapped in NAT64 (
64:ff9b::/96), 6to4 (2002::/16), or IPv4-compatible IPv6 form, so a link-local or metadata address behind one is blocked. Its URL errors no longer print credentials or a token from the query string (#2476) [fix:mcp]
[3.11.2] - 2026-09-27
This is a bugfix sprint: llmenv had several places where it trusted stale or wrong state without checking it.
A hardcoded model id for the anthropic-api consolidation backend didn't exist, so it failed every call. A shell hook and a session-log event both trusted an environment inherited from a different project instead of checking the current directory. A leading ~ in a config path or in LLMENV_CONFIG_DIR/LLMENV_STATE_DIR silently resolved to the wrong directory in release builds. None of these failed loudly — they just did the wrong thing.
Alongside those fixes, this release tightens llmenv's integration with codebase-memory-mcp 0.11.0 (tool tiers, a version-floor check in llmenv doctor, and a SessionStart memory recall that's now capped in size and ordered most-specific-first), trims duplicate bytes out of ICM's scope-context storage, and cleans up a disabled plugin's leftover hooks instead of leaving them firing forever.
Added
llmenv doctorwarns when the installed codebase-memory-mcp is older than 0.11.0, the release llmenv's tool tiers and guidance assume. Seedoctor
Fixed
- The
anthropic-apiconsolidation backend defaulted to a model id that does not exist (claude-sonnet-5-20250624), so every call failed unlessANTHROPIC_MODELwas set. It now defaults toclaude-sonnet-5, and anANTHROPIC_MODELalias such asopus(valid for Claude Code, rejected by the Messages API) is ignored with a warning.llmenv doctorno longer claims a stale default forCLAUDE_CODE_SUBAGENT_MODEL. See Post-session consolidation - codebase-memory-mcp 0.11.0's new read-only tools (
get_file_outline,compare_graphs) no longer prompt on every call, andindex_repositorywithpersistence: trueis denied, so a model can no longer write a.codebase-memory/graph.db.zstartifact into a repo unprompted. See Theindex_repositoryname guard - The
TurnStartmemory recall is now capped at 8,000 bytes of whole records and ordered most specific first (project tags, bundles, broader scopes, then the unfiltered recall). Claude Code was saving the 10-35 KB output to a file and showing the model only a 2 KB preview of the least specific memories. See Lifecycle hooks Config::loadnow expands a leading~/~userin the config path itself instead of relying on adebug_assertthe release build compiled out — a release build previously accepted a tilde-prefixed path silently and failed later with a confusing "failed to read config file" error instead of resolving it. Only~itself goes through string handling; the rest of the path joins back on as raw bytes, so a non-UTF-8 path is never mangled either way.LLMENV_CONFIG_DIR/LLMENV_STATE_DIRhad the identical gap — a tilde-prefixed override silently resolved to a literal./~/...directory relative to cwd — and are fixed the same way. (#1910)- The shell hook's (
llmenv hook zsh|bash) guard against redundant re-renders now compares$PWDagainst the project root it last resolved for, instead of only checking that$LLMENV_STATE_DIRis set. A child shell forked into a different project directory — a tmux/zellij pane, or a tool like herdr that forks panes off a parent shell — kept the parent's config, tags, and cache path indefinitely instead of picking up its own project's scope. (#1895) - The scope-header session-log event now sanitizes the project name the same way tags and bundles already are: control characters are escaped and whitespace runs collapse to
_. A project name with a control character or embedded space could previously rewrite terminal output or inject a fakellmenv-tag:/llmenv-bundle:token into content ICM's FTS index searches. llmenv's own internaltracinglog messages get the same control-character escaping now, for the same reason. See Finding a session later (#1911) - Scope-context memory now stores only the tags/bundles/project fields ICM needs, instead of the same record's byte-identical instruction paragraph every project's SessionEnd store repeated. The instruction text is still injected once via SessionStart, so agents see it the same as before — recalling scope context across several projects no longer pays for that paragraph once per project, on every turn. (#1792)
- Disabling or removing a plugin now purges a self-registered hook it wrote into
settings.json, instead of leaving it behind forever (still firing on everySessionStart). Only covers a hook traceable back to that plugin's own resolved install directory — a hook a plugin registers somewhere else entirely is a known, tracked gap (#1932). See Materialize. (#1793)
[3.11.1] - 2026-08-25
One change since 3.11.0, and it's a security-posture one: llmenv stops pinning two of codebase-memory-mcp's launch defaults, CBM_CACHE_DIR and CBM_ALLOWED_ROOT, leaving scope control to the tool's own defaults and whoever configures it.
Security
- llmenv no longer overrides two of
codebase-memory-mcp's defaults when launching it:CBM_CACHE_DIRis only set whencodebase_memory[].index_pathis explicitly configured (previously defaulted to<state_dir>/codebase-memory, redundant since codebase-memory-mcp's own default cache is already shared per account), andCBM_ALLOWED_ROOTis no longer set at all.CBM_ALLOWED_ROOTpreviously pinned the tool to the project root soindex_repositorycouldn't be steered outside it (#365) — that restriction is removed by explicit user direction; restricting the tool's scope is now the end user's call via codebase-memory-mcp's own config, not llmenv's default. This is a real security-posture change, not just cleanup: without configuring a restriction yourself,codebase-memory-mcpwill act on whatever path it's asked to. See Configuration (#1493, #1495)
[3.11.0] - 2026-08-17
The through-line for this release is configuration llmenv accepted but never acted on. Four features.slippage layers — three of them defaulting to true — were wired into the config schema and nothing else, so turning them on changed nothing; all four run now, along with two opt-in transcript-scan layers (#317). Most of the opencode adapter work is the same shape: a permission rule could name a key opencode has no equivalent for, overlap another pattern in a way that quietly reversed it, or carry stray whitespace that stopped it matching any tool — each rendered a rule that did nothing, and each is now either a hard render failure or a visible warning (#1328, #1344, #1345). Tags, bundles, and marketplace entries dropped for being malformed used to report at a log level the default filter discards, so a bundle that never fired left no trace anywhere; they say so out loud now (#1345). And llmenv prune/llmenv regenerate exit non-zero when they fail, instead of exiting 0 over a printed warning (#1346).
llmenv doctor gained two reports: the installed versions of the external tools llmenv wires in but doesn't ship, and which Claude Code lifecycle hooks are actually wired in the active scope (#1185, #741). opencode's built-in todo list now feeds the llmenv task tracker, the way Claude Code's task tools have since 3.6.0 (#1304).
Security work concentrates on races and silent denies: directory tree walks descend by file descriptor instead of re-resolving a path the caller already checked (#1066), an index_repository call that overrides the project key is denied so one call can't replace an unrelated repository's index (#1331), and a blocked tool call finally tells Claude why — the deny used a field name Claude Code doesn't read, so every llmenv deny had been reaching the model with no reason attached. Separately, the llmenv crate's library surface is much narrower: ~300 items that were pub only because nothing had checked are crate-private now (#1314).
Added
-
features.slippage's remaining layers now do something.rule_reinjection,read_before_edit,self_critique, andmetricswere accepted by config — three of them defaulting totrue— but had no implementation, so enabling them changed nothing. All four are wired now, along with the two opt-in transcript-scan layers (explain_before_act,answer_before_act). See Configuration (#317) -
opencode's built-in todo list now feeds the
llmenv tasktracker, the way Claude Code's task tools have since 3.6.0. opencode'stodowritereplaces the whole list on every call, so llmenv reconciles it against the tracker by task title: new titles are added,in_progressstarts a task,completedfinishes it, and a task that disappears from the list is left open rather than guessed at. Gated on the sameblock_engine_task_toolsopt-out. See Commands (#1304) -
llmenv doctornow reports the installed version of the external tools llmenv wires in but doesn't ship (icm,codebase-memory-mcp) and how to update each. The two differ in what's possible:icm upgrade --applyinstalls its own update, whilecodebase-memory-mcp updateonly prints the install command for your machine — so llmenv reports that one rather than claiming it updates anything. No network check is made, so the report says what you have, not whether it's current. See Commands (#1185) -
llmenv doctornow lists which lifecycle hooks (session_start,session_end,turn_start,stop) are wired for Claude Code in the active scope, and what would enable any that aren't. There was previously no way to confirm hook wiring from inside llmenv short of reading the generatedsettings.jsonby hand.turn_start's gate is read straight from the generator; the rest are held in step by a test that renderssettings.jsonfor each combination and fails if the report disagrees. See Commands (#741)
Security
-
Directory tree walks now descend by file descriptor instead of by path. Copying skill sources, folding a stranded transcript directory into the durable store, and pruning empty directories each resolved a path, checked it, then handed the same path back to the kernel — so a directory swapped for a symlink in between redirected the operation, including one walk that removes directories. Each walk now opens its root once with
O_NOFOLLOWand reaches every entry through that descriptor, so there is no second resolution to race. This also retires the dev/inode tripwire added in 3.10.0 for skill copies: the race it detected can no longer happen. Same-user hardening, not a privilege boundary. (#1066) -
llmenv now denies
index_repositorycalls that carry anamewhen codebase-memory-mcp is active. The name overrides the project key the index is written under, and codebase-memory-mcp doesn't check whether that key already belongs to another repository — one call could replace an unrelated project's index with the current repo's data, with no prompt, since the tool is auto-allowed so theSessionStartauto-index doesn't need approval. Calls withoutname, llmenv's own included, are unaffected. See MCP (#1331) -
A blocked tool call now tells Claude why. The deny llmenv writes on
PreToolUseused the field namedeniedReason, which Claude Code doesn't read — the call was blocked but no reason reached the model, so it had nothing to go on but a retry. The field is nowpermissionDecisionReason. Affects every llmenv deny: read-once, the task-tracker redirect, and the newindex_repositoryguard (#1331) -
opencode's hook bridge now passes a tool call's actual arguments.
tool.execute.beforewas reading them from the wrong parameter, sotool_inputwas an empty object on every PreToolUse call, andtool.execute.aftersent them as a JSON string rather than an object and dropped the tool result entirely. Any opencode hook that inspects tool arguments was seeing nothing (#1331)
Changed
-
Claude Code's session start now runs one
llmenvprocess instead of two. The drift check was registered as its ownSessionStarthook alongsidehook-run session_start, so both fired and each re-parsed the config; the check now runs insidehook-run session_start. Behaviour is unchanged — the same restart hint appears on drift — andllmenv check-staleremains available to run directly. See Engines (#741) -
The
llmenvcrate's library surface is substantially narrower: ~300 items that werepubonly because nothing had checked are nowpub(crate)or private. The crate is published so the binary can be installed from crates.io, and its module tree exists to servemain.rsand the tests — it has never been a supported API, and the crate docs now say so explicitly. The fourllmenv-*support crates are unaffected: they are real published libraries and their public API is excluded from this narrowing. (#1314)
Fixed
- A pattern- or path-scoped permission rule targeting one of opencode's action-only keys (
TodoWrite,WebFetch,WebSearch, and the native-onlyquestion/doom_loop) now fails the opencode render with an error naming the rule, instead of writing a value opencode's schema rejects. opencode discards the entire config file when any one key fails to decode and reports nothing, so a single scopedWebFetchrule silently voided every MCP server, LSP entry, and permission rule in the generatedopencode.json. As with any adapter failure, other engines still regenerate and the previousopencode.jsonis left in place. See Engines (#1328) - Two permission patterns for the same opencode tool that match some of the same inputs now fail the opencode render when the later-sorting one isn't the narrower of the two, instead of quietly reversing a rule. opencode applies the last matching rule in config key order and llmenv emits the pattern map sorted, so a
denywritten as* --force*paired with anallowongit *resolved toallowforgit push --force— the deny never took effect. A wildcard baseline plus a narrower override still renders as before. See Engines (#1328) - A
native_permissions.opencoderule whose tool name carries stray whitespace (webfetch (…)) is now trimmed, and one with whitespace inside the name is rejected. Either used to render a permission key opencode never matches against a tool, so the rule silently had no effect. See Engines (#1328) - The opencode permission ordering check now spans permission keys, not just patterns within one key. opencode wildcard-matches the key against the tool name too, so a
native_permissions.opencoderule keyed*applies to every tool — and a wildcarddenycould lose to a per-toolallowthat sorts after it. A bare*deny-all baseline alongside per-tool rules is still accepted. See Engines (#1344) - A permission rule naming a tool an engine has no equivalent for is no longer silently dropped without a trace, but it doesn't warn on every
export/regenerateeither — the rule still applies to the engines that do have the tool, so it's working config.llmenv doctornames the engines that drop it, and Engines documents the mapping and its gaps per adapter. A tool name that isn't in the neutral vocabulary at all is a config typo and is reported byexport/regenerate— once per name, rather than once per rule per adapter. The neutral vocabulary and its per-engine mappings now live in one table, so opencode's and crush's no longer drift apart.Skillmaps to opencode'sskillkey instead of being dropped, since it has an exact equivalent. (#1345, #1371) - A tag or bundle name dropped for failing the charset/length/count rules is now reported with a visible
warning:line naming it. Like the opencode case above, it previously went to a log level the default filter discards, so a tag that never activated any bundle looked like a bundle that simply didn't fire. Applies to.llmenv.yaml,config.yaml's scopes, and$LLMENV_EXTRA_TAGS. See Configuration (#1345) - A marketplace entry skipped for a missing or malformed
name/source/url/packagefield is now reported visibly instead of at a discarded log level, so a plugin that never becomes available says why. (#1345) llmenv prunenow exits non-zero when an entry could not be removed, cache entries and plugin cache entries alike. The per-entry failures were already printed, but the command still exited 0, so a scripted prune moved on with the cache still occupied. See Commands (#1346, #1372)llmenv regeneratenow exits non-zero when any engine's config could not be regenerated, naming the engines that failed. It previously exited 0 as long as one other engine succeeded, so a rejected config scrolled past as a warning above a success message while that engine kept its stale config. Every other engine is still regenerated.llmenv exportdeliberately keeps exiting 0 — it runs on every prompt — but now names the engines whose output is missing. See Commands (#1346)llmenv pruneno longer stops at the first cache entry it can't remove. One undeletable entry — most often a directory another tool left without its owner write bit — aborted the whole pass with nothing butPermission denied (os error 13), so nothing was collected and every later run failed at the same place. Prune reports the entry with its path, keeps going, restores write access to directories it created itself and retries, and treats anything left behind as a failure of the command.doctor --gchad the same fault and gets the same fix. See Commands (#1372)- Every filesystem error in the cache prune and GC walks now names the path it happened on, instead of surfacing a bare errno with nothing to act on. (#1372)
TMPDIR(andTMP/TEMP/CLAUDE_CODE_TMPDIR) now point into the durable state directory instead of the per-hash cache folder. That folder is deleted byllmenv pruneand replaced whenever a config edit mints a new shape hash, which left every already-running shell withTMPDIRpointing at nothing — silently, until something needed a temp file, and then as somebody else's error: a signedgit commit, for instance, fails withcould not create temporary file: No such file or directoryand never mentions llmenv. The state directory is never pruned, so the path stays valid for the life of the shell. See Environment variables (#1379)- The forward-merge cascade now chains through each release branch —
release/3.xintorelease/4.x, thenrelease/4.xintomain— instead of merging the pushed branch into every target independently. Merging intomaindirectly meantmainnever received the intermediate branch's own commits from the cascade, and left the merge bases tangled so the next merge re-conflicted over the same files. It also resolves the version-manifest conflict that every forward-merge hits — two release lines always carry different versions, soCargo.toml,Cargo.lock, and the fourcrates/*/Cargo.tomlcollided on every run and halted the cascade for a human. The target branch keeps its own version, but only after checking that the source changed nothing but version literals in that file, so a dependency edit can never be dropped silently. Only affects the release automation, not the shipped binary. (#1380, #1381)
[3.10.0] - 2026-08-13
The task tracker grows up: task add now chains onto the last task in the session by default instead of creating an orphan (--no-parent opts out; #929), task edit mutates a task in place instead of delete-and-recreate (#930), and task session summary rolls a session's tasks and notes into one artifact (#931).
Most of this release is engine-rendering correctness fixes, concentrated in Crush and opencode: several permissions.allow/deny/ask rules were silently mapping to keys those engines don't have and so had no effect, Claude Code's own allow/deny conflicts now resolve deny-wins, content scopes now participate in precedence, and a null in any native.<engine> block now deletes the key on every write path instead of just some. New capabilities landed alongside the fixes: output_styles for Claude Code, a tiered permission policy for codebase-memory-mcp tools, and native_default_models for Crush's per-role model extras.
Security-wise, several more symlink gaps in skill-source copying and state-directory inheritance are closed — a symlinked skill source, output path, or SKILL.md is now rejected instead of followed (#1337, #1341).
Finally, llmenv export/the shell-hook flow is deprecated in favor of llmenv launch <engine>, landing in v4.0.0 — export keeps working until then (#1056).
Added
- Opt-in cache hit/miss telemetry for the content-hash materialize cache, the merge-signature cache, the read-once dedup cache, and the plugin marketplace cache — one
[LLMENV_CACHE] <name> <hit|miss> <duration>msstderr line per cache lookup, gated behind the sameLLMENV_TRACE_TIMINGvar that already gates hook-run's per-phase timing markers. See Troubleshooting (#1260) - Opt-in per-MCP-call timing — one
[LLMENV_MCP_CALL] <tool> <duration>usstderr line per MCP tool call (icm_wake_up,icm_memory_recall,icm_memory_store, etc.), gated behind the sameLLMENV_TRACE_TIMINGvar. See Troubleshooting (#1259) - Opt-in memory-recall/context-size telemetry for
TurnStart— one[LLMENV_CONTEXT] recall_entries=N recall_bytes=N injected_entries=N injected_bytes=N advisory_stripped=Nstderr line, gated behind the sameLLMENV_TRACE_TIMINGvar. See Troubleshooting (#1261) task add --no-parentforces a top-level task, overriding the new implicit-chain default (see Changed below). See Commands (#929)llmenv task edit <id>mutates an existing task in place — retitle it, re-parent or detach it, add/removeblocked_ondependencies, or add/delete a note — instead of requiring delete-and-recreate. See Commands (#930)llmenv task session summary [<id>] [--format json]rolls up a session's tasks, notes, and states into one artifact — e.g. for a memory write or a status report at the end of a session. See Commands (#931)- The active-tag count is now also capped across every source combined (network/host/user/content scopes,
.llmenv.yaml,$LLMENV_EXTRA_TAGS), not just per source — each could individually stay within its own 64-tag cap while the union still ballooned to several hundred, and every active tag becomes one recall query per turn. See Concepts (#1041) features.task_tracker.block_engine_task_tools(defaulttrue) opts out of the #985 auto-injectedPreToolUseblock on Claude Code's nativeTaskCreate/TaskList/TaskUpdatetools, while keeping the rest of the task tracker (CLAUDE.md fragment, lifecycle reminders) active — for projects that genuinely use those native tools for multi-agent teammate coordination rather than solo step tracking. See Configuration (#980)native_default_models.<engine>deep-merges onto the rendered per-roledefault_modelsblock, giving Crush's per-role extras (reasoning_effort,think,max_tokens) a supported route intocrush.json— previously there was no way to set them, sincemodelsis a modeled keynative.crushrejects andnative_model_providersmerges onto theprovidersblock, notmodels. See Configuration (#1031)- Claude Code now renders a tiered allow/ask permission policy for
mcp__codebase-memory-mcp__*tools, mirroring the ICM memory MCP's tiering — read-only/query tools and non-destructive mutations (index_repository,ingest_traces) are pre-approved by default, and the two genuinely destructive tools (delete_project, andmanage_adr— an unversioned overwrite of the project's ADR with no history) ask. Every codebase-memory-mcp tool call previously prompted individually. Overridable per tier via the newcodebase_memory[].mcp_permissions, same shape asfeatures.memory[].mcp_permissions. Two known caveats documented alongside this: the pre-approved read tools aren't workspace-scoped (they can read any indexed project, not just the active one), andindex_repository'snameoverride can clobber a different project's index without a prompt (tracked separately, #1331). See Configuration (#1323) output_styles, a new capability for declaring Claude Code output styles (system-prompt tone/role/format changes, distinct fromCLAUDE.md-style project knowledge) — materialized tooutput-styles/<name>.mdwith theoutputStylesettings key set when exactly one is active. Every other engine (Crush, opencode) has no equivalent, so the same content renders as a generated skill instead, automatically — no config-author-side fallback logic needed.llmenv doctorflagsforce_for_pluginset outside a plugin bundle, since Claude Code only honors it for plugin-shipped styles. A style name colliding with a first-class, reserved, or plugin-projected skill name is rejected instead of silently overwriting (Crush) or being dropped by (opencode) that skill. See Configuration (#1130, #1333)
Changed
- Behavior change:
llmenv task addwithout--parentno longer creates a parentless task — it now defaults to the most recently created task in the same session, so a run of plaintask adds forms an ordered chain by default. The chain never crosses sessions. Use the new--no-parentflag for a deliberate top-level task. See Commands (#929) - Behavior change: the default cache-hashing mode (
normal) now nests materialized folders by major version only (<adapter>/<major>/<shape>), notmajor.minor— a minor/patch upgrade reuses the existing folder instead of minting a new one and orphaning the old tree, since the manifest dotfile's content hash already drives drift detection and reconciliation regardless of version. Only a major version bump mints a new folder now. The oldmajor.minorfolders become unreferenced garbage, cleaned up by the existing age-basedgc/pruneretention — no dedicated one-time sweep was added, since that retention already covers it. Setcache.hashing: strictfor the old per-minor-version isolation, orloosefor none. See Concepts (#1263)
Deprecated
llmenv export/the shell-hook flow is deprecated, superseded byllmenv launch <engine>(#1056), a supervised, ambient replacement landing in v4.0.0.exportkeeps working through v4.0.0 — this is advance notice, not a removal. See #1056 for the replacement's design (no docs page yet; it hasn't landed).
Fixed
llmenv regenerateno longer leaves a 0-byteCLAUDE.mdin the materialized Claude Code folder when there is noAGENTS.md/rules content and no fragment applies — the file is omitted entirely, and a copy left by an earlier render is cleaned up rather than going stale. See Engines (#1262)- A
nullin the catch-allnative.<engine>block now deletes the key from the generatedsettings.jsonso the engine applies its own default, instead of emitting an explicit JSONnull. This only ever showed up on keys llmenv renders itself (autoMemoryEnabled,effortLevel,advisorSize), which are emitted before the overlay precisely sonativecan override them; anullon any other key was already dropped. See Configuration (#1264) - The opencode adapter no longer leaves a 0-byte
AGENTS.mdin the materialized folder when there is no rules content — same fix as #1262, applied to the opencode adapter. See Engines (#1269) - The null-deletes-the-key fix from #1264 now also covers the other three catch-all write paths that could still emit an explicit JSON
null:.claude.json(native_mcp.claude_code),crush.json(native.crush), andopencode.json(native.opencode)..claude.jsonmattered most — it is persistent user state, not a rebuildable cache folder, so a stray null there survived across renders instead of being rebuilt away. See Configuration (#1270) llmenv check-stale --auto-fixnow reports the same dead-config diagnosticsexport/regeneratealready do (deadnative_*.<engine>keys, Claude-only permission patterns under opencode) instead of silently re-materializing without them — the only remaining materialize path that skipped this call. See Commands (#1075)contentscopes now participate in scope-precedence resolution (ranked betweenuserandproject) and inllmenv status scopes's listing — both previously omittedcontententirely, so a bundle firing only via acontentscope always lost every scalar capability conflict regardless of match specificity, and a configured content scope never showed up as active/orphaned instatus scopes. See Configuration (#845)- A marketplace's
.claude-plugin/marketplace.jsonentry with an npm-source object ({"source": "npm", "package": ..., "version": ...}) is no longer silently dropped while parsing —llmenv plugin-sync/regenerateused to report the plugin as "not found in marketplace manifest" even though the entry was present, just in a source shape llmenv didn't parse. llmenv doesn't clone npm sources itself — the target engine's own npm-install mechanism (e.g. Claude Code's/plugin install) resolves them directly from the cloned marketplace manifest. (#1014) - A bundle's
bundle.yamlcan now declaremodel_providersanddefault_models— both were missing from the allowed top-level key list and hard-rejected as unknown keys, despiteCapabilities's own doc comments documenting bundle-level support for both. Found while addingnative_default_modelsfor #1031. See Configuration (#1031) - A
permissions.allowrule with apatternorpathsscope (e.g.Bash+git status:*) is no longer rendered to Crush'sallowed_toolsat all, instead of thetool(pattern)/tool(path)entry it used to produce. Source-verified against Crush's own matching code:allowed_toolsonly ever compares the bare tool name or a fixedtool:actionstring, never a command or path, so the scoped entry was already silently inert. Dropping it keeps Crush's deny-by-default behavior (the tool still prompts) instead of substituting a wider, unscoped grant for a narrower one that was never enforceable in the first place. See Engines (#1306) - An unscoped
permissions.allowrule (e.g.{ tool: Read }) now actually grants what it says for Crush: the neutral tool name is translated to Crush's own identifier (Read->view,WebFetch->fetch, etc.) before rendering toallowed_tools. Previously the neutral name was rendered verbatim, which never matched anything in real Crush — its tool names are lowercase and not always a simple case change (source-verified againstcharmbracelet/crush's tool registry). A neutral tool with no Crush equivalent (Task,NotebookEdit, ...) is dropped (logged, not silent) rather than rendering a name Crush ignores. Reviewpermissions.allow/deny/askbefore upgrading if you use Crush: an unscoped allow rule that previously matched nothing now grants that tool for real, and a tool named in bothallowanddeny/askis correctly withheld (Crush has nodenied_toolsof its own, so this is now cross-checked on the neutral side) rather than the two rules coexisting as before.Edit/MultiEditallow also implies file creation under Crush (itsedit/multiedittools create missing files and parent directories), unlike Claude Code'sEdit, which requires an existing path. See Engines (#1321) - A tool listed in both
permissions.allowandpermissions.deny(oraskanddeny) for Claude Code no longer lands in both permission buckets of the generatedsettings.json. Deny now wins outright for a directly conflicting rule, matching Claude Code's own deny > ask > allow resolution order — previously an existing property test's "buckets never overlap" invariant could be violated for this specific case (a plain rule, no native override involved), which meant one of the two conflicting entries could silently never fire. (#1322) - A
permissions.allow/ask/denyrule forWrite,MultiEdit, orLSnow maps to opencode's real permission keys (edit,edit, andlistrespectively) instead ofwrite/multiedit/ls, which don't exist in opencode's schema — source-verified against opencode's ownpermission.tsconfig schema. Those three rules previously rendered a key opencode's permission check would never match, so they had no effect. A tool name with no confirmed opencode equivalent (e.g.NotebookEdit) is now dropped with a logged warning instead of guessing at a lowercase key — same principle as the Crush fixes above. (#1326)
Security
- A skill source directory (first-class
skills:entry, or one projected from a plugin's ownskills/directory) that is already a symlink at the time llmenv checks it is now rejected instead of followed. A symlink swapped in after that check and before the copy is a separate race, not fully closed by this change (see the next entry) — needs a concurrent local write into a config-author-controlled path, not a privilege boundary. (#1337) - Several more symlink gaps found while reviewing the fix above: a skill source path that changes filesystem identity (checked via dev/inode comparison, Unix only) during its copy now discards the copy and fails — a best-effort tripwire against a swap left in place, not a guarantee (an attacker who can observe copy completion and restore the original directory first is not caught; full closure needs
openat-style filesystem-descriptor-relative I/O across every tree walk in the codebase, tracked separately as #1066). A symlink already present at a materialized output path or file llmenv owns (e.g.skills/<name>and everything under it) is now rejected instead of followed — previouslycreate_dir_owner_only/write_owner_onlywould write through it. A symlinkedSKILL.mdat a skill's own root is now a hard error instead of being silently dropped from the copy (which used to produce a confusing "missing SKILL.md" failure later, and missed a case-insensitive match on filesystems like macOS's default); any other symlinked file is still skipped, now with a visible warning instead of a debug-only log line.llmenv's own state-directory inheritance (history.jsonl, the MCP needs-auth cache) applies the same never-follow-a-symlink check on both the source and destination sides. (#1341)
[3.9.0] - 2026-08-10
All bug fixes, no new features. The inherited-session-state work from 3.8.0
continues: Claude Code's own session-logs/ now survives a cache-folder
change the same way /resume history and OAuth tokens already did (#1064),
and the macOS keychain write behind that OAuth login no longer passes the
secret through a child process's argv, closing a ps-visible exposure
window (#1061). Wraps up the #1139 memory-classification review: hook-run
now names the real cause — tag-inactive, no content directory, or a
rejected bundle name — instead of a generic "no memory backend" message
(#1140, #1142), and llmenv status read-once distinguishes an unreadable
cache dir from a genuinely empty one (#1180). Also fixes a symlink-hardening
gap in transcript inheritance (#1065), a byte-vs-character truncation panic
in session consolidation (#1166), unescaped control characters in
doctor/export's printed config strings (#1076), and a
features.repeat_detect Stop-reminder that repeated forever instead of
backing off (#1247).
Fixed
no memory backend active for this scopeno longer misreports a declaredfeatures.memoryentry as "nothing declared" when the entry is simply gated on awhentag that isn't active right now — hook-run now reports that case distinctly, naming the affectedserver_host(s).llmenv doctor --all'sdisable_bundlesorphan check had the same "does it exist" instead of "is it tag-active" bug, including mis-attributing a disabled bundle as the cause when its only memory entry was itself tag-inactive — both are fixed the same way. See Configuration (#1140)- The "bundle(s) X fired but have no content directory" hook-run message no longer claims that as the sole cause when a firing bundle was actually skipped for a rejected/unsafe name instead — the wording now covers both. (#1142)
llmenv status read-oncenow shows(unreadable)instead of(none)when the ReadOnce cache directory exists but can't be read (e.g. a permission error) — the two used to look identical.llmenv setup's project-config scan anddoctor's version-skew check now log a read failure there instead of silently showing nothing. (#1180)- Folding a stranded transcript directory into the durable state store (
export's inherit step) no longer writes through a same-user-planted symlink. The copy fallback used whenrenamefails cross-device wrote through a symlink at the destination instead of replacing it, the newest-file comparison read a symlink's target's mtime instead of its own, and creating the destination directory silently succeeded when it already resolved through a symlink to somewhere else. Same-user hardening, not reachable privilege escalation. (#1065) - Session consolidation no longer panics when the LLM backend returns a rule over 500 characters containing non-ASCII text. The truncation sliced by byte offset while the length bound is documented in characters, so a multi-byte character straddling that offset crashed the parse instead of truncating cleanly. (#1166)
llmenv doctor/exportno longer print config-derived strings (native_permissions.*keys, permission tool names and patterns, bundle/marketplace names, hook matchers) to a terminal without escaping control characters. Since #1072 widened validation to the merged manifest, these can arrive from a shared or marketplacebundle.yaml— a key or pattern containing an ANSI escape or a carriage return could rewrite or hide surrounding terminal output. (#1076)- Stop losing Claude Code's own internal session logs (
session-logs/, one file per calendar day) on every cache-folder change — the same silent-data-loss shape #1059 fixed for/resumetranscripts, now covering this directory too. See Configuration (#1064) features.repeat_detect's task-tracker Stop reminder no longer repeats forever. It already escalated to allmenv task waitpointer once pastthresholdidentical repeats, but kept repeating that same message every turn after — moot advice when none of the listed tasks belong to the current session (e.g. two sessions sharing a project), which had no legitimate way to stop the loop. Pastthreshold * 3repeats it now goes silent instead. See Configuration (#1247)- The macOS OAuth keychain write no longer passes the token through a child process's argv. It shelled out to
security add-generic-password -w <blob>, which is readable viapsby other processes of the same user for the child's lifetime; it now calls the Security framework directly with the secret as an in-memory buffer. No new access was ever granted by the old path (anything running as this user could already read the keychain item outright) — this closes a process-accounting/monitoring exposure, not a privilege-escalation path. (#1061)
[3.8.0] - 2026-08-10
Mostly a hardening release. A long directory-permission series locks down
nearly every cache/state directory llmenv creates to owner-only (0700),
and a parallel pass on the memory and consolidation lifecycle closes a
broadcast-kill hazard, several TOCTOU windows, and failure paths that used
to vanish into /dev/null or a debug-only log level instead of surfacing.
On the feature side: Claude Code's OAuth login (and third-party MCP server
logins) now survive a cache-folder change, capabilities.permissions.preset: safe-readonly ships ready-made allow rules for read-only CLI tools,
features.cd_guard warns when a Bash command resets the shell's working
directory, and the task tracker gains --force, blocked_on enforcement,
and current-task lookups. Bundles disagreeing on the same scalar capability
at the same precedence now hard-error instead of silently picking a winner.
Added
- Inherit the Claude Code OAuth token across cache folders, so a config edit or version bump no longer produces a login prompt. Previously only the account identity (
oauthAccount) was inherited — the folder knew who you were but not that you were logged in. Covers both stores:.credentials.jsonon Linux/WSL and the macOS keychain item, whose service name embeds a hash of the config-dir path and so is no more stable across folders than a file. A live cached token is never overwritten by a stale folder's, and a folder's own token is never replaced.llmenv logincaptures the token too;llmenv doctorreports whether one is cached and whether it expired;llmenv doctor --gcdrops the keychain item belonging to each folder it deletes. See Configuration (#1057) - Keep third-party MCP server logins (Slack, Notion, Linear, …) across cache folders. Claude Code stores those tokens under
mcpOAuthin the same store as the login token, so they ride along with it — but a lapsed Claude login no longer discards them, since the two authenticate different things and expire independently.mcp-needs-auth-cache.jsonis inherited too, so Claude Code doesn't re-probe every OAuth server after a hash change, andllmenv doctorreports how many MCP tokens are cached. See Configuration (#1058) - Warn about
native_<feature>.<engine>keys no engine will ever read, instead of dropping them silently. A typo (native_mcp.opencde), or a key naming a real engine whose adapter doesn't read that map (native_model_providers.claude_code,native_hooks.opencode), used to parse, merge, and hash cleanly and then vanish.llmenv export,llmenv regenerate, andllmenv doctornow report both cases across every per-engine map, reading the merged config so keys contributed by abundle.yamlare covered;llmenv validatefails outright on an unknown engine id. See Engines (#1032) - Flag
capabilities.permissionspatterns that use Claude Code's colon-prefix syntax (a trailing:*command prefix, or adomain:/url:filter) when opencode is also installed and enabled. opencode matches a pattern as a plain glob, so the rule never applies there — and a deaddenyfails open, which the warning calls out specifically. Reported byllmenv doctor,llmenv export, andllmenv regenerate, for bundle-contributed rules as well as top-level ones. Seedoctor(#838) - Add
llmenv task ls --current-projectto narrow a task listing to the current project's tasks (any session ever tagged to it, open or closed), andllmenv task show --current/--nextto jump straight to the task in progress (or the next actionable one after it) without hunting throughtask lsfirst. Seetask(#927, #928) - Add
llmenv completions --installto write a shell completion script straight to its standard directory ($BASH_COMPLETION_USER_DIR/~/.local/share/bash-completion/completionsfor bash, $ZSH_CUSTOM/~/.zsh/completionsfor zsh,~/.config/fish/completionsfor fish) instead of only ever printing to stdout — most users never discoveredcompletionsexisted because wiring it up meant knowing the right path yourself. Auto-detects the shell from$SHELLwhen omitted,--diroverrides the target,--forceallows overwriting an existing file. Seecompletions(#756) - Add
features.cd_guard, a warn-onlyPreToolUseadvisory on Bash commands thatcd, on by default. "Shell cwd was reset to<path>" was the single most common non-empty Bash stderr signature across ~18k archived sessions (77 occurrences) — Claude Code resets the working directory after every Bash call thatcds, standalone or as the leading step of a compound command, silently breaking any following command that assumed the new directory. Prose guidance alone wasn't stopping it; this mechanizes the reminder instead, without ever blocking the call. See Configuration (#976) - Add
capabilities.permissions.preset: safe-readonly, a core-shipped bundle ofallowrules (withdenycompanions closing the one dangerous flag each tool has) for the read-only CLI tools this project's own bundled rules already tell the agent to prefer —rg,ast-grep,shellcheck,shfmt, plus read-onlygit status/diff/log/show/blameandls.fdis excluded: its own dangerous flag can hide behind a short-flag cluster in a way adenyglob can't catch. 272 of the "Claude needs your permission" prompts across ~18k archived sessions were for exactly these tools, because core shipped no defaultallowrules for them and every config had to reinvent its own. See Configuration (#975) llmenv doctornow flags a config that allows a legacy tool (grep,find) without also allowing the replacement this project's own rules recommend for it (rg,fd) — a cheap nudge toward the newsafe-readonlypreset for configs that haven't adopted it. See Configuration (#975)- Add
features.memory[].wakeup_max_tokensto control the size of theSessionStartwake-up pack. llmenv previously calledicm_wake_upwith no arguments at all, so icm's own MCP handler silently fell back to its hardcoded 200-token default instead of the 500 a user might expect from icm's ownconfig.toml— that file is never consulted on this path. Set it explicitly to request a different budget; values outside20-4000(the range icm's handler clamps to) fail validation instead of being silently truncated. See Configuration (#1216)
Changed
llmenv task start <id>now refuses to start a task with an unmetblocked_onreference instead of only warning and starting it anyway —blocked_onis an explicit dependency the user configured on purpose, so an unresolved one is a real ordering violation, not just untidy. Pass--forceto override. Ablocked_onreference is satisfied only once the target task and every one of its descendants are done, so blocking on a parent task alone covers its whole child set (e.g. several parallel sibling tasks) without ablockedge per sibling. An undone--parentrelationship is unaffected — it's organizational grouping, not an ordering guarantee, and now gets an explicit soft-block warning (starts anyway) where previously nothing checked it at all. Seetask(#1164)llmenv task lsnow requires--session <id>or--all— it previously defaulted to listing every session's tasks across the whole store with no flag at all, easy to reach for by accident when only the current session's tasks were wanted. Pass--allto deliberately see everything. Seetask(#1124)hook-runreuses the bundle-merge result from the lastregenerate/exportinstead of redoing it on every invocation. The prior in-process merge cache (#813) never actually hit in real usage — eachhook-runis a fresh subprocess — so the disk I/O and YAML parsing behind memory-backend resolution ran on everySessionStart/TurnStart/SessionEnd. It's now persisted to a small cache file keyed on bundle/config content, with a live merge as the fallback whenever that key doesn't match. See Materialize (#920)LLMENV_TRACE_TIMING's per-phase marker now fires on everyhook-runevent, not just the ones that reach the full memory-dispatch stage (previously 4 of 11). Each field is present only for phases the event actually reached, so an early return still reports whateverconfig_load/scope_evalcost it incurred instead of nothing. See Troubleshooting (#1128)
Fixed
-
A bundle-contributed
features.memoryentry naming aserver_hostthat has no entry anywhere in the mergedhost:table (top-level config.yaml plus every bundle) is now rejected at merge time instead of being accepted and only failing later, deep in hook-run's own MCP resolution — the same checkConfig::validate()already applied to top-levelfeatures.memoryentries, extended to bundles. Found during review of #1216. (#1224) -
A bundle a project turns off via
disable_bundlesno longer contributes itsfeatures.memory/hostentries to the ICM memory endpoint that lifecycle hooks resolve.hook-runcomputed its own firing-bundle set that honored tag matches andenable_bundlesbut skippeddisable_bundles, so hooks could resolve memory against a bundle the materialized manifest had already excluded — and, for a project that setdisable_bundles, the two disagreeing sets also meant the bundle-merge cache never hit. A disabled bundle is likewise no longer named in memory recall queries or in the context chunk stored in the backend. See Configuration (#1125) -
no memory backend active for this scopenow says which of the four causes applies instead of one message for all of them: no bundles fired, nothing declaresfeatures.memory, a firing bundle has no content directory (so itsbundle.yamlwas never read), or the only bundle supplying memory is turned off viadisable_bundles.llmenv doctor --allwarns about that last case too — previously memory worked in~/, stopped the moment youcd'd into the project, anddoctorstayed green. A top-levelfeatures.memoryentry whoseserver_hostlives in a disabled bundle'shost:table now names the bundle in its error as well. See Configuration (#1131). Found during review of #1125. -
llmenv doctor --all'sdisable_bundlesorphan warning no longer contradicts itself when two or more bundles supplyfeatures.memory— it printed one "only supplied by bundle X" line per bundle, so two suppliers produced two mutually exclusive "only" claims in the same report. It now emits a single message naming every supplier. (#1139) -
A
bundle.yamlllmenv can't parse or read no longer masquerades as "no memory backend configured". The bundle merge behind memory-endpoint resolution swallowed its error and defaulted to no bundle contributions, so a broken bundle file sent you off to read your scope config — the one place the problem wasn't. It now reports the parse failure. A failed merge-cache signature is logged rather than silently degrading the #920 optimization to unexplained hook latency. (#1132). Found during review of #1125. -
Detached memory children can no longer fail into
/dev/null. Web-fetch memory stores, post-session consolidation, and detached transcript records were spawned with their stderr discarded, and the errors meant to compensate logged at a level the default filter drops — so any of them could fail with no trace anywhere. Their stderr now goes to$XDG_STATE_HOME/llmenv/detached-hook.log(owner-only, rotated at 512 KiB), and their failures log at error level, mirroring the fix #1086/#1091 shipped for the mcp-proxy and indexer logs. See Troubleshooting (#1133). Found during review of #1125. -
llmenv doctorno longer reportsnative_permissions.opencodeandnative_permissions.crushas orphaned keys. The orphan check hardcodedclaude_codeas the only engine name, so the two newer adapters' own permission overrides were flagged even though both adapters read them. It also no longer treats an MCP server name as a validnative_permissionskey — that map is keyed by engine, so such a key was itself dead config. (#1032) -
llmenv doctor's engine binary check no longer skips engines added after it was written; it now walks the adapter registry instead of a hardcodedcrush/opencodelist. (#1032) -
llmenv setupno longer skips engines added after it was written. Bothprobe_engines(which checksPATHfor installed engines) andcompute_disabled_engines(which computes the resultingdisabled_enginesconfig) hardcoded the same three-engine list rather than reading the adapter registry, so a new adapter wouldn't be offered by the wizard and could end up explicitly disabled even with its binary installed. Same bug class as #1032. (#1074) -
llmenv statuslineno longer vanishes whenconfig.yamlwon't parse. It rendered nothing at all — the command exited non-zero with empty stdout and the parse error went only to a stderr the engine discards, so a YAML typo silently blanked the status line in every open terminal with no signal anywhere. It now exits 0 and renders⚠️ llmenv: config error — run 'llmenv doctor'instead. Seestatusline(#1052) -
An IPv6
memory.listen_hostno longer starts a proxy llmenv can never see. The bind address was assembled as{host}:{port}, giving::1:9092— which the liveness probe can't parse, since IPv6 needs bracketing. So the proxy started, went undetected, and every following export waited out the bind window, reported "did not bind", and started another one. The address is now built (and parsed) throughSocketAddr, so the two can't disagree. Found during review of #1084–#1086. (#1087) -
A
^C(or a dropped SSH session) whilemcp-proxywas starting no longer disables the memory backend permanently.llmenv exportruns in the shell's foreground process group, so it died holding its spawn lockfile — and with no staleness check, every later export failed against a file most users had never heard of. The lock now records its holder and is reclaimed when that process is gone. A concurrent export during a cold start also waits for the first one's proxy instead of immediately reporting a lockfile error. Found during review of #1084–#1086. (#1087) -
mcp-proxystartup failures are diagnosable again. The proxy's stderr went to/dev/null, so a proxy that wouldn't start produced only "did not bind … check that the port is free and mcp-proxy is correctly installed" — advice that named two causes that were both wrong in practice, while the real one (anImportErrorfrommcp-proxy's open-endedmcprequirement) was only visible by re-running the command by hand. Its stderr now goes to$XDG_STATE_HOME/llmenv/mcp-proxy.log(owner-only, rotated at 1 MiB), the failure warning quotes the tail of that log, and the speculative hints are gone. See MCP Servers and the Memory Backend (#1086) -
llmenv exportno longer warns thatmcp-proxyfailed to start when it started fine. The post-spawn check slept a fixed 300 ms and probed once, but a real proxy takes ~0.55 s to bind (~2 s viauvx, which pays uv's resolve cost) — so every cold start printed a bind-failure warning and deleted the pidfile, while the proxy it had just launched came up moments later and kept running, orphaned. llmenv now polls for the bind every 50 ms for up to 5 s, and reports a proxy that exits before binding immediately rather than waiting the budget out. See MCP Servers and the Memory Backend (#1084) -
Stop recording a dead pid as the running
mcp-proxy, and stop launching a second proxy when the first is already serving. Liveness required both a pidfile and a listening port, so a live proxy whose pidfile went missing read as dead: llmenv spawned a replacement that died instantly on the taken port, wrote that dead child's pid to the pidfile, and then saw the original proxy answer its probe — reporting success. The pidfile was left permanently wrong but non-empty, which the old check read as proof of life forever, and the "listen_host is '0.0.0.0'" warning fired on a run that started nothing. The bind address is now the sole liveness signal; the pid is written only after the bind is confirmed and the child is confirmed alive, and a pidfile naming a process that isn't running is cleared. See MCP Servers and the Memory Backend (#1085) -
Stop losing
/resumehistory on every cache-folder change. Claude Code keeps its transcripts inprojects/insideCLAUDE_CONFIG_DIR, so a config edit or version bump left the session list empty.projects/now lives once in the durable state dir with each folder symlinked to it, andhistory.jsonlis copied in when a folder has none. Transcripts stranded by the old behavior are folded into the shared store on first run, newest copy of a session winning. The previousmigrate_ephemeralmechanism only ran instricthashing mode and scanned the wrong directory level, so on the default mode it never migrated anything. See Configuration (#1059) -
A cached ICM transcript session id is no longer trusted forever. Session logging correlates each Claude Code session with an ICM transcript session, recorded once and reused on every later hook event — but if ICM restarted or pruned that session in between, the stale id was replayed with no recovery short of restarting
llmenv. It's now revalidated once per launch (atSessionStart) before being trusted, and a failed revalidation re-establishes a fresh session instead. Found during review of #1087. (#1090) -
A failing
codebase-memory-mcpindex run is diagnosable again instead of leaving nothing to look at. Indexing a repo can take minutes and runs detached so it never blocksSessionStart, but its stderr went to/dev/null— so a failure partway through was invisible. It's now captured to<index_path (or its default)>/index.log, size-bounded and owner-only, mirroring the same fix #1086 shipped for the mcp-proxy log. See Configuration (#1091). Found during review of #1087. -
A stale cached MCP session id is recovered from instead of replayed forever.
llmenv's MCP HTTP client caches theMcp-Session-Ida server hands out oninitializeand reuses it on every call — but a server restart or session expiry made every later call fail (HTTP 400/404) with no recovery short of restartingllmenv. It now clears the cache and re-initializes once before giving up. Found during review of #1087. (#1094) -
A locked macOS keychain no longer reads as "no credential stored".
security find-generic-passwordfailed with its stderr discarded, so a keychain awaiting unlock and a genuinely absent credential looked identical — both silently degraded into an unexplained re-login prompt. Any lookup failure other than the documented "item not found" exit code now surfaces as an error naming the likely cause.security add-generic-passwordfailures also report the tool's own diagnostic instead of just a status code. Found during review of #1087. (#1092) -
Post-session consolidation no longer leaks a
claudesubprocess on every LLM-call timeout. The 120-second call toclaude -pran withoutkill_on_drop, so a timeout dropped the process handle without terminating it — each one potentially holding an open API session. Same root cause as themcp-proxyorphan #1087 fixed. Found during review of #1087. (#1093) -
A set-but-empty
LLMENV_STATE_DIR,LLMENV_CONFIG_DIR, orCLAUDE_CONFIG_DIR(e.g. from a strayexport FOO=in a shell profile) is no longer treated as a real override. It resolved to a relative path, scattering the task tracker'stasks/*.json, the statusline's usage-delta cache, orllmenv-status.jsoninto whatever directory the process happened to run from instead of the intended state/config/cache location — invisible to every later command run from elsewhere. All three now fall through to their documented default, same as when the variable is unset. Found during pre-pr-review of #1109. (#1111) -
TaskList/TaskCreateno longer report an unreadable task or session store as an empty one.list_tasks/list_sessionscollapsed a genuine read error (permission denied, a bad mount, anLLMENV_STATE_DIRpointing at a file) to the same empty result as "nothing tracked yet," soTaskListdenied with a false "(no tasks tracked yet)" andTaskCreatecould auto-start a second session on top of the store it couldn't read — both now surface the real error and point atllmenv taskfor a manual fallback instead. Found during pre-pr-review of #1109. (#1112) -
The task/session store's directories and its lock file are now created owner-only (
0700/0600) from the moment they're created, instead of at the default (often world-readable) permissions and narrowed only later. Found during pre-pr-review of #1109. (#1113) -
write_owner_only_atomic's parent directory is now owner-only (0700) at every level, not just the immediate parent, and a directory that already existed at a looser mode is hardened too. It used tocreate_dir_allthe parent (default umask, typically0755) and chmod only that immediate parent afterward — a TOCTOU window, and a permanent world-readable state for any intermediate ancestor the chmod never touched, or any directory created before this hardening existed. Found during pre-pr-review of #1177. (#1178) -
A set-but-empty
HOME(e.g. from a strayexport HOME=in a shell profile) is no longer treated as a real value.expand_tildeexpanded~/restto/rest— anchored at the filesystem root — instead of leaving it unchanged like an unsetHOME; the interactive setup wizard's config/plugin scanning and project-tag/scope discovery had the same gap. Same bug class as #1111. Found during pre-pr-review of #1177. (#1179) -
Five more state/cache directories are now created owner-only (
0700):mcp-proxy's pidfile/lockfile parent and its bounded-log directory, the session-log append directory, the throttle usage-cache directory, and the durable materialization state dir (plus every configured tool's subdirectory). Same bug class as #1178. Found during pre-pr-review of #1184. (#1186) -
llmenv doctor --allnow flags a network scope whosematchhas nogateway_macas an orphan that can never activate. The matcher only evaluatesgateway_mac;ssid/cidrare accepted by the config schema and documented as fields, but silently ignored — so a scope keyed only onssid/cidrnever fired, with no signal anywhere short of reading the docs. See Getting Started (#1051) -
Five more directories are now created owner-only (
0700): the bundle materialization cache root, theread_once/repeat_detecthook state directories, the plugin/marketplace cache root, and the config directory created by bothllmenv initand thellmenv setupwizard (two separate call sites doing the same thing).open_bounded_log's directory-hardening and file-open are now one atomic call instead of two, so a hardening failure (e.g.EPERMchmod'ing a directory owned by another uid) can no longer open the log inside an unhardened directory unnoticed. Same bug class as #1178/#1186. Found during pre-pr-review of #1186's own PR. (#1196) -
A user-configured
features.codebase_memory.index_pathis no longer forced to0700. Since #1186, indexing forced that permission unconditionally, which broke setups sharing the directory with acodebase-memory-mcpprocess running under a different uid (separate service account, differently-mapped container) — indexing then failed with anEACCESvisible only via debug logging. Only llmenv's own default state-dir-rooted cache directory is still hardened; an explicitindex_pathoverride now keeps whatever permissions its owner already gave it. See Configuration (#1196) -
A timed-out post-session consolidation call no longer orphans
claude -p's own descendants. #1093 made a timeout kill the directclaude -pchild instead of leaking it, butkill_on_droponly signals that one pid — any MCP servers or tool subprocessesclaude -pspawned kept running. It's now spawned into its own process group, and a timeout kills the whole group via a directkill_process_groupsyscall instead of shelling out tokill— closing both a several-ms pid-recycling race in that fork+exec window and a broadcast-kill hazard, where the group-kill'spid <= 0guard letpid == 1through and the kernel treats a negated1as "every process the caller may signal" rather than "process group 1". Found during pre-pr-review of #1163, and again during pre-pr-review of this fix's own PR. (#1165) -
Four more directories are now created owner-only (
0700): the bundle materialization cache root under the default hashing mode (#1196 only reached the less-common strict mode's cache root, leaving the default mode unprotected —cache_rootis now hardened unconditionally before any mode-specific branch, including Strict's early return, rather than only thedestpath beneath it), the statusline widget's PR-lookup and usage-delta caches,llmenv doctor's cache-directory-writable check, and the plugin-payload cache directory. As withindex_pathin #1196, hardening only applies when the check itself creates the directory — an already-existing cache dir owned by a different uid (a separate service account, a differently-mapped container) keeps whatever permissions its owner gave it instead of being forced. Same bug class as #1178/#1186/#1196. Found during pre-pr-review of #1196's own PR. (#1198) -
Two bundles at the same scope precedence disagreeing on
effort_level,advisor_size,auto_memory_enabled, or anyfeatures.*scalar (slippage,context_mode,upgrade,read_once,task_tracker,repeat_detect,cd_guard) now hard-errors naming both, instead of silently picking whichever contributor happened to be seen last.default_modealready had this protection; the shared resolver backing every other scalar didn't, an undocumented gap from the documented "same-precedence disagreement is a hard error" merge policy. (#1215)
[3.7.0] - 2026-07-28
Mostly config-schema hardening: native.<engine> fragments now reject malformed
shapes and point at the right escape hatch instead of silently dropping config,
and tags/bundle names are validated instead of failing silently deep in ICM.
Also ships opencode model-provider rendering parity with Crush, an on-by-default
repeat-loop guard (features.repeat_detect), LLMENV_EXTRA_TAGS for
tag-activation without a committed marker file, and a 1997 GeoCities-style
retro skin for the docs site.
Added
- Give the docs site (
website/) a 1997 GeoCities-style retro skin — dark black-and-gold theme, tiled background, marquee banner, under-construction badge, and a per-browser hit counter, all checked against WCAG AA contrast. Site-only change; nollmenvCLI/config behavior affected. (#1027) - Add background MIDI music to the docs site, playing continuously while browsing. Includes a fixed mute/play toggle per WCAG 2.1's audio-control requirement, since browsers already block true autoplay until the visitor interacts with the page. (#1027)
- Add model provider configuration rendering to the opencode adapter —
capabilities.model_providers/default_modelsnow render intoopencode.json'sprovider/model/small_modelfields, matching the existing Crush support.api_typemaps to the AI SDK package name opencode expects (e.g.openai→@ai-sdk/openai-compatible);default_models'slarge/smallroles map to opencode's two default-model slots. See Configuration (#1004) - Add
capabilities.native_model_providers.<engine>— the escape hatch for provider keys opencode and Crush accept butcapabilities.model_providershas no field for (opencode's per-modelreasoningEffort, say). Deep-merges onto the rendered provider block, and renders on its own so a hand-written provider survivesllmenv regenerate. See Engines (#1008) - Add
features.repeat_detect, an engine-neutral guard against stuck-loop behavior, on by default. Covers two cases: a model repeating the identical tool callthresholdtimes in a row (default 3), and — the more common real-world trigger — a model ignoring the task tracker's "you still have a task in progress" reminder every turn instead of pausing it. Both surface an advisory (the tool-call case nudges trying something else; the reminder case points atllmenv task wait <slug> "<reason>") rather than blocking anything, and it fires for any adapter/model since it lives in the shared lifecycle-hook layer rather than per-adapter code. See Configuration (#1006) - Add
LLMENV_EXTRA_TAGS, a comma-separated env var that unions extra tags into the active scope tag set — works with or without a committed.llmenv.yaml, for cases like a client repo you can't add config files to, a throwaway clone, or a personal-only tag you don't want to share via a checked-in file. See Configuration (#1020)
Changed
- The task-tracker redirect messages for Claude Code's built-in
TaskCreate/TaskUpdatenow mentionllmenv task wait|block, not juststart|note|done, so the agent is pointed at the full command set instead of just the original three. Also trimmed the redirect and Stop-hook wording (stop_hook_reminder) to cut repeated boilerplate on every turn/call, and shrankskills/llmenv/references/task-tracker.mdfrom 97 to 29 lines to match its sibling reference files. Seetask(#994, #995)
Fixed
- Rejecting a modeled key in
native.<engine>pointed you atnative_<key>.<engine>as if that field always existed — forprovider,model,lsp, andinstructionsit never did. The error now names the one hatch that applies, or the neutralcapabilitiesfield when there is none. See Engines (#1008) - A
native_*.<engine>fragment that wasn't a mapping (usually a YAML indentation slip) silently deleted the whole block it was meant to merge into and exited 0 — taking any neutrally-declared MCP servers or hooks with it. It now errors, naming the field and the shape it got. See Engines (#1008) - The SessionStart/Stop task-tracker reminders scoped
wiptasks to the current project but not the current session, so an agent in one terminal could be nudged with directive "keep working — don't stop mid-task" language about a task a completely different, concurrently-running session owned — risking two agents driving the same branch/PR at once. Each task in the reminder now names the session that started it, and the wording never presumes ownership: it conditions resuming or finishing a task on the agent actually recognizing it as its own earlier work. Seetask(#1028) Capabilities::is_empty()never checkedfeatures.codebase_memory, so a config fragment whose only content was acodebase_memoryentry was silently reported as empty — dropping it whereveris_empty()gates rendering/merging. It now accounts forcodebase_memorylike every other feature list. See Configuration (#1021)merge_capabilitieshardcodedadvisor_sizetoNone, so settingadvisor_sizein any bundle or scope silently never reached the generated engine settings. It's now resolved by highest-precedence-wins like every other scalar capability field. Found during pre-pr-review of #1025.- Document
capabilities.model_providers/capabilities.default_modelsin the configuration reference — the schema has supported custom model-provider endpoints and role-keyed default models for several releases with no user-facing docs. See Configuration (#994) llmenv materialize'sopencode.schema.jsonsidecar — documented as shipping back in 3.3.0 (#660) but never actually wired into the crate — now really gets written alongsideopencode.json, which now points its own$schemafield at the sidecar instead of opencode's hosted schema. See Engines (#1001)docs/env-vars.mddocumentedLLMENV_ACTIVE_TAGS/LLMENV_ACTIVE_SCOPES/LLMENV_ACTIVE_BUNDLESas colon-separated; the code has always joined them with commas. Corrected while adding docs forLLMENV_EXTRA_TAGS(#1020)- A tag (or bundle name in
enable_bundles/disable_bundles) from.llmenv.yaml,config.yaml's scopes, or$LLMENV_EXTRA_TAGScontaining anything outside alphanumeric/-/_used to pass through unnoticed until ICM's recall query rejected it — silently disabling memory recall/store and session logging for the rest of the session, with no visible error. Tags and bundle names are now validated (and length- and count-capped) where they're created; invalid or excess entries are dropped with atracing::warn!(visible withRUST_LOG=warn) instead. See Configuration (#1035) - The MCP docs page linked the
memory:config reference at a nonexistent anchor (configuration#memory), dropping readers at the top of the Configuration page instead of thefeatures.memory:section. See MCP Servers and the Memory Backend (#1037)
[3.6.1] - 2026-07-24
A bug-fix and small-UX patch centered on the task tracker: Claude Code's built-in task tools now feed the llmenv task tracker instead of bypassing it, task ls output is grouped and filterable, and reminders no longer leak across projects. It also fixes feature-enabled MCP permission precedence on Claude Code and trims per-session context bloat — the statusline {pr} and branch widgets self-resolve their PR under engines that don't send one, rendered hooks no longer fire twice per event, and the ICM memory injection stays silent when the store is empty. Adds the opencode adapter, stale MCP server pruning, and tiered MCP permission rules for built-in servers.
Added
- Add Opencode engine adapter (
src/adapter/opencode.rs) — full feature parity with the Claude Code adapter: rendersopencode.json(MCP, LSP, permissions, env vars),AGENTS.mdwith frontmatter translation, rules, and a JS hook bridge shim that maps Opencode plugin events to llmenv hook subprocess calls with Claude-shaped stdin payloads. Plugin content (skills, commands, agents, MCP) from Claude Code bundles is translated into Opencode-native forms (#657) - Add model provider configuration rendering to the Crush adapter —
capabilities.model_providersandcapabilities.default_modelsare now rendered intocrush.json(#682) - Add stale MCP server pruning to the Claude Code adapter — servers previously owned by llmenv but absent from the resolved set are removed from
.claude.json, preserving user-added servers (#739) - Add tiered MCP permission rules for built-in servers (ICM, context-mode) — read-only tools are auto-allowed, mutation tools prompt the user, and destructive tools are denied, matching the sensitivity tier of each tool (#694)
llmenv task lshuman output now groups tasks by session (current-project sessions first), indents subtasks under their parent, prefixes each row with a state glyph + label, and annotates blocked tasks with theirblocked_onrefs; new--state <open|wip|waiting|done>(repeatable) and--hide-done/--activefilters compose with--sessionand apply to--format jsontoo. Seetask(#926)- Feature-enabled MCPs (
features.context_mode,features.memory) now take amcp_permissionsoverride to customize the read-only/mutation/destructive tier→action policy per feature. Seemcp_permissions(#946)
Changed
- The bundled
llmenvskill's task rules now guide agents to link tasks liberally with--parent(ordered decomposition) andblock --on(real dependencies) and to record milestones, design rationale, and failures withtask note. Seetask(#932)
Fixed
- Fix opencode hook shim generating misleading warning when bundle path resolution fails — diagnostic now correctly describes stale or restructured bundles (#769)
- Fix
split_frontmattercrash on empty/single-delimiter input in the opencode adapter (#769) - Fix silent
remove_fileerror discard in claude_code companion file cleanup — now emitstracing::warn!on failure - Add
tracing::warn!diagnostics toread_owned_serversI/O and parse error paths - The task-tracker Stop hook no longer re-injects the
waiting-task FYI every turn;waitingtasks are now silent on Stop and surface only in the SessionStart reminder. Seetask(#933) llmenv task addno longer warns "you have N task(s) already in progress" forwaitingtasks — only genuinelywiptasks count, since starting new work alongside a task paused on external input is legitimate (#933)- The statusline
{pr}widget no longer renders empty under engines (like Claude Code) that don't send aprfield — it now self-resolves viagh pr viewfor the current branch, cached briefly so it doesn't shell out on every render. Seestatusline:(#950) - The task-tracker Stop hook's
wipreminder and SessionStart'swaitingreminder no longer leak across projects sharing the same task store — awip/waitingtask from one project no longer nags a hook running in another. Seetask(#949) - Feature-enabled MCP permissions (context-mode, ICM) no longer conflict between a wildcard allow and per-tool tier rules; Claude Code's
deny > ask > allowprecedence was silently shadowing the wildcard, so mutation tools prompted on every call and destructive tools were blocked outright even with the feature enabled. Default policy now allows read-only and mutation tools without prompting, and asks before destructive ones. An explicitnative_permissionsrule on a built-in MCP tool now takes precedence over the tier default for that tool (deny > ask > allow), rather than emitting a competing entry. Seemcp_permissions(#946, #972) - The statusline
branchwidget's PR hyperlink no longer stays inert under engines (like Claude Code) that don't send aprfield — the branch text now links to the current branch's PR via the same self-resolvinggh pr viewlookup the{pr}widget uses (#950), sharing its short-lived cache. Seestatusline:(#973) - Rendered
settings.jsonno longer lists each hook twice for the same event on a first or strict render — the freshly generated hooks doc is now deduped at generation time (the same strip-nulls-then-dedup passreconcilealready applied when a prior file existed), so each guard fires once per event instead of launching two (or, for dual-interpreter guards, four) processes per tool call (#977) - The ICM memory injection no longer adds a
No memories foundblock or a "consider saving" nag to the context on every prompt when the store is empty — advisory-line stripping is now case-insensitive to server wording, and a recall left with only advisory/blank lines injects nothing (#978) - With the task tracker enabled, Claude Code's built-in
TaskCreate/TaskList/TaskUpdatetools are now redirected into thellmenv tasktracker instead of Claude's ephemeral task state —TaskCreaterecords a real task (auto-starting a session when none is open),TaskListreturns the tracker's view, andTaskUpdatemaps status to start/done/delete. Previously the agent's built-in task tools bypassed the tracker, so it sat mostly unused. Seetask(#985) - Features set at the root of
config.yaml(features:) are no longer silently dropped from the generated engine config.build_manifestonly fedmerge()thecapabilities:block, so a root-leveltask_tracker,slippage, orcontext_mode(incl. itsmcp_permissionsoverride) never reached the manifest that renderers gate on — the task-tracker hooks, slippage guardrails, built-in skill reference docs, and MCP-permission overrides could all silently go missing. Rootfeatures:now folds into the merged manifest (root wins over bundle-contributed values) (#987) - A hook removed from your config no longer lingers in the generated
settings.json.reconcileunions rendered hooks with what's already on disk (to preserve hooks a plugin self-registers at runtime), which meant a hook llmenv used to render but no longer does was kept forever. llmenv now records the hooks it renders and, on the next render, purges its own dropped hooks while still preserving genuinely-foreign ones (#991)
[3.6.0] - 2026-07-22
3.6.0 includes three new engine-facing pieces — an in-engine task tracker, a first-class llmenv statusline subcommand, and a third supported engine (opencode, alongside Claude Code and Crush) — plus a codebase-memory-mcp integration.
A string of hook-run perf work landed too: single-walk scope.content matching instead of one walk per matcher, uname(2) instead of shelling out to hostname, memory-recall dedup, and cutting redundant config.yaml re-parses and per-invocation clones/reads/stats across hook-run, export, and regenerate.
On the fix side: opencode permission precedence and malformed-rule handling, skill-frontmatter YAML escaping for control chars and Unicode noncharacters, several read_once/session-log ordering bugs, and null-valued hook keys leaking into generated engine configs.
Added
- Add an in-engine task tracker (
llmenv task add|start|done|wait|ls|show|note|block|clear), off by default. Seetask(#231) - Add mandatory, project-tagged task sessions: every task belongs to a session, each session is tagged with the project it started in, and any number can be open at once.
task session startsurfaces an existing same-project session with a--resume/--replace/--newcheckpoint instead of colliding; sessions carry a--description, andtask session lslists the open ones for recovery after a context compaction. Seetask(#905) - Add an
llmenvskill materialized into every engine (Claude Code, opencode, Crush) with a reference file per enabled built-in (task tracker, memory, context-mode, codebase-memory), replacing the old Claude-Code-only task-tracker CLAUDE.md fragment. Seetask(#905) - Add a first-class
llmenv statuslinesubcommand with 21 configurable widgets, replacing the old ad hoc status line. Seestatusline:(#836) - Opt-in per-phase hook-run timing via
LLMENV_TRACE_TIMING— emits phase durations as onellmenv-trace {json}stderr line, off by default llmenv doctorflagshook.matchervalues shaped like file globs (e.g.*.rs) — Claude Code only matcheshook.matcheragainst tool name, so these silently never fire (#837)- Add
features.codebase_memory, a first-class integration for codebase-memory-mcp. See MCP servers (#365) - Add the opencode adapter —
opencodeis now a third supported engine alongsideclaude_codeandcrush, at near-parity with Claude Code. See Engines (#876)
Changed
- Hook-run performance: single-walk
scope.contentmatching instead of one walk per matcher (#703),uname(2)instead of shelling out tohostname, memory-recall dedup for repeated blocks, and fewer redundantconfig.yamlre-parses/clones/reads/stats across hook-run, export, and regenerate
Fixed
- Bundle/user hooks no longer emit null-valued
tool/commandkeys into the generated Claude Code or Crush config (#720) - Skill frontmatter
name/descriptioncontaining control characters or Unicode noncharacters no longer produces invalid YAML when auto-quoted (#859, #873) features.read_onceno longer silently drops Debug-level session-log capture forPreToolUseevents (#864)- A computed
read_oncedeny/advisory result is no longer discarded if an unrelated hook-run pipeline error occurs afterward (#867) SessionEndsession-log capture is no longer skipped when the redundant-store dedup check fires (#866)- opencode adapter: a native
allowrule no longer silently overrides a structureddenyrule for the same tool+pattern (#877); a malformed native permission rule string no longer falls back to wildcard-allow (#882) - A hook whose handler
typedoesn't match its populated field now fails config load with a clear error, instead of silently loading as a no-op (#851) - A computed
read_oncedeny result is now always enforced (was only guarded bydebug_assert!, a no-op in release builds) (#868) config.yamlnow rejects a duplicatescope.contentid, matching the existingnetwork/host/usercheck (#843)- Claude Code adapter: a
Writepermission rule is now rewritten toEditbefore reachingsettings.json, matching Claude Code's own deprecation (#888) - opencode/crush plugin materialization no longer fails with a missing
install_locationwhencache.remote_sync: false - The
icmstatusline widget always rendered empty — its parser expected JSON, but the underlying tool returns plain text (#903) - The
config_stalestatusline widget ignored a custom icon override unless a customformatwas also set (#904) - Sync-state, marketplace-manifest, and MCP-proxy pidfile reads now surface non-
NotFoundI/O errors (e.g. permission denied) instead of masking every stat failure as "file absent" (#893) llmenv memory diffno longer risks overwriting the snapshot baseline when a stat error masks an existing snapshot as absent, and now surfaces read errors (#911); the opencode adapter surfaces permission errors on a plugin'scommands//agents/directories instead of silently skipping them (#912)- Directory and file reads across cache prune/gc, skill validation, bundle rules/content ingestion, opencode plugin MCP/hooks parsing, and settings import now surface permission errors instead of an
exists()stat masking them as "absent" — closing the last of this class, including a case where an unreadable skills directory silently bypassed skill validation (#915, #916)
[3.5.1] - 2026-07-15
Fixed
remote_syncno longer blocks manualllmenv syncandllmenv plugin-synccommands — it only gates the non-interactive throttled pull duringllmenv export(#835)
[3.5.0] - 2026-07-15
Added
- Configurable session-log retention:
session_log.transcript.retention_days— best-effort deletion of stale session-log files before each SessionStart; validated >= 1 (#812) - Add
cache.remote_syncconfig option (defaulttrue) to disable remote git operations — prevents shell freezes when 1Password's SSH agent is locked and an SSH askpass prompt hangs terminal-based git ops (#833)
Changed
- Build manifest once per export/regenerate instead of once per adapter, reducing repeated work in multi-engine setups (#708)
- Hot-path optimizations for hook-run pipeline: cache Env::detect() results (30s TTL), cache bundle merge by config mtime, reuse Tokio runtime and MCP HTTP client via OnceLock (#813)
Fixed
- Remove dead process-static CONFIG_CACHE from hook_run that never saved a parse (each hook event is a fresh process); poisoned-cache log no longer fires on cold-start misses (#706)
- Add eprintln! diagnostic when fs::canonicalize() fails in read-once, so operators can detect non-canonicalized cache keys (#728)
- Add eprintln! diagnostic when deprecated PascalCase 'filePath' key is used in read-once, surfacing format drift (#729)
- Preserve MCP server sub-keys (runtime auth tokens) across re-materialization in
merge_mcp_into_claude_json— fixes silent auth loss on every materialize in Loose/Normal mode (#814) - Fail fast on manifest build error with preserved error chain instead of silently falling back to stale manifest (#708)
- Gate git marketplace and external plugin sync behind
cache.remote_syncto prevent hangs when remote sync is disabled - Distinguish local-only commits from pushed commits — prints "Committed locally (remote sync disabled — push skipped)" instead of misleading "Synced config to GitHub" when remote_sync is off
- Add
## Version X.xheaders to the generated website changelog for correct section hierarchy across major versions
[3.4.0] - 2026-07-14
This release tightens error diagnostic coverage across two dozen silent-fallthrough sites, adds PermissionMode variants for granular permission control, hardens cache GC edge cases, and normalizes JSON/YAML merge null-strip behavior.
Added
- Add
auto,dontAsk, andmanualPermissionMode variants alongside existing boolean/string forms —autois only honored from user-scope settings,dontAskskips the permission prompt, andmanualmatches the default deny-mode behavior (#748) - Migrate ephemeral state (
projects/) across hash changes in Strict mode materialization (#746, #797)
Fixed
- Fold
strip_json_nullsintonormalize_jsonso every merge path (not justreconcile_settings) benefits from null-tolerant merge dedup (#718) - Add null-stripping to
normalize_yamland insert-path null guard tomerge_yamlfor YAML merge parity with JSON (#718) - Session log transcript correlation (
session_log::state) no longer silently fails whenstate_dir()is unavailable — falls back to CWD with atracing::warn!instead of returningNone/Err(#737) - Add
tracing::warn!diagnostics to 7 additional silent-error swallowing sites in file_sink, event serialization, read-once canonicalize, throttle error body, consolidation error body, and MCP client error body reads (#773) - Enrich pre-subscriber diagnostics — promote event serialization failures
to
error!, add URL context to throttle/consolidation error messages, and log fallback path instate_path()warnings (#784) - Surface silent error swallowing in read-once hook —
state_dir()resolution failures are now logged as warnings before returning empty strings (#760) - Surface silent error swallowing in doctor version skew check —
read_dirfailures on adapter cache directories are now logged as warnings instead of being silently skipped (#764) - Surface silent error swallowing in login auth status update —
CacheManifest::readfailures are now logged as warnings instead of being silently skipped (#765) - Surface silent error swallowing in auth, throttle, hook-run, and reconcile_settings — read/parse failures are now logged as warnings instead of being silently discarded (#749)
- Fix transcript session id parsing — ICM returns the session id as a JSON object, not a bare ULID, so every transcript record call was passing a JSON blob instead of a real id and records went nowhere (#755)
- Add diagnostics for walkdir entry errors in scope matcher — I/O errors during directory traversal are now logged as warnings instead of silently skipped (#752)
- Add diagnostics for project marker file read errors — read failures on
.llmenv.yamlare now logged as warnings before returning defaults (#753) - Add diagnostics for config-context stdin JSON parse failures — parse errors are now logged as warnings before falling back to SessionStart (#754)
- Surface silent error swallowing in settings.json parse — parse failures
in
apply_seeded_settingsare now logged as warnings instead of silently returning defaults (#762) - Surface silent error swallowing in version comparison — malformed version
strings in
compare_versionsare now logged as warnings instead of silently returningEqual(#766) - Surface silent error swallowing in session log path resolution — path resolution failures are now logged to stderr instead of silently falling back to CWD before the tracing subscriber is initialized (#763)
- Upgrade
debug_assert!totracing::warn!in scope matcher — walkdir entries outside the workspace root are now surfaced as warnings instead of only being checked in debug builds (#761) - Remove angle brackets from bare URLs in changelog and release docs —
<url>is interpreted as JSX by Docusaurus, breaking thedocs.ymlCI build againstwebsite/docs/changelog.mdandwebsite/docs/release.md(#811) - GC in Normal mode now age-checks each shape individually instead of treating the entire version generation as one unit (#738, #797)
- Clock-skew handling in GC — entries with future mtimes are now treated as expired with a logged warning instead of silently skipped (#797)
- Edge-case hardening in cache lifecycle — log I/O errors in ephemeral
migration, attempt older siblings on copy failure, clean up
.tmpstaging directories in GC, and log unexpected entries (#797)
[3.3.0] - 2026-07-13
Deprecated
- The old boolean
session_logshape (file: bool,transcript: bool,verbose: bool) is deprecated. It still parses in 3.x but will be removed in 4.0. Migrate to the new per-sink mapping blocks. (#744)
Removed
- Remove dead
difffield fromReadOnceconfig schema — the planned phase-2 delta mode was never implemented (#725)
Changed
session_log.verbosereplaced with per-sinklevel(info/debug/trace).session_log.fileandsession_log.transcriptare now mapping blocks withenabled+levelfields. Old boolean shape still parses. (#740)
Fixed
- Early-exit hook-run before scope evaluation for events that produce no memory actions — saves ~3.5ms per PreToolUse dispatch on a loaded config (#702)
- Thread
--engineflag through to adapter selection so hook-runs targeting non-default engines (e.g. opencode) actually use the correct adapter instead of always env-sniffing (#704) - Fix WebSearch auto-store labelling "URL: unknown" instead of
the actual search query — read
tool_input.queryfor WebSearch and label asQuery:(#707) - Strip ICM advisory lines ("Consider saving", "No memories found.") from hook-run recall output — ~1KB/turn of noise in agent conversations (#692)
- Fix doctor false-flagging marketplaces pinned to annotated
tags as broken —
git rev-parse <tag>returns the tag object SHA, not the commit SHA; use^{commit}peeling for commit-vs-commit comparison (#695) - Fix project-scoped tags from
.llmenv.yamlleaking into host-level plugin collection, MCP server, and throttle resolution — introducenon_project_tags()to exclude project-scoped tags from host config generation (#696) - opencode adapter not activating when
OPENCODE_CONFIG_DIRis unset (now falls back to checking ifopencodeis on PATH) (#657) - Fix read-once hook using PascalCase
filePathwhen Claude Code sends snake_casefile_path— production read-once was a complete no-op against any Read call (#724) - Move
prune_stale_sessionsfromSessionCache::load()(runs on every Read) tosave()— eliminates redundant readdir + stat per Read call (#726) - Surface silent error swallowing in config load, session-log
correlation, and setup detection — add
inspect_errdiagnostics before.ok()/.ok()?/unwrap_or_default()that silently discarded errors (#731, #710, #712, #713)
Added
- Add
llmenv upgradesubcommand for self-upgrade from GitHub releases (--check,--track beta|release,features.upgrade.trackconfig option) (#686) - Add model provider configuration
(
capabilities.model_providers) with schema types, validation, merge rules, and CrushAdapter rendering (#526, #527, #528) - Add default model selection
(
capabilities.default_models) for role-keyed model resolution across providers (#530) - Add content-based scope matching with file glob
patterns (
scope.content) — auto-activates tags when matching files exist in the working directory, without requiring.llmenv.yamlmarkers (#278) - Cache hashing now supports
version: majorgranularity — sethashing: { normal: { version: major } }in config.yaml to key cache folders on major version only (e.g.1/instead of1.2/). Default remainsminorfor full backward compatibility. (#651) - opencode engine support — new
opencodeadapter with full parity vs the claude-code adapter: AGENTS.md, rules, skills, MCP (local/remote), LSP, permissions, hook bridging via a generated JS shim plugin, and Claude-plugin content translation (#656, #657) - JSON Schema generation for materialized configs — adapters that
derive
JsonSchemaon their output structs now emit a{adapter}.schema.jsonsidecar alongside the native config file, enabling IDE validation and editor autocompletion for materialized opencode.json files. (#660) - Add read-once file deduplication hook — tracks files
read via the Read tool within a session and skips
re-reading unchanged files within a configurable TTL
(
features.read_once). Includes deny-mode envelope to block writes to never-read files (#318) - Add slippage control bundle — effort-level injection
and compaction-survival rules to improve agent behavior
consistency across long sessions
(
features.slippage) (#317) - Add TTL-based memory retention pruning
(
llmenv memory prune,memories.retentionconfig with per-type durations,memories.auto_pruneflag during materialize) (#270) - Add post-session LLM consolidation — after SessionEnd, distills recent memories into permanent semantic rules via direct Anthropic API call, reducing context drift across sessions (#595)
[3.2.0] - 2026-07-11
Changed
- Move WebFetch/WebSearch ICM storage and PostSession consolidation to background detached child processes, reducing hook latency for common events (#670)
- Cache parsed config by file mtime in hook-run to avoid redundant YAML parsing on each event (#670)
Added
llmenv doctorchecks that config-dependent executables (icm,mcp-proxy/uvx,claude,crush) are available onPATH, respecting each tool's config conditions (memory entries, disabled engines, optional status). (#655)- Add Discord community link to README and getting-started guide
Fixed
capabilities.permissionsandnative_permissionsrules (top-level or bundle-contributed) whosepattern/pathshave unbalanced parentheses — e.g. a process-substitution deny pattern likebash <(curl *— are now rejected at config-load time with a fix hint, instead of rendering into aTool(pattern)string that Claude Code/Crush silently drop at settings-load time. This previously leftdenyrules silently non-functional with no warning fromllmenv doctoror config validation. (#664)- Validate skill-file paths with CommonMark-aware parsing (
pulldown-cmark) instead of fragile heuristics. Fenced/indented code blocks and inline code spans containing~/.claudeno longer falsely trigger configuration-path validation errors. (#659) - Fix root-level
lsp:andskills:declarations inconfig.yamlnot being materialized into the rendered manifest. These were parsed, validated, and documented but silently never reached the output. (#661) - Fix false
"marketplace.json broken"warning fromllmenv doctorwhen the context-mode marketplace clone is properly synced but lacks a standalonemarketplace.json— the marketplace is managed internally and the check was a false positive - Fix loopback address detection in the ICM MCP SSRF guard to cover the
full
127.0.0.0/8range, unspecified addresses (::,::0,0.0.0.0), and provide a safer fallback whenneeds_proxycannot be determined - Fix background PostSession consolidation child process inheriting stdin, which could cause hangs; add trace logging for CONFIG_CACHE poison detection
[3.1.0] - 2026-07-10
Added
- Auto-activate OS tag in scope resolution — bundles with OS-specific
when:tags (e.g.linux,macos,windows) now activate automatically without requiring manual scope configuration (#638) - Create plugin cache directory automatically on export (
CLAUDE_CODE_PLUGIN_CACHE_DIR), and addllmenv prune --plugin-cacheflag for explicit shared plugin cache cleanup (#643)
Fixed
- Build static Linux binaries with musl (
*-linux-musl) instead of glibc (*-linux-gnu) so the pre-built Homebrew-tap binaries work on any Linux distro regardless of system glibc version (#647) - Fix typos in
llmenv pruneoutput text
[3.0.0] - 2026-07-10
Major changes since v2.4.1
This release introduces a multi-engine architecture (Crush alongside Claude Code), a built-in persistent memory system via ICM, automatic context-mode integration, and a new interactive setup wizard. Full granular changeset in the rc.1 and rc.2 sections below.
- Multi-engine support — llmenv now drives Crush as a second agent engine
alongside Claude Code.
export/hook/regenerateiterate all installed adapters. The CrushAdapter renders hooks, MCP servers (stdio/SSE/HTTP), LSP, permissions, and skills against Crush's actual schema. - ICM Memory System — Built-in persistent memory with session logging
(transcript + JSONL file), CLI observability (
llmenv memory stats|list|diff|prune), importance/type annotations, consolidation groundwork, andSessionStart/SessionEndlifecycle hooks that actually wire memory wake-up and store. - Context-mode integration — Enabling
features.context_modeauto-wires the context-mode plugin: marketplace clone, MCP server, durable data dir, and permissions. Supersedes the removedLLMENV_BASH_BAN. llmenv setupwizard — Interactive command that scans existing tool configs (~/.claude,~/.cursor), prompts for preferences, and generates a validatedconfig.yamlwith starterAGENTS.md.- First-class LSP & Skills — Declare language servers (
name,command,filetypes,init_options, etc.) and skills directly in config or bundles, tag-scoped and independent of the plugin model. - MCP field parity —
headers,disabled,disabled_tools, andtimeouton MCP server entries. - Config validation & observability —
llmenv doctorwarns on dangling bundle dirs, unused marketplace entries, and orphanednative_permissions.disabled_enginesskips rendering for named engines. Token-efficiency checks indoctor,--compressexport flag. - BREAKING:
session_logis now a mapping ({ file, transcript, verbose, path, max_content_bytes }) instead of a path string. The old string form is rejected with a migration hint. - Removed:
LLMENV_BASH_BANenv var; superseded by context-mode.
Changes since v3.0.0-rc.2
- Forward-merged from 2.4.0: per-hash
CLAUDE_CODE_TMPDIRtemp isolation andCLAUDE_CODE_PLUGIN_CACHE_DIRdurable plugin cache (#630, #632) - Forward-merged from 2.4.0:
CONTEXT_MODE_DATA_DIRand other state-directory env vars now emit forward-slash paths on all platforms (#497) llmenv doctorstructural validation: dangling bundle directories, unused marketplace entries, orphanednative_permissionskeys (#604)- CI: trusted publishing to crates.io via OpenID Connect
[3.0.0-rc.2] - 2026-07-09
Added
llmenv setupinteractive wizard: scans existing tool configurations (~/.claude,~/.cursor), prompts for GitHub repo and bundle organization, and generates a validatedconfig.yamlwith starterAGENTS.md. (#561, #575)llmenv setup --rescan: re-read existing tool configs and refresh the enumeration JSON without overwriting config.yaml, AGENTS.md, or bundle contents. Composes with--no-launchand--path. (#576)- The Claude Code adapter now renders
capabilities.lsp: entries with anextension_to_languagemap (new field, e.g.{".rs": "rust"}) render into a synthetic skills-directory plugin (skills/llmenv-lsp/.claude-plugin/plugin.json), which Claude Code auto-loads with no marketplace or install step — its only LSP surface is a plugin'slspServersmanifest key. Entries without the map are skipped (with a warning) rather than rendered incorrectly, since the existingfiletypesfield (language ids) doesn't reliably convert to Claude's required extension-to-language form. (#556) CrushAdapterhardening: incompatible hook events,mcp_toolhooks, and non-skill plugin content (agents/,commands/,hooks/) now warn and skip instead of hard-erroring the entire render — one unsupported piece no longer blocks Crush output altogether. (#543)llmenv doctornow reports, by name, every hook event that aPATH-detected adapter can't materialize (e.g. Crush skipping aPostToolUsehook), and its token-efficiency checks now count a var as set if it's declared innative.claude_code.env, not only in the live process environment. (#543)- Top-level
disabled_enginesconfig list: skip rendering for named engines (e.g.claude_code,crush) even when their binary is onPATH. An entry that doesn't match any registered engine prints a warning on everyexport/regenerate/doctorrun (not justllmenv validate). Matching is case-insensitive, soClaude_CodeorCRUSHdisable the same engines as their lowercase form, and the--engineflag's own unknown-engine check now matches case-insensitively too. (#562, #564) - Add optional
<!-- llmenv-type: episodic|semantic|procedural -->HTML-comment marker in context chunks to classify stored memories by type. Types persist as ICM memory metadata and can be filtered in recall. Configurable default viadefault_typeon memory server entries. (#267) - Add
llmenv memory stats|list|diff|pruneCLI subcommand for ICM store observability.statsshows record counts,listdumps memories for the active scope,diffhighlights changes since the last session snapshot. (#268) - Add optional
<!-- llmenv-importance: low|medium|high|critical -->marker to tag memory importance at write time. Configurable per-type defaults viatype_importancemap on memory server entries. SessionEnd writes now skip duplicate chunks when unchanged. (#269) - Add
consolidationconfig section withenabledandmax_rules_per_sessionfields. Wires a diagnostic consolidation hook into the SessionEnd lifecycle; LLM integration deferred. (#271, #595) - Add three structural validation checks to
llmenv doctor: warn on dangling bundle directories (declared but missing on disk), unused marketplace entries (defined but unreferenced), and orphanednative_permissionskeys (no matching MCP server or engine adapter) (#604)
Changed
- Replace stale Claude Code env var table in
docs/env-vars.mdwith a link to the upstream docs
Fixed
- Fix
export/regeneratenever actually materializing Crush output: the internal materialization step ignored which adapter was passed in and always rendered Claude Code's layout, socrush.jsonandCRUSH_GLOBAL_CONFIG/CRUSH_GLOBAL_DATAwere never produced even withcrushonPATH.regeneratealso gained the same per-adapterPATH-gated loopexportalready had. (#543) - Fix
CrushAdapterhard-erroring the entire render over a single incompatible hook event,mcp_toolhook, or plugin withagents//commands//hooks/content — one unsupported bundle previously blocked Crush output altogether. Incompatible pieces are now skipped with a warning naming them; everything Crush can support still materializes. (#543) - Fix
LLMENV_STATE_DIR(and other configured tool-state relocation vars) getting silently overwritten with the wrong adapter's state directory once more than one adapter materializes in the sameexport/regeneraterun — the durable-state feature is scoped to tools writing intoCLAUDE_CONFIG_DIR, so it now only runs for the Claude Code adapter instead of once per adapter. (#543) - Fix unbounded, non-timeout-bounded DNS resolution in the ICM MCP client's SSRF
guard:
validate_url_productionresolved domain hosts via a plain blockingto_socket_addrs()call before the 2sHOOK_TIMEOUTwas ever applied, so a slow or failing DNS resolver could hangllmenv hook-run— including the per-promptturn_starthook — for minutes instead of seconds. Resolution is now bounded by the same timeout via a dedicated helper. (#547) - Fix
CrushAdapterexportingCRUSH_GLOBAL_CONFIGpointing directly at the renderedcrush.jsonfile instead of the directory containing it. Crush's own config loader joinscrush.jsonontoCRUSH_GLOBAL_CONFIGitself, so the file-path value made it look forcrush.json/crush.jsonand fail to load —crushcouldn't start with any llmenv-managed config.CRUSH_GLOBAL_CONFIGnow points at the cache directory, matching the original design intent. (#551) - Fix
CrushAdapterrendering hooks in Claude Code's nested{matcher, hooks: [{type, command, tool}]}shape instead of Crush's flatHookConfig({matcher?, command}) — Crush read an emptycommandoff the wrapper object and rejected the whole config withhook PreToolUse[0]: command is required, so no hook (or any other capability sharing the render) ever reached Crush. Also ports Claude Code's bundle-relative hook-script path resolution (a barehooks/foo.shin a hookcommandresolves against the bundle's directory) into the shared adapter helper so Crush benefits from it too — it previously only ran for Claude Code, leaving a bundle-authored relative script path broken under Crush. (#551) - Fix
CrushAdapterrendering MCP servers, LSPinit_options, and permissions in Claude Code's shapes instead of Crush's actual schema (crush.json schema), found by auditing the adapter against it: every MCP server previously failed to initialize because Crush's requiredtypefield (stdio/sse/http) was either missing (stdio entries) or set to the nonexistent value"remote"(remote entries) — Crush's MCP client hits anunsupported mcp typeerror for anything else. LSPinit_optionswas written under Claude Code'sinitializationOptionskey, so Crush's plainjson.Unmarshalsilently dropped it.permissions.denied_tools/default_modewere also dropped — Crush'sPermissionsConfighas onlyallowed_tools; not a security regression (Crush already denies-by-default outside the allow-list), but dead output. The full rendered config (all three MCP transports, hooks, LSP, permissions) now validates against the real schema with zero violations. (#554) - Fix the ICM memory backend (
session_start/turn_start/session_end) being completely non-functional whenever it resolved to loopback or a private-network address — the documented common topology (AGENTS.md: "the resolved icm MCP endpoint can be a remoteicm serve"). Four bugs stacked, each masking the next: the SSRF guard rejected loopback/private/ULA outright (now split intoSsrfPolicy::PublicOnlyvs.AllowPrivateNetwork, the latter used by the ICM client); the client never sent theAcceptheader MCP's Streamable HTTP transport requires (406); the client never performed the MCPinitializesession handshake the transport requires (400 missing session ID); and theSessionEndstore action never sent the tool's requiredtopicfield. All four fixed together; verified end-to-end against a live ICM server. (#548) - Fix remaining hardcoded ClaudeCodeAdapter call sites: thread the actual adapter identity through
build_and_materialize,run_export,run_regenerate,run_prune,run_doctor,run_throttle_inner, andhook_runinstead of assuming Claude Code (#544) - Fix skill materialization rejecting a
SKILL.mdwhosedescriptioncontains a colon (e.g. "Triggers on: ...");name/descriptionvalues are now auto-quoted before the strict YAML parse so a single malformed-looking skill no longer takes down the whole adapter (#568) - Fix bundle hook paths in generated
settings.jsonreferencing the source directory instead of the materialized cache directory. Hook paths now resolve against the cache copy via two-pass resolution — direct join for clean relative paths, suffix-match against the materialized manifest for shell-variable/absolute prefixes — with longest-suffix matching and path-boundary checks to prevent ambiguous matches. (#162) - Fix memory deduplication snapshot being written before the MCP store call completed.
A transient store failure left the snapshot ahead of reality, causing the next
SessionEndto skip the store and permanently lose the memory chunk. - Fix unknown keys under
features:silently degrading instead of producing a clear error;Featuresnow rejects unknown fields at parse time. (#602) - Fix skills with the same name from different bundles colliding in materialization after tag filtering; skills are now deduplicated by name, keeping the first occurrence. (#600)
- Fix
llmenv doctornot verifying the context-mode marketplace clone exists whenfeatures.context_mode.enabledis true; now warns if the marketplace hasn't been synced yet. (#601) - Fix example bundle hook matchers using glob patterns (
*.rs,*.py,*.ps1) instead of valid tool-name regexes; corrected to^(Edit|Write|MultiEdit)$. (#605) - Fix example bundle commands containing unsubstituted template placeholders and incorrect ICM CLI usage instead of ICM MCP calls. (#606)
- Fix example
fyiapp: race-condition inmkdirlock inrefresh.sh, missingTypeErrorin toggle handler, missingOrigincheck on POST endpoints, and phantomtopFocusinSPEC.md. (#607) - Fix example plugin augmentation: pinned slop-scan wrapper and cryptic dangling
bullet in
general.md. (#608)
[3.0.0-rc.1] - 2026-07-01
Added
features.context_modebuilt-in feature: enablingfeatures.context_mode.enabledauto-wires the context-mode plugin (marketplace, plugin, durableCONTEXT_MODE_DATA_DIR, and MCP permission) — the token-efficiency counterpart to the built-in ICM memory feature. Warns when the plugin is also declared manually in a plugin-collection. (#490)- ICM-transcript session logging: llmenv records scope + lifecycle (and, with
session_log.verbose, prompts and tool use) into ICM's transcript store via the ICM MCP, discoverable byllmenv-tag:/llmenv-bundle:tokens and project. A local JSONLfilesink mirrors the same stream, independent of ICM reachability. (#382) - The Claude Code adapter now auto-registers
SessionStart/SessionEndhooks runningllmenv hook-run, fixing a gap where the ICM memory wake-up/store dispatcher existed but was never wired into generatedsettings.json— memory wake-up/store now actually fires. Continuous per-prompt recall (turn_start) is still unwired; tracked in #499. (#382) - Multi-engine foundation for a second agent engine (Crush):
export,hook, andregeneratenow iterate a registry of engine adapters, materializing each into its own per-engine cache subtree and skipping any whose binary isn't onPATH. Claude-only users see no behavior change. Groundwork for the Crush adapter (#506); no Crush support ships yet. (#502) - Add first-class
lsp:capability: declare language servers (name,when,command,args,env,disabled,filetypes,root_markers,init_options,timeout) at the top level or inside a bundle, tag-scoped likemcp. Engines with no LSP concept (Claude Code) silently ignore them. (#503) - Add first-class
skills:capability, decoupled from plugins: declare a skill (name,path,when) directly in config or a bundle, tag-scoped, validated with the same frontmatter and path checks as plugin-bundled skills. (#504) - Add MCP server field parity:
headers,disabled,disabled_tools, andtimeouton MCP server entries. All optional — existing configs parse unchanged. (#505) CrushAdapter: Crush is now a supported engine.export/hook/regeneraterendercrush.jsonwhencrushis onPATH. What maps: permissions →allowed_tools/denied_tools(lossy, fail-closed —askrules collapse todenied_tools, never silently allowed; Crush has no ask concept); hooks →PreToolUseonly (mcp_tool-kind hooks and unsupported hook events hard-error with an actionable message); MCP servers (includingheaders,disabled_tools,timeout); LSP servers →lsp.<name>; first-class skills and plugin-projected skills →options.skills_paths. Non-skill plugin content (agents/,commands/) hard-errors naming the offending plugin.native.crush/native_permissions.crush/native_hooks.crush/native_mcp.crushmerge verbatim — provider/model config lives here until first-class provider config ships (#508). Docs in #507. (#506)
Changed
- Behavior change (dual-engine export):
export,hook, andregeneratenow iterate all registered engine adapters. Ifcrushis onPATH, a newcrush/cache subtree is materialized andCRUSH_GLOBAL_CONFIG/CRUSH_GLOBAL_DATAare exported alongside the existing Claude Code env vars. Claude-only users (nocrushbinary on PATH) see no change. (#502, #506) - BREAKING:
session_logis now a mapping ({ file, transcript, verbose, path, max_content_bytes }), not a path string. ICM transcript logging is on by default. The pre-3.0session_log: "<path>"form is rejected with a migration hint. (#382)
Removed
LLMENV_BASH_BANenv var and its deny-rule wiring. It was broken as shipped (read from llmenv's process env before bundle-declared values landed) and is superseded by the built-in context-mode feature. (#490, removes #464)
Fixed
- Fix marketplace and plugin-payload sync returning a broken clone with unstable cache key when git HEAD cannot be resolved. Now detects and errors on broken clones (after clone or pull), cleans up the corrupted directory, and forces a fresh clone on retry (#537)
Version 2.x
[2.4.1] - 2026-07-10
- CI updates to support trusted publishing to crates.io
[2.4.0] - 2026-07-10
Added
- Add per-hash temp directory isolation for Claude Code subprocesses:
CLAUDE_CODE_TMPDIR,TMPDIR,TMP, andTEMPenv vars now point to<cache_dir>/<hash>/tmp/, scoping temporary files to the current content hash (#630) - Add durable plugin cache directory:
CLAUDE_CODE_PLUGIN_CACHE_DIRnow points to<state_dir>/plugins/so plugins are not re-downloaded on every scope change (#632)
Fixed
- Fix hook context emission including
additionalContextcontent in store-only events (SessionStart, SessionEnd), which Claude Code's hook schema rejects — store-only events now emit empty output instead of triggering a validation error at the end of every session (#558) - Fix
CONTEXT_MODE_DATA_DIRand other state-directory env vars (frommaterialize::state::state_env_vars) emitting platform-native path separators (\on Windows) instead of forward slashes, breaking cross-platform compatibility for consumers that parse paths in these env vars. Normalization consolidated into the existingnormalize_relhelper. (#497)
[2.3.0] - 2026-06-30
Added
- Add
features.throttle: keep an LLM backend within its rate limits by polling usage and inserting a capped, adaptive delay as the request budget runs low, instead of hitting a hard 429. Tag-scoped likefeatures.memory; currently supports theumansbackend (#487)
[2.2.1] - 2026-06-24
Fixed
- Fix
llmenv exportaborting with "variable value contains forbidden control character" forLLMENV_ICM_CONTEXTand other legitimately multiline values; value validation now rejects only NUL, since every emission path single-quotes the value and newlines are inert there (#469)
[2.2.0] - 2026-06-23
Added
- Add built-in
token-efficiencyexample bundle with env vars (LLMENV_BASH_BAN,CBM_WARN_THRESHOLD,CBM_AUTOINDEX), SessionEnd auto-handoff hook, SessionStart context-mode reminder hook, PostToolUse reject-scanner scaffold, and minimalnative_permissionslimiting Bash to state-mutation operations (git, mkdir, curl, trash). Include per-stack rule files (bash.md,rust.md,typescript.md,skill-gates.md) documenting the skill-gate pattern for conditional skill activation by language tag, prerequisite, or indexed content (#218, #219, #220, #222, #223) - Add
--compressflag tollmenv export: strips trailing whitespace and collapses excessive blank lines for token-efficient AGENTS.md output (#226) - Wire
LLMENV_BASH_BANenv var into the Claude Code adapter permission layer: when set, denies Bash tool invocations whose commands match any comma-separated prefix pattern before execution (#464)
Fixed
- Fix
token-efficiencyexample bundle declaringBASH_BANinstead ofLLMENV_BASH_BAN; the Bash deny feature silently failed for any user of the example config (#466) - Fix
token-efficiencyexample bundle placing env vars underfeatures.envinstead of the top-levelenvkey and using snake_case hook event names instead of PascalCase (e.g.session_end→SessionEnd); env vars were not exported and hooks never fired - Fix
LLMENV_BASH_BANaccepting patterns containing),(, and newlines that produced malformed deny rules; invalid pattern characters are now rejected at startup (#465) - Fix
LLMENV_BASH_BANtreating a non-unicode env var value the same as the variable being unset; non-unicode values now return an error instead of silently disabling enforcement (#465) - Fix
llmenv export --compressnot preserving the final newline, producing non-POSIX output (#465)
[2.1.0] - 2026-06-23
Added
- Add
session_logconfig field: opt-in JSONL tracing of all llmenv log events to a file for diagnosing hooks and materialization without reading stderr (#382) - Add SSH auth negotiation timeout (
ssh -o ConnectTimeout) and HTTP pack-transfer stall detection (http.lowSpeedTime/http.lowSpeedLimit) to all git subprocesses, preventing indefinite hangs on slow or unresponsive servers (#453) - Add annotated
examples/my-llmenv/reference config: a fully commented example coveringconfig.yaml, five bundles, hooks, skills, rules, and scripts - Detect volta, fnm, Linux pnpm (
~/.local/share/pnpm/), and macOS pnpm (~/Library/pnpm/) install paths when seedinginstallMethodin Claude Code settings; previously these were classified asnative
Fixed
- Fix
GIT_SSH_COMMANDbeing overwritten by llmenv's SSH timeout injection; user SSH identity files,ProxyJump, and other existing SSH customizations are now preserved - Fix
seed_install_methodoverwriting a user-customizedinstallMethodvalue in Claude Codesettings.json; the field is now only written when absent - Fix
seed_install_methodsilently swallowing I/O errors (e.g. permission denied) when readingsettings.json; non-NotFound errors now propagate - Fix long interactive session pause when GitHub remote is unreachable: all git
subprocesses now apply a TCP connection timeout (
GIT_CONNECT_TIMEOUT— 10 s for background fetch/pull, 30 s for explicit plugin clone/fetch) - Fix malformed
marketplace.jsonentries (missing or invalidsourcefield) being silently dropped; these now emit awarnlog with the entry details (#361)
Security
- Reject NUL, newline, and carriage-return characters in env var values at config load time; these were previously accepted silently and could interfere with shell export (#356)
- Reject
file://transport in external plugin source URLs; onlyhttps://and SSH remotes are permitted (#360) - Remove
StrictHostKeyChecking=accept-newfrom llmenv's SSH options for git operations; this option weakened host-key verification (MITM/DNS-hijacking exposure) and was unrelated to the timeout feature it was grouped with
[2.0.5] - 2026-06-18
Added
- Fold six
*-lslisting commands intostatussubcommands:status bundles,status tags,status scopes,status mcps,status marketplaces,status plugins. The top-level*-lsforms are retained as hidden deprecated shims and will be removed in 2.1. - Add
context --bundle <name>to narrow the context view to a single bundle, showing its env vars, hooks, MCPs, plugins, and skills - Add
context --whyto show activation tracing — which scope triggered each active tag and which tags fired each bundle - Add
export --explainto annotate each exported variable with its source (adapter or llmenv introspection) - Add
sync --dry-runto preview pending config changes without committing - Add
check-stale --auto-fixto automatically re-materialize config on drift rather than only printing a warning - Add
validatecommand to check config for structural issues (duplicate bundle names, bundles with no activation tags) - Add
edit [bundle-name]command to openconfig.yamlor a named bundle file directly in$EDITOR - Add
completions <shell>command to generate shell completion scripts for bash, zsh, and fish - Document
regenerate,login,config-context, andconfig-guardcommands incommands.md; addregenerateandloginto thegetting-started.mdquick-reference table - Expand
doctorentry incommands.mdto list the token-efficiency settings it checks
Fixed
- Fix
editcommand allowing paths outside the config root via..traversal; the target path is now canonicalized and validated before opening - Fix
editcommand ignoring arguments in$EDITOR(e.g.code --wait); the editor value is now split on whitespace before invoking - Fix
validatenot checkingenable_bundlesreferences in project-scoped config; unknown bundle names now report an error regardless of scope type - Fix
plugin-syncsilently succeeding when a configured plugin is absent from the marketplace manifest after sync; it now prints a user-visible error and exits non-zero - Fix
statuslisting commands anddoctor --allincorrectly classifying MCPs, bundles, and plugins as orphaned when theirwhen:tags are emitted only by project scopes; the emitted-tag set now includes project-scope active tags
[2.0.4] - 2026-06-16
Added
- Provide prebuilt
linux/aarch64(ARM64) release binaries
Fixed
- Fix
hookEventNamebeing emitted at the top level of hook JSON instead of insidehookSpecificOutput; it is now nested per the Claude Code hook schema, so hooks that read the event name from context find it in the right place (#419) - Fix
llmenv plugin-syncsilently dropping all externally-sourced plugins (e.g.slack,superpowers) whosemarketplace.jsonentry uses the{"source": "git", "url": "..."}object form; only bare-string sources were parsed, so every object-form entry was lost. Malformed object-form entries now emit a warning, and the related messages correctly direct users tollmenv plugin-syncinstead ofllmenv sync - Fix hooks crashing with a broken-pipe error when the agent truncates their
stdout early; hooks are fail-soft and now exit 0 on
SIGPIPE(#422) - Fix bundle and tag memory recall errors being silently discarded; all MCP
action failures (recall, tag recall, bundle recall, store) now emit a
tracing::warn!with structured context so misconfigured or unreachable recall is diagnosable without source-level debugging (#421)
[2.0.3] - 2026-06-15
Fixed
- Fix
SessionStart(and other hook) output missing the requiredhookEventNamefield, causing Claude Code to reject hook JSON with "hookSpecificOutput is missing required field 'hookEventName'" on startup
[2.0.2] - 2026-06-14
Fixed
- Fix
cargo release --workspacenot bumping sub-crates: add explicitshared-version = trueto each sub-craterelease.tomlso cargo-release treats them as part of the workspace version group - Fix CI publish step silently timing out when sub-crate versions don't match the release tag: add upfront version validation that fails fast with a clear error message
- Fix
pre-release-hook = []panic in cargo-release 1.1.2: remove empty hook arrays from sub-crate configs and update workspace hook to use${WORKSPACE_ROOT}so it resolves correctly from any sub-crate working directory
[2.0.1] - 2026-06-14
Fixed
- Fix multi-crate crates.io publishing: enable sub-crates (
llmenv-util,llmenv-paths,llmenv-git,llmenv-config) for publishing with required metadata, bump all to 2.0.0 to match root, and publish in dependency order with crates.io indexing polls in CI
[2.0.0] - 2026-06-14
Added
- Add token-efficiency checks to
llmenv doctor: warns whenCLAUDE_AUTOCOMPACT_PCT_OVERRIDE,BASH_MAX_OUTPUT_LENGTH,MAX_MCP_OUTPUT_TOKENS, orENABLE_PROMPT_CACHING_1Hare not set (or misconfigured); informs whenCLAUDE_CODE_SUBAGENT_MODELis unset; warns when nocontext-modeMCP server is configured - Add
config::template::generate_template()function;llmenv initnow derives the config template from a single source rather than a hardcoded string, making it easier to keep the template in sync as the schema evolves - Add
llmenv config-contextsubcommand, auto-registered as aSessionStarthook by the Claude Code adapter; emits source config file and bundles directory paths ashookSpecificOutput.additionalContextso the agent always knows where to edit llmenv config rather than touching managed cache files - Add
llmenv config-guardsubcommand, auto-registered as aPreToolUsehook (matcher:Write,Edit,MultiEdit) by the Claude Code adapter; warns when the agent writes to a path inside the managed cache directory and redirects to the source config; always exits 0 (fail-soft, never blocks the write) - Add stable authentication cache:
oauthAccountcredentials are now stored instate/auth/<uuid>.jsonoutside the content-hashed config dir and automatically re-injected on every new materialization; Claude Code no longer requires re-authentication after a version bump, project switch, or directory change (#172) - Add
llmenv login [--global]subcommand: captures credentials viaclaude auth login, saves them to the stable auth cache, and optionally persists them globally (#172) - Add
init.seeded_settingstoconfig.yaml: user-selected keys from~/.claude/settings.jsonare seeded intosettings.jsonon first materialization of a new config folder, carrying over preferences without overwriting managed settings;llmenv initnow prompts to log in, import from~/.claude, or skip (#172) - Add per-bundle
features.memoryoverrides: bundles can declare afeatures:block inbundle.yamlto use a different memory daemonserver_hostper scope, enabling different daemons on different machines or networks without a global config change (#335)
Changed
- Replace ASCII pipeline and precedence diagrams in the concepts and philosophy documentation pages with Mermaid flowcharts; the diagrams now render as proper graphs on the Docusaurus docs site
Removed
- Breaking: Remove
env(and its deprecated aliasvars) from the top-levelbundle:config field. Bundle-level environment variables must now be declared inbundle.yamlundercapabilities.env. (#352)
Fixed
- Fix
config-guardpath-prefix check accepting..-based traversal paths (e.g.~/.cache/llmenv/../../../etc/shadowmatched as inside the cache); paths are now normalized lexically before the prefix check - Fix
config-guardsilently swallowing JSON parse failures when the hook payload was malformed; non-empty non-JSON stdin now logs a warning to stderr - Fix
config-guardnot logging whenCLAUDE_CONFIG_DIRis set but has no recognizableclaude-codeancestor directory; the fallback is now visible to operators - Fix
config-contextsilently substituting a wrong default path when config path resolution fails; it now emits a warning to stderr and returns a degraded-state context message rather than feeding the agent incorrect file paths - Fix missing bundle directories being silently ignored;
llmenvnow logs a warning when a configured bundle name has no corresponding directory, making typos and deleted directories detectable - Fix
mcp[].envkeys not being validated for theLLMENV_prefix or reserved state vars (CLAUDE_CONFIG_DIR,LLMENV_STATE_DIR); these were accepted silently wherecapabilities.envalready rejected them, creating an inconsistent validation gap - Fix git fetch spawn errors logged at
debuglevel in the background sync path; a spawn error (git binary missing or misconfigured) is unexpected and is now logged atwarnso operators can see it - Fix git reset errors during explicit plugin sync silently logged at
debuglevel; errors are now logged atwarnso sync failures surface in production logs (#376) - Fix clock skew silently bypassing the pull throttle check; when the stored sync
timestamp is in the future,
llmenvnow logs awarnwith the skew magnitude (skew_secs) and proceeds with the pull rather than silently skipping it (#377) - Fix missing
plugin.jsonafter a plugin sync being silently ignored;llmenvnow logs awarnwhen the plugin manifest is absent after materializing the plugin, making broken plugin installs diagnosable (#379)
Version 1.x
[1.0.10] - 2026-06-11
Added
llmenv plugin-syncnow fetches externally-sourced plugins — those whosesourceinmarketplace.jsonis a git URL rather than a relative path within the marketplace clone. Payloads are cloned to a stable path outside the hash-keyed config dir so they survive config changes without requiring a manual/plugin installor re-authentication (#353)
Fixed
- Fix
env:declared in a bundle'sbundle.yamlbeing silently dropped; bundle-level env vars are now merged and exported alongsideBundle.vars(#351) - Reject reserved env var names (
CLAUDE_CONFIG_DIR,LLMENV_STATE_DIR) and theLLMENV_*prefix incapabilities.envat validation time; silently setting these would shadow adapter-emitted vars and produce conflicts that are impossible to diagnose at runtime (#354) - Detect same-precedence conflicts in
capabilities.envkey merging and error with the contributor names and values, matching the existingdefault_modeconflict behaviour; previously one of the conflicting values would silently win (#355)
[1.0.9] - 2026-06-10
Fixed
- Fix
memory.listen_hostunspecified-address warning emitting on every shell prompt; the warning now only appears when the ICM proxy actually starts or restarts (#347)
[1.0.8] - 2026-06-09
Added
- Memory server now supports a
listen_hostoption underfeatures.memory(default"127.0.0.1"). Set to"0.0.0.0"to accept connections on all interfaces, or to a specific IP to bind to one interface. Fixes #337.
Fixed
- Fix shell hook functions (
__llmenv_precmd,__llmenv_prompt) triggering a full environment render inside non-interactive subshells (e.g. Claude Code's Bash tool); add early-return guards for both$-interactivity and$LLMENV_STATE_DIRalready-active checks (#338) - Fix empty directories left in rendered output when a bundle contributes no
files to a subdirectory;
create_dir_allis now followed by a bottom-up prune pass that removes empty dirs without touching the output root (#336)
[1.0.7] - 2026-06-05
Added
- Add
mcp:support inbundle.yaml; declare MCP servers inside a bundle using the same format asconfig.yaml; tagless entries are active whenever the bundle is selected, tagged entries are further filtered by active scope tags (#329) llmenv initnow generates aREADME.mdorientation file in the config directory on first run; the write is skipped if aREADME.mdalready exists (#325)
Fixed
- Fix bundle
mcp:entries accepting names with characters outside[a-zA-Z0-9_-]; invalid names are now rejected with a clear error (#329) - Fix missing collision detection between
config.mcpand bundlemcp:entries; a name declared in both sources now errors at startup instead of silently producing duplicate servers (#329) - Fix
mcp-lsomitting bundle-declared MCP servers; bundle MCPs are now listed with a(bundle)annotation and correct active/orphan status (#329) - Fix bundle
mcp:entries accepting the reserved nameicm; the guard now matches the one already present for top-levelconfig.mcp(#329) - Fix
llmenv initemitting a config.yaml template with a nestedtransport:block for MCP servers; the correct flat schema (type/command/argsat the top level) is now emitted (#325) - Fix
llmenv initsilently replacing non-UTF-8 path bytes with?; non-UTF-8 paths now fail with a clear error (#325)
[1.0.6] - 2026-06-05
Added
- Add
effort_levelandadvisor_sizeas first-class capability fields; rendered intosettings.jsonaseffortLevelandadvisorSizefor engine adapters to consume (advisor_sizeuses generic sizes"small","medium","large"so adapters map to engine-specific models vianativeoverrides) - Add
envfield toNetworkScope,HostScope, andUserScope; environment variables declared on a scope are injected when that scope matches, extending the existing bundle-level env-var pattern to all scope types - Add GitHub Actions workflow to auto-close issues when PRs merge to
release/*branches; GitHub's native auto-close only works on the default branch, so this workflow parses merged PR bodies for closing keywords and closes referenced issues via the API - Add GitHub Actions workflow to forward-merge
release/*branches through the release chain intomain; a fix pushed to an older release line cascades forward through newer lines automatically, opening a labeled PR (and halting) on the first conflict or protected branch instead of being dropped
Changed
- Rename
bundle.varstobundle.env; the old keyvarsis still accepted as a backward-compatible alias so existing configs continue to work
Fixed
- Fix
mcp-proxyspawned duringllmenv exportinheriting the calling shell's stdio; when the export was sourced over SSH viasource <(llmenv export)the proxy wrote its logs into the process-substitution pipe, flooding the terminal withcommand not found: INFO:lines. The proxy now redirects stdio to/dev/nulland starts in its own process group so terminal job-control signals no longer reach it - Fix
llmenv syncsilently reporting success whengit pushfailed; a rejected or failed push is now surfaced as an error with git's own message - Fix git operations potentially hanging on a credential prompt when run with a
non-interactive stdin (CI, or a sourced
llmenv export); all git subprocesses now detach stdin so they fail fast instead of blocking - Fix materialized skills failing silently when they referenced bundled scripts
via hardcoded
~/.claudepaths; such paths resolve against the default config dir, not the materialized folder llmenv actually boots. Materialization now rejects skills (and rules/CLAUDE.md) carrying~/.claudeor$HOME/.claudepaths, naming the offending file - Fix marketplace
git clone/fetchfailures hiding git's diagnostic output; the underlying stderr is now surfaced (auth failure, bad URL, disk full are distinguishable) with any embedded credentials scrubbed from the message - Fix
llmenvconfig auto-pull silently swallowing a failed fast-forward (diverged history, conflict, network); a one-line nudge now points atllmenv syncinstead of failing invisibly on every shell prompt
[1.0.5] - 2026-06-03
Changed
- GitHub release notes now include inline SHA256 checksums and the changelog
section for the released version; checksums no longer require downloading a
separate
checksums.txtattachment to verify
Fixed
- Fix documentation referencing
mcp.jsonfor MCP server configuration; servers have been written tomcpServersin.claude.jsonsince v1.0.0 - Fix
state:key andfeatures.memory:subsection missing from configuration reference - Fix
hook-runcommand and command aliases (scopes,tags,bundles,mcps,marketplaces,plugins) missing from commands reference - Add SLSA provenance verification instructions to release documentation; SLSA artifacts have been published since v1.0.0 but were undocumented
[1.0.4] - 2026-06-03
Aborted release. CI pipeline issue.
[1.0.3] - 2026-06-03
Fixed
- Fix
reconcile_settingssilently dropping native passthrough keys (e.g.statusLine,cleanupPeriodDays) on re-renders whensettings.jsonalready exists; non-owned keys fromfreshare now written through on every render
[1.0.2] - 2026-06-02
Fixed
- Fix marketplace sync failure silently dropping
CLAUDE_CONFIG_DIRon export; missing local clone now warns and continues rather than propagating an error that exited 0 without emitting the env var (#281) - Fix
run_exportallowingbuild_and_materializefailures to exit 0 without emittingCLAUDE_CONFIG_DIR; build failures now exit non-zero (#281) - Fix materialize creating empty cache directories when source bundles are deleted or moved (#285)
- Fix
doctorfalsely reporting marker-enabled bundles (e.g.rust-dev,python-dev) as orphans when no project marker is currently active (#284) - Fix
doctorsuppressing legitimate orphan warnings due to overly-broad marker-driven heuristics matching non-marker bundles and tags - Add remediation hint (
llmenv plugin-sync) to marketplace unavailability warning during export
[1.0.1] - 2026-06-02
Added
- Add changelog to Docusaurus documentation site (#258)
Fixed
- Fix documentation links in README; correct missing
/docs/path segment in several links (#265, #266)
[1.0.0] - 2026-06-01
Added
- Add
llmenv doctordiagnostic command with config, cache, and git health checks;--gcflag for garbage collection;cache_retention_hourssetting (default 168 hours) - Add
llmenv prunecommand with--all,--older-than <duration>, and--dry-runflags; symlink-safe deletion, orphaned*.tmpstaging dirs always cleared (#63) - Add
llmenv synccommand for on-demand configuration synchronization with configurable sync interval - Add
hook-runcommand for engine-neutral lifecycle event dispatching (session_start,turn_start,session_end); hooks degrade gracefully on failure so they never block the agent (#171) - Add ICM-aware Claude Code adapter: auto-merges MCP servers into
.claude.json, suppresses native auto-memory when ICM is active, and registerscheck-staleSessionStarthook for drift detection (#121, #122, #123, #124) - Add per-feature
nativeoverride maps (native_permissions,native_hooks,native_plugins,native_mcp) for engine-specific config passthrough; catch-allnative.<engine>block for unmodeled keys; modeled-feature keys in the catch-all are a hard error (#96, #97, #102) - Add first-class plugin and marketplace support with git and local sources;
Claude Code adapter renders
extraKnownMarketplacesandenabledPluginsintosettings.json; newmarketplace-ls,plugin-ls, andplugin syncCLI commands (#59) - Add engine-neutral permission rule rendering into Claude Code
settings.jsonwith native suppression (deny is authoritative over allow/ask) (#34) - Add cross-project tag-scoped memory recall via
turn_starthook; tags validated before expansion to prevent metacharacter injection (#197) - Add
--color <auto|always|never>flag withNO_COLORandCLICOLOR_FORCEsupport; colored markers intag-ls,scope-ls,bundle-ls,doctor, andstatus(#62) - Add scope matching via WiFi SSID, hostname, OS user, and project markers
(e.g.
.llmenvrc) - Add bundle system for tag-activated environment variable groups; multiple bundles can be active simultaneously
- Add zsh and bash shell integration with throttled configuration sync via shell hooks
- Add scope-aware MCP server integration with automatic process lifecycle management and server binding configuration
- Add MIT and Apache-2.0 license texts with per-dependency attribution via
cargo-about;cargo denygates license policy in CI and on pre-push (#253) - Add user documentation: getting-started guide, configuration schema reference, ICM topology/MCP integration guide, and updated README
Changed
- BREAKING: Replace two-knob
cache.hashing: strict|version+cache.version_fidelityconfig with singlecache.hashing: loose|normal|strict(defaultnormal);normal→<adapter>/<version_mm>/<shape>/,loose→<adapter>/<shape>/,strict→<adapter>/<VERSION_TAG>-<content_hash>/; existing configs using the old keys must migrate (#246) - BREAKING: Write MCP servers to
mcpServersobject in.claude.jsoninstead of standalonemcp.json; foreign keys are preserved on read-modify-write merge; remote servers now carry an explicit"type"field;enabledMcpjsonServersis no longer emitted (#244) - Change config format from TOML to YAML (
~/.config/llmenv/config.yamlreplacesconfig.toml);llmenv initemits YAML; migrated from deprecatedserde_yamltoserde_yaml_ng(#76) - Change
hook-runfrom multi-threaded to current-thread tokio runtime, reducing startup overhead on the agent hot path; fail-soft contract locked by integration tests (#186, #187, #189)
Fixed
- Fix
llmenv prunecounting symlinks as removed when unlink failed; failures are non-fatal but now logged and reported under a separatefailedlist (#255) - Fix corrupt
.llmenv-manifest.jsonbeing discarded silently; parse failure now emits atracing::warn!(#247) - Fix deep-merge producing duplicate sequence entries, making
merge(merge(x)) != merge(x); all write paths normalize on insert (#107, #108, #109, #110, #111) - Fix path traversal detection to parse path components instead of substring
matching; catches trailing
foo/..patterns the old checks missed (#65) - Fix shell variable name validation
- Fix shell metacharacter escaping in exported variables
- Improve error messages with operation context and actionable guidance
Security
- Validate env var names at source in
build_and_materializein addition to the final emission loop, preventing injection in theexport NAME=...shell contract (#67)