Skip to main content

Troubleshooting

Start with the two diagnostic commands, then drill into the specific failure below.

llmenv doctor # validate config + wiring; --gc to also clean the cache
llmenv context # show resolved scopes/tags/bundles for the current dir

My config isn't being picked up​

  • Wrong config path. llmenv reads $LLMENV_CONFIG_DIR if set, otherwise the platform config dir (~/.config/llmenv). Confirm with llmenv status, which reports the file it loaded.
  • Parse error. llmenv doctor reports YAML parse failures and missing required fields. A common cause is an unquoted value with a colon — see Configuration → YAML gotchas.

A scope never activates​

llmenv scope-ls # marks active scopes
llmenv tag-ls # marks active tags
  • Network scopes match on gateway_mac only today (ssid/cidr are parsed but ignored). A VPN or captive network can change or hide the gateway, leaving the scope unmatched. Fall back to a host scope (matches by hostname, always reliable) that emits the same tag.
  • Host scopes match case-insensitively against the local hostname. Run hostname and compare.
  • User scopes match $USER exactly.

A project marker isn't detected​

  • The .llmenv.yaml walk ascends from the current directory to $HOME inclusive, then stops. A marker above $HOME is intentionally ignored.
  • When $HOME is unset, only the current directory itself is checked.
  • Confirm detection with llmenv context — LLMENV_ACTIVE_PROJECT and LLMENV_PROJECT_ROOT are set only when a marker matched.
  • Malformed marker YAML degrades to defaults (id/name from the folder basename) and logs a warning; it does not fail the whole resolution.

A bundle / MCP server / plugin won't fire​

These all select by tag intersection. If a contributor's tags aren't in the active set, it stays dormant.

  • Check the active tags with llmenv tag-ls.
  • llmenv doctor flags orphans: a contributor whose tags no scope emits, and a scope whose tags no contributor consumes.
  • To force a bundle on inside a project regardless of tags, add it to the marker's enable_bundles list.

The agent is running stale config​

After you change config, a running agent keeps the directory it booted with. The SessionStart hook runs llmenv hook-run session_start, which compares the booted content hash against the current one and prints a restart hint on drift (it ran as a separate llmenv check-stale hook before v3.11.0). You can run the check on its own:

llmenv check-stale

Restart the agent to pick up the new config.

The cache is growing / I want a clean slate​

llmenv prune --dry-run # preview
llmenv prune # remove old-version folders + orphaned *.tmp
llmenv prune --older-than 14d # remove current-version folders older than 14d
llmenv prune --all # nuke everything (re-materializes on next export)
llmenv doctor --gc # diagnostics + GC in one pass

Memory backend issues​

  • Server not activating — it renders only when one of memory.tags is active. Check llmenv tag-ls.
  • Client can't reach the server — confirm the host: address resolves and the port is open: nc -vz <addr> <port>.
  • mcp-proxy missing — the server host needs mcp-proxy or uvx on PATH. llmenv export errors with an install hint if neither is present.
  • mcp-proxy won't start (added in v3.8.0) — the warning quotes the tail of the proxy's stderr log; read the whole thing at ${XDG_STATE_HOME:-$HOME/.local/state}/llmenv/mcp-proxy.log.
  • no memory backend active for this scope (added in v3.8.0) — the message names which of the four causes applies: no bundles fired, nothing declares features.memory, a firing bundle has no content directory (so its bundle.yaml was never read), or the only bundle supplying memory is turned off via disable_bundles. llmenv doctor --all flags that last case too.
  • Web-fetch stores, consolidation, or transcript records go missing (added in v3.8.0) — those run in detached children with no terminal. Their stderr is captured to ${XDG_STATE_HOME:-$HOME/.local/state}/llmenv/detached-hook.log (owner-only, rotated at 512 KiB), and their failures log at error level so the default log filter doesn't drop them.

See MCP & Memory for the full topology and security model.

Profiling hook-run latency​

Lifecycle hooks run on the agent's hot path, so a slow hook-run shows up as prompt lag. To see where the time goes, set LLMENV_TRACE_TIMING (to any value) before the hook fires. Every hook-run invocation then emits one line to stderr (stdout is untouched), whether it completes the full memory/session-log stage or takes one of the hook's several early-return paths:

llmenv-trace {"config_load_us":123,"scope_eval_us":456,"prep_us":78,"mcp_us":9012}

