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_DIRif set, otherwise the platform config dir (~/.config/llmenv). Confirm withllmenv status, which reports the file it loaded. - Parse error.
llmenv doctorreports 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_maconly today (ssid/cidrare 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
hostnameand compare. - User scopes match
$USERexactly.
A project marker isn't detected​
- The
.llmenv.yamlwalk ascends from the current directory to$HOMEinclusive, then stops. A marker above$HOMEis intentionally ignored. - When
$HOMEis unset, only the current directory itself is checked. - Confirm detection with
llmenv context—LLMENV_ACTIVE_PROJECTandLLMENV_PROJECT_ROOTare 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 doctorflags 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_bundleslist.
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.tagsis active. Checkllmenv tag-ls. - Client can't reach the server — confirm the
host:address resolves and the port is open:nc -vz <addr> <port>. mcp-proxymissing — the server host needsmcp-proxyoruvxonPATH.llmenv exporterrors with an install hint if neither is present.mcp-proxywon'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 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 --allflags 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'sHashingMode::Strictfolder 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 byhook-runto skip a full merge.read_once— the per-session read-once dedup cache (hook-run'sPreToolUsehandler), tagged with the filepaththat was looked up.plugin_marketplace— whether a plugin marketplace's git clone was already present, tagged with the marketplacename. A pinned marketplace's forced refresh (re-clone to converge on the pin) always reportsmiss, 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.