# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/),
and this project adheres to [Semantic Versioning](https://semver.org/).

<!-- 3.0 next-header -->

## [Unreleased] - ReleaseDate

## [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`/`regenerate` iterate 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, and `SessionStart`/
  `SessionEnd` lifecycle hooks that actually wire memory wake-up and store.
- **Context-mode integration** — Enabling `features.context_mode` auto-wires
  the context-mode plugin: marketplace clone, MCP server, durable data dir,
  and permissions. Supersedes the removed `LLMENV_BASH_BAN`.
- **`llmenv setup` wizard** — Interactive command that scans existing tool
  configs (`~/.claude`, `~/.cursor`), prompts for preferences, and generates a
  validated `config.yaml` with starter `AGENTS.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`, and `timeout`
  on MCP server entries.
- **Config validation & observability** — `llmenv doctor` warns on dangling
  bundle dirs, unused marketplace entries, and orphaned `native_permissions`.
  `disabled_engines` skips rendering for named engines. Token-efficiency checks
  in `doctor`, `--compress` export flag.
- **BREAKING:** `session_log` is 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_BAN` env var; superseded by context-mode.

### Changes since v3.0.0-rc.2

- Forward-merged from 2.4.0: per-hash `CLAUDE_CODE_TMPDIR` temp isolation and
  `CLAUDE_CODE_PLUGIN_CACHE_DIR` durable plugin cache (#630, #632)
- Forward-merged from 2.4.0: `CONTEXT_MODE_DATA_DIR` and other state-directory
  env vars now emit forward-slash paths on all platforms (#497)
- `llmenv doctor` structural validation: dangling bundle directories, unused
  marketplace entries, orphaned `native_permissions` keys (#604)
- CI: trusted publishing to crates.io via OpenID Connect

## [3.0.0-rc.2] - 2026-07-09

### Added

- `llmenv setup` interactive wizard: scans existing tool configurations
  (`~/.claude`, `~/.cursor`), prompts for GitHub repo and bundle organization,
  and generates a validated `config.yaml` with starter `AGENTS.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-launch` and `--path`. (#576)
- The Claude Code adapter now renders `capabilities.lsp`: entries with an
  `extension_to_language` map (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's `lspServers` manifest key. Entries without the map are
  skipped (with a warning) rather than rendered incorrectly, since the existing
  `filetypes` field (language ids) doesn't reliably convert to Claude's required
  extension-to-language form. (#556)
- `CrushAdapter` hardening: incompatible hook events, `mcp_tool` hooks, 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 doctor` now reports, by name, every hook event that a `PATH`-detected
  adapter can't materialize (e.g. Crush skipping a `PostToolUse` hook), and its
  token-efficiency checks now count a var as set if it's declared in
  `native.claude_code.env`, not only in the live process environment. (#543)
- Top-level `disabled_engines` config list: skip rendering for named engines
  (e.g. `claude_code`, `crush`) even when their binary is on `PATH`. An entry
  that doesn't match any registered engine prints a warning on every
  `export`/`regenerate`/`doctor` run (not just `llmenv validate`). Matching is
  case-insensitive, so `Claude_Code` or `CRUSH` disable the same engines as
  their lowercase form, and the `--engine` flag'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 via `default_type` on memory server entries. (#267)
- Add `llmenv memory stats|list|diff|prune` CLI subcommand for ICM store observability. `stats`
  shows record counts, `list` dumps memories for the active scope, `diff` highlights 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 via `type_importance` map on memory
  server entries. SessionEnd writes now skip duplicate chunks when unchanged. (#269)
- Add `consolidation` config section with `enabled` and `max_rules_per_session` fields.
  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 orphaned `native_permissions` keys (no matching MCP server or
  engine adapter) (#604)

### Changed

- Replace stale Claude Code env var table in `docs/env-vars.md` with a link to the
  [upstream docs](https://code.claude.com/docs/en/env-vars)

### Fixed

- Fix `export`/`regenerate` never actually materializing Crush output: the internal
  materialization step ignored which adapter was passed in and always rendered Claude
  Code's layout, so `crush.json` and `CRUSH_GLOBAL_CONFIG`/`CRUSH_GLOBAL_DATA` were never
  produced even with `crush` on `PATH`. `regenerate` also gained the same per-adapter
  `PATH`-gated loop `export` already had. (#543)
- Fix `CrushAdapter` hard-erroring the *entire* render over a single incompatible hook
  event, `mcp_tool` hook, or plugin with `agents/`/`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 same `export`/`regenerate` run — the durable-state
  feature is scoped to tools writing into `CLAUDE_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_production` resolved domain hosts via a plain blocking
  `to_socket_addrs()` call before the 2s `HOOK_TIMEOUT` was ever applied, so a slow
  or failing DNS resolver could hang `llmenv hook-run` — including the per-prompt
  `turn_start` hook — for minutes instead of seconds. Resolution is now bounded by
  the same timeout via a dedicated helper. (#547)
- Fix `CrushAdapter` exporting `CRUSH_GLOBAL_CONFIG` pointing directly at the rendered
  `crush.json` file instead of the directory containing it. Crush's own config loader
  joins `crush.json` onto `CRUSH_GLOBAL_CONFIG` itself, so the file-path value made it
  look for `crush.json/crush.json` and fail to load — `crush` couldn't start with any
  llmenv-managed config. `CRUSH_GLOBAL_CONFIG` now points at the cache directory, matching
  the original design intent. (#551)
- Fix `CrushAdapter` rendering hooks in Claude Code's nested `{matcher, hooks:
  [{type, command, tool}]}` shape instead of Crush's flat `HookConfig` (`{matcher?,
  command}`) — Crush read an empty `command` off the wrapper object and rejected the
  whole config with `hook 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 bare `hooks/foo.sh` in a hook
  `command` resolves 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 `CrushAdapter` rendering MCP servers, LSP `init_options`, and permissions in
  Claude Code's shapes instead of Crush's actual schema
  (https://charm.land/crush.json), found by auditing the adapter against it: every
  MCP server previously failed to initialize because Crush's required `type` field
  (`stdio`/`sse`/`http`) was either missing (stdio entries) or set to the
  nonexistent value `"remote"` (remote entries) — Crush's MCP client hits an
  `unsupported mcp type` error for anything else. LSP `init_options` was written
  under Claude Code's `initializationOptions` key, so Crush's plain
  `json.Unmarshal` silently dropped it. `permissions.denied_tools`/`default_mode`
  were also dropped — Crush's `PermissionsConfig` has only `allowed_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 remote `icm serve`"). Four bugs stacked, each masking the next:
  the SSRF guard rejected loopback/private/ULA outright (now split into
  `SsrfPolicy::PublicOnly` vs. `AllowPrivateNetwork`, the latter used by the ICM
  client); the client never sent the `Accept` header MCP's Streamable HTTP
  transport requires (406); the client never performed the MCP `initialize`
  session handshake the transport requires (400 missing session ID); and the
  `SessionEnd` store action never sent the tool's required `topic` field. 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`, and `hook_run` instead of assuming Claude Code (#544)
- Fix skill materialization rejecting a `SKILL.md` whose `description` contains a colon (e.g.
  "Triggers on: ..."); `name`/`description` values 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.json` referencing 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
  `SessionEnd` to skip the store and permanently lose the memory chunk.
- Fix unknown keys under `features:` silently degrading instead of producing a clear
  error; `Features` now 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 doctor` not verifying the context-mode marketplace clone exists when
  `features.context_mode.enabled` is 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 `fyi` app: race-condition in `mkdir` lock in `refresh.sh`, missing
  `TypeError` in toggle handler, missing `Origin` check on POST endpoints, and
  phantom `topFocus` in `SPEC.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_mode` built-in feature: enabling `features.context_mode.enabled`
  auto-wires the context-mode plugin (marketplace, plugin, durable
  `CONTEXT_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 by `llmenv-tag:` / `llmenv-bundle:` tokens and
  project. A local JSONL `file` sink mirrors the same stream, independent of
  ICM reachability. (#382)
- The Claude Code adapter now auto-registers `SessionStart`/`SessionEnd` hooks
  running `llmenv hook-run`, fixing a gap where the ICM memory wake-up/store
  dispatcher existed but was never wired into generated `settings.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`,
  and `regenerate` now iterate a registry of engine adapters, materializing each
  into its own per-engine cache subtree and skipping any whose binary isn't on
  `PATH`. 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 like
  `mcp`. 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`, and
  `timeout` on MCP server entries. All optional — existing configs parse
  unchanged. (#505)
- `CrushAdapter`: Crush is now a supported engine. `export`/`hook`/`regenerate`
  render `crush.json` when `crush` is on `PATH`. What maps: permissions →
  `allowed_tools`/`denied_tools` (lossy, fail-closed — `ask` rules collapse to
  `denied_tools`, never silently allowed; Crush has no ask concept); hooks →
  `PreToolUse` only (`mcp_tool`-kind hooks and unsupported hook events hard-error
  with an actionable message); MCP servers (including `headers`, `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.crush` merge 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`, and `regenerate`
  now iterate all registered engine adapters. If `crush` is on `PATH`, a new
  `crush/` cache subtree is materialized and `CRUSH_GLOBAL_CONFIG` /
  `CRUSH_GLOBAL_DATA` are exported alongside the existing Claude Code env vars.
  Claude-only users (no `crush` binary on PATH) see no change. (#502, #506)
- **BREAKING:** `session_log` is now a mapping (`{ file, transcript, verbose,
  path, max_content_bytes }`), not a path string. ICM transcript logging is on
  by default. The pre-3.0 `session_log: "<path>"` form is rejected with a
  migration hint. (#382)

### Removed

- `LLMENV_BASH_BAN` env 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)

## [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`, and `TEMP` env 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_DIR` now points to
  `<state_dir>/plugins/` so plugins are not re-downloaded on every scope change (#632)

### Fixed

- Fix hook context emission including `additionalContext` content 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_DIR` and other state-directory env vars (from
  `materialize::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 existing `normalize_rel` helper. (#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 like `features.memory`; currently supports the `umans` backend (#487)

## [2.2.1] - 2026-06-24

### Fixed

- Fix `llmenv export` aborting with "variable value contains forbidden control character" for
  `LLMENV_ICM_CONTEXT` and 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-efficiency` example 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 minimal `native_permissions` limiting 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 `--compress` flag to `llmenv export`: strips trailing whitespace and collapses excessive blank
  lines for token-efficient AGENTS.md output (#226)
- Wire `LLMENV_BASH_BAN` env 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-efficiency` example bundle declaring `BASH_BAN` instead of `LLMENV_BASH_BAN`; the
  Bash deny feature silently failed for any user of the example config (#466)
- Fix `token-efficiency` example bundle placing env vars under `features.env` instead of the
  top-level `env` key 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_BAN` accepting patterns containing `)`, `(`, and newlines that produced
  malformed deny rules; invalid pattern characters are now rejected at startup (#465)
- Fix `LLMENV_BASH_BAN` treating 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 --compress` not preserving the final newline, producing non-POSIX output
  (#465)

<!-- 2.1 next-header -->

## [2.1.0] - 2026-06-23

### Added

- Add `session_log` config 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
  covering `config.yaml`, five bundles, hooks, skills, rules, and scripts
- Detect volta, fnm, Linux pnpm (`~/.local/share/pnpm/`), and macOS pnpm
  (`~/Library/pnpm/`) install paths when seeding `installMethod` in Claude Code
  settings; previously these were classified as `native`

### Fixed

- Fix `GIT_SSH_COMMAND` being overwritten by llmenv's SSH timeout injection;
  user SSH identity files, `ProxyJump`, and other existing SSH customizations
  are now preserved
- Fix `seed_install_method` overwriting a user-customized `installMethod` value
  in Claude Code `settings.json`; the field is now only written when absent
- Fix `seed_install_method` silently swallowing I/O errors (e.g. permission
  denied) when reading `settings.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.json` entries (missing or invalid `source` field)
  being silently dropped; these now emit a `warn` log 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; only `https://` and
  SSH remotes are permitted (#360)
- Remove `StrictHostKeyChecking=accept-new` from 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 next-header -->

## [2.0.5] - 2026-06-18

### Added

- Fold six `*-ls` listing commands into `status` subcommands: `status bundles`,
  `status tags`, `status scopes`, `status mcps`, `status marketplaces`,
  `status plugins`. The top-level `*-ls` forms 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 --why` to show activation tracing — which scope triggered each
  active tag and which tags fired each bundle
- Add `export --explain` to annotate each exported variable with its source
  (adapter or llmenv introspection)
- Add `sync --dry-run` to preview pending config changes without committing
- Add `check-stale --auto-fix` to automatically re-materialize config on drift
  rather than only printing a warning
- Add `validate` command to check config for structural issues (duplicate bundle
  names, bundles with no activation tags)
- Add `edit [bundle-name]` command to open `config.yaml` or 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`, and `config-guard` commands
  in `commands.md`; add `regenerate` and `login` to the `getting-started.md`
  quick-reference table
- Expand `doctor` entry in `commands.md` to list the token-efficiency settings
  it checks

### Fixed

- Fix `edit` command allowing paths outside the config root via `..` traversal;
  the target path is now canonicalized and validated before opening
- Fix `edit` command ignoring arguments in `$EDITOR` (e.g. `code --wait`);
  the editor value is now split on whitespace before invoking
- Fix `validate` not checking `enable_bundles` references in project-scoped
  config; unknown bundle names now report an error regardless of scope type
- Fix `plugin-sync` silently 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 `status` listing commands and `doctor --all` incorrectly classifying MCPs,
  bundles, and plugins as orphaned when their `when:` 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 `hookEventName` being emitted at the top level of hook JSON instead of
  inside `hookSpecificOutput`; 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-sync` silently dropping all externally-sourced plugins
  (e.g. `slack`, `superpowers`) whose `marketplace.json` entry 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 to
  `llmenv plugin-sync` instead of `llmenv 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 required `hookEventName` field,
  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 --workspace` not bumping sub-crates: add explicit
  `shared-version = true` to each sub-crate `release.toml` so 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 when
  `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE`, `BASH_MAX_OUTPUT_LENGTH`, `MAX_MCP_OUTPUT_TOKENS`,
  or `ENABLE_PROMPT_CACHING_1H` are not set (or misconfigured); informs when
  `CLAUDE_CODE_SUBAGENT_MODEL` is unset; warns when no `context-mode` MCP server is
  configured
- Add `config::template::generate_template()` function; `llmenv init` now 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-context` subcommand, auto-registered as a `SessionStart` hook
  by the Claude Code adapter; emits source config file and bundles directory paths
  as `hookSpecificOutput.additionalContext` so the agent always knows where to edit
  llmenv config rather than touching managed cache files
- Add `llmenv config-guard` subcommand, auto-registered as a `PreToolUse` hook
  (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: `oauthAccount` credentials are now stored in
  `state/auth/<uuid>.json` outside 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 via `claude auth login`,
  saves them to the stable auth cache, and optionally persists them globally (#172)
- Add `init.seeded_settings` to `config.yaml`: user-selected keys from
  `~/.claude/settings.json` are seeded into `settings.json` on first materialization of
  a new config folder, carrying over preferences without overwriting managed settings;
  `llmenv init` now prompts to log in, import from `~/.claude`, or skip (#172)
- Add per-bundle `features.memory` overrides: bundles can declare a `features:` block in
  `bundle.yaml` to use a different memory daemon `server_host` per 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 alias `vars`) from the top-level
  `bundle:` config field. Bundle-level environment variables must now be declared
  in `bundle.yaml` under `capabilities.env`. (#352)

### Fixed

- Fix `config-guard` path-prefix check accepting `..`-based traversal paths (e.g.
  `~/.cache/llmenv/../../../etc/shadow` matched as inside the cache); paths are now
  normalized lexically before the prefix check
- Fix `config-guard` silently swallowing JSON parse failures when the hook payload was
  malformed; non-empty non-JSON stdin now logs a warning to stderr
- Fix `config-guard` not logging when `CLAUDE_CONFIG_DIR` is set but has no recognizable
  `claude-code` ancestor directory; the fallback is now visible to operators
- Fix `config-context` silently 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; `llmenv` now logs a warning
  when a configured bundle name has no corresponding directory, making typos and
  deleted directories detectable
- Fix `mcp[].env` keys not being validated for the `LLMENV_` prefix or reserved state
  vars (`CLAUDE_CONFIG_DIR`, `LLMENV_STATE_DIR`); these were accepted silently where
  `capabilities.env` already rejected them, creating an inconsistent validation gap
- Fix git fetch spawn errors logged at `debug` level in the background sync path;
  a spawn error (git binary missing or misconfigured) is unexpected and is now logged
  at `warn` so operators can see it
- Fix git reset errors during explicit plugin sync silently logged at `debug` level;
  errors are now logged at `warn` so 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, `llmenv` now logs a `warn` with the skew magnitude
  (`skew_secs`) and proceeds with the pull rather than silently skipping it (#377)
- Fix missing `plugin.json` after a plugin sync being silently ignored; `llmenv` now
  logs a `warn` when the plugin manifest is absent after materializing the plugin,
  making broken plugin installs diagnosable (#379)

<!-- 1.0 next-header -->

## [1.0.10] - 2026-06-11

### Added

- `llmenv plugin-sync` now fetches externally-sourced plugins — those whose
  `source` in `marketplace.json` is 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 install` or re-authentication (#353)

### Fixed

- Fix `env:` declared in a bundle's `bundle.yaml` being silently dropped;
  bundle-level env vars are now merged and exported alongside `Bundle.vars`
  (#351)
- Reject reserved env var names (`CLAUDE_CONFIG_DIR`, `LLMENV_STATE_DIR`) and
  the `LLMENV_*` prefix in `capabilities.env` at 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.env` key merging and error
  with the contributor names and values, matching the existing `default_mode`
  conflict behaviour; previously one of the conflicting values would silently
  win (#355)

## [1.0.9] - 2026-06-10

### Fixed

- Fix `memory.listen_host` unspecified-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_host` option under `features.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_DIR` already-active checks (#338)
- Fix empty directories left in rendered output when a bundle contributes no
  files to a subdirectory; `create_dir_all` is 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 in `bundle.yaml`; declare MCP servers inside a bundle using
  the same format as `config.yaml`; tagless entries are active whenever the bundle
  is selected, tagged entries are further filtered by active scope tags (#329)
- `llmenv init` now generates a `README.md` orientation file in the config
  directory on first run; the write is skipped if a `README.md` already 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.mcp` and bundle `mcp:` entries;
  a name declared in both sources now errors at startup instead of silently
  producing duplicate servers (#329)
- Fix `mcp-ls` omitting 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 name `icm`; the guard now
  matches the one already present for top-level `config.mcp` (#329)
- Fix `llmenv init` emitting a config.yaml template with a nested `transport:`
  block for MCP servers; the correct flat schema (`type`/`command`/`args` at the
  top level) is now emitted (#325)
- Fix `llmenv init` silently 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_level` and `advisor_size` as first-class capability fields; rendered
  into `settings.json` as `effortLevel` and `advisorSize` for engine adapters to
  consume (`advisor_size` uses generic sizes `"small"`, `"medium"`, `"large"` so
  adapters map to engine-specific models via `native` overrides)
- Add `env` field to `NetworkScope`, `HostScope`, and `UserScope`; 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 into `main`; 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.vars` to `bundle.env`; the old key `vars` is still accepted as
  a backward-compatible alias so existing configs continue to work

### Fixed

- Fix `mcp-proxy` spawned during `llmenv export` inheriting the calling shell's
  stdio; when the export was sourced over SSH via `source <(llmenv export)` the
  proxy wrote its logs into the process-substitution pipe, flooding the terminal
  with `command not found: INFO:` lines. The proxy now redirects stdio to
  `/dev/null` and starts in its own process group so terminal job-control
  signals no longer reach it
- Fix `llmenv sync` silently reporting success when `git push` failed; 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 `~/.claude` paths; such paths resolve against the default config
  dir, not the materialized folder llmenv actually boots. Materialization now
  rejects skills (and rules/CLAUDE.md) carrying `~/.claude` or `$HOME/.claude`
  paths, naming the offending file
- Fix marketplace `git clone`/`fetch` failures 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 `llmenv` config auto-pull silently swallowing a failed fast-forward
  (diverged history, conflict, network); a one-line nudge now points at
  `llmenv sync` instead 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.txt` attachment to verify

### Fixed

- Fix documentation referencing `mcp.json` for MCP server configuration;
  servers have been written to `mcpServers` in `.claude.json` since v1.0.0
- Fix `state:` key and `features.memory:` subsection missing from
  configuration reference
- Fix `hook-run` command 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_settings` silently dropping native passthrough keys (e.g.
  `statusLine`, `cleanupPeriodDays`) on re-renders when `settings.json` already
  exists; non-owned keys from `fresh` are now written through on every render

## [1.0.2] - 2026-06-02

### Fixed

- Fix marketplace sync failure silently dropping `CLAUDE_CONFIG_DIR` on export;
  missing local clone now warns and continues rather than propagating an error
  that exited 0 without emitting the env var (#281)
- Fix `run_export` allowing `build_and_materialize` failures to exit 0 without
  emitting `CLAUDE_CONFIG_DIR`; build failures now exit non-zero (#281)
- Fix materialize creating empty cache directories when source bundles are
  deleted or moved (#285)
- Fix `doctor` falsely reporting marker-enabled bundles (e.g. `rust-dev`,
  `python-dev`) as orphans when no project marker is currently active (#284)
- Fix `doctor` suppressing 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 doctor` diagnostic command with config, cache, and git health
  checks; `--gc` flag for garbage collection; `cache_retention_hours` setting
  (default 168 hours)
- Add `llmenv prune` command with `--all`, `--older-than <duration>`, and
  `--dry-run` flags; symlink-safe deletion, orphaned `*.tmp` staging dirs always
  cleared (#63)
- Add `llmenv sync` command for on-demand configuration synchronization with
  configurable sync interval
- Add `hook-run` command 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 registers
  `check-stale` `SessionStart` hook for drift detection (#121, #122, #123, #124)
- Add per-feature `native` override maps (`native_permissions`, `native_hooks`,
  `native_plugins`, `native_mcp`) for engine-specific config passthrough; catch-all
  `native.<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 `extraKnownMarketplaces` and `enabledPlugins` into
  `settings.json`; new `marketplace-ls`, `plugin-ls`, and `plugin sync` CLI
  commands (#59)
- Add engine-neutral permission rule rendering into Claude Code `settings.json`
  with native suppression (deny is authoritative over allow/ask) (#34)
- Add cross-project tag-scoped memory recall via `turn_start` hook; tags
  validated before expansion to prevent metacharacter injection (#197)
- Add `--color <auto|always|never>` flag with `NO_COLOR` and `CLICOLOR_FORCE`
  support; colored markers in `tag-ls`, `scope-ls`, `bundle-ls`, `doctor`, and
  `status` (#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 deny` gates 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_fidelity` config with single `cache.hashing: loose|normal|strict`
  (default `normal`); `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 `mcpServers` object in `.claude.json`
  instead of standalone `mcp.json`; foreign keys are preserved on
  read-modify-write merge; remote servers now carry an explicit `"type"` field;
  `enabledMcpjsonServers` is no longer emitted (#244)
- Change config format from TOML to YAML (`~/.config/llmenv/config.yaml`
  replaces `config.toml`); `llmenv init` emits YAML; migrated from deprecated
  `serde_yaml` to `serde_yaml_ng` (#76)
- Change `hook-run` from 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 prune` counting symlinks as removed when unlink failed; failures
  are non-fatal but now logged and reported under a separate `failed` list (#255)
- Fix corrupt `.llmenv-manifest.json` being discarded silently; parse failure
  now emits a `tracing::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_materialize` in addition to
  the final emission loop, preventing injection in the `export NAME=...` shell
  contract (#67)

<!-- next-url -->
[Unreleased]: https://github.com/phaedrus1992/llmenv/compare/v3.0.0...HEAD
[3.0.0]: https://github.com/phaedrus1992/llmenv/compare/v3.0.0-rc.2...v3.0.0
[3.0.0-rc.2]: https://github.com/phaedrus1992/llmenv/compare/v3.0.0-rc.1...v3.0.0-rc.2
[3.0.0-rc.1]: https://github.com/phaedrus1992/llmenv/compare/v2.3.0...v3.0.0-rc.1
[2.4.1]: https://github.com/phaedrus1992/llmenv/compare/v2.4.0...v2.4.1
[2.4.0]: https://github.com/phaedrus1992/llmenv/compare/v2.3.0...v2.4.0
[2.3.0]: https://github.com/phaedrus1992/llmenv/compare/v2.2.1...v2.3.0
[2.2.1]: https://github.com/phaedrus1992/llmenv/compare/v2.2.0...v2.2.1
[2.2.0]: https://github.com/phaedrus1992/llmenv/compare/v2.1.0...v2.2.0
[2.1.0]: https://github.com/phaedrus1992/llmenv/compare/v2.0.5...v2.1.0
[2.0.5]: https://github.com/phaedrus1992/llmenv/compare/v2.0.4...v2.0.5
[2.0.4]: https://github.com/phaedrus1992/llmenv/compare/v2.0.3...v2.0.4
[2.0.3]: https://github.com/phaedrus1992/llmenv/compare/v2.0.2...v2.0.3
[2.0.2]: https://github.com/phaedrus1992/llmenv/compare/v2.0.1...v2.0.2
[2.0.1]: https://github.com/phaedrus1992/llmenv/compare/v2.0.0...v2.0.1
[2.0.0]: https://github.com/phaedrus1992/llmenv/compare/v1.0.10...v2.0.0
[1.0.10]: https://github.com/phaedrus1992/llmenv/compare/v1.0.9...v1.0.10
[1.0.9]: https://github.com/phaedrus1992/llmenv/compare/v1.0.8...v1.0.9
[1.0.8]: https://github.com/phaedrus1992/llmenv/compare/v1.0.7...v1.0.8
[1.0.7]: https://github.com/phaedrus1992/llmenv/compare/v1.0.6...v1.0.7
[1.0.6]: https://github.com/phaedrus1992/llmenv/compare/v1.0.5...v1.0.6
[1.0.5]: https://github.com/phaedrus1992/llmenv/compare/v1.0.4...v1.0.5
[1.0.4]: https://github.com/phaedrus1992/llmenv/compare/v1.0.3...v1.0.4
[1.0.3]: https://github.com/phaedrus1992/llmenv/compare/v1.0.2...v1.0.3
[1.0.2]: https://github.com/phaedrus1992/llmenv/compare/v1.0.1...v1.0.2
[1.0.1]: https://github.com/phaedrus1992/llmenv/compare/v1.0.0...v1.0.1
[1.0.0]: https://github.com/phaedrus1992/llmenv/releases/tag/v1.0.0