Each value is an integer microsecond count for that phase:

  • config_load_us — reading and parsing config.
  • scope_eval_us — evaluating scopes against the environment.
  • prep_us — everything between scope evaluation and the MCP round-trip: building recall queries, generating the context chunk, constructing the MCP client (reqwest/TLS on a connection cache miss), building the scope context, and the one-time ~3 ms tokio runtime build on the session's first hook-run.
  • mcp_us — the MCP round-trip plus session logging; usually the dominant term.

(added in v3.8.0) Each field is present only if that phase actually ran, so an event that early-returns before scope evaluation (e.g. a PreToolUse with no active sinks) reports config_load_us alone, one that early-returns after scope evaluation adds scope_eval_us, and so on — earlier versions only emitted this marker for events reaching the full memory-dispatch stage (4 of 11 hook events), so every other event's config_load/scope_eval cost was invisible. Runs that error before the marker point still emit nothing. The var is off by default and adds no measurable overhead when unset.

Profiling cache hit/miss rates​

(added in v3.10.0) The same LLMENV_TRACE_TIMING var also enables cache telemetry: one stderr line per cache lookup, across every persistent cache layer llmenv has —

[LLMENV_CACHE] content_hash hit 0.012ms
[LLMENV_CACHE] merge_sig miss 0.412ms
[LLMENV_CACHE] read_once hit 0.001ms path=CLAUDE.md
[LLMENV_CACHE] plugin_marketplace miss 842.100ms name=my-marketplace
  • content_hash — the materialize cache's HashingMode::Strict folder reuse (materialize). Loose/Normal mode always writes in place and has no hit/miss concept, so it emits nothing.
  • merge_sig — the disk-persisted bundle-merge memory/host slice (materialize::merge_cache), read by hook-run to skip a full merge.
  • read_once — the per-session read-once dedup cache (hook-run's PreToolUse handler), tagged with the file path that was looked up.
  • plugin_marketplace — whether a plugin marketplace's git clone was already present, tagged with the marketplace name. A pinned marketplace's forced refresh (re-clone to converge on the pin) always reports miss, even when a clone already existed, since it's a deliberate cache invalidation rather than a reuse.

Profiling per-MCP-call timing​

(added in v3.10.0) The same LLMENV_TRACE_TIMING var also enables per-call MCP timing: one stderr line per tool call, covering every MCP call the process makes (icm_wake_up, icm_memory_recall, icm_memory_store, icm_transcript_show, and so on), not just the aggregate mcp_us bucket in the per-phase marker above —

[LLMENV_MCP_CALL] icm_wake_up 66544us
[LLMENV_MCP_CALL] icm_memory_recall 2340us

The duration includes a stale-session retry's full cost when one happens (the retry itself already logs a warning) — this is deliberately "wall time this call actually took", not "time excluding recovery". No result or entry count is reported: the client has no notion of what a "result" means for an arbitrary tool.

Off by default, same as the markers above — enabling LLMENV_TRACE_TIMING enables all four (per-phase, cache, per-MCP-call, context/recall).

Profiling memory-recall and injected-context size​

(added in v3.10.0) The same LLMENV_TRACE_TIMING var also enables recall/injection telemetry for TurnStart's memory-recall dispatch (project-scoped recall, plus one tag- and bundle-scoped recall per active tag/bundle):

[LLMENV_CONTEXT] recall_entries=4 recall_bytes=3896 injected_entries=4 injected_bytes=3896 advisory_stripped=2
  • recall_entries/recall_bytes — how many of the dispatched recall actions came back non-empty, and their total byte size, before dedup.
  • injected_entries/injected_bytes — how many of those actually ended up in the injected context, and their total byte size, after dedup.
  • advisory_stripped — recall actions that came back non-empty but weren't injected: either the whole response was advisory-only noise (e.g. "No memories found."), or it exactly duplicated an already-kept action's text (the same memory recalled under more than one active tag/bundle).

Granularity is per recall action (one per project/tag/bundle scope), not per individual memory record inside a response — parsing ICM's recall-response text format to count individual records would couple this telemetry to a format owned by a separate system. Only emitted when the dispatched actions included at least one recall (SessionStart's icm_wake_up and SessionEnd's icm_memory_store never emit this line).

Sync conflicts​

llmenv sync runs git add/commit/push on the config repo. If the remote has diverged, resolve it like any git conflict (pull/rebase, fix, re-run). llmenv does not force-push.