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 3.x
[Unreleased] - ReleaseDate
Added
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
-
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 for a tool opencode has no key for is now reported with a visible
warning:line naming the tool. It previously went to a log level the default filter discards, so the dropped rule left no trace anywhere.Skillis also mapped to opencode'sskillkey now instead of being dropped, since it has an exact equivalent. See Engines (#1345) - 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 a plugin cache entry could not be removed. 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)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)
[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)