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 status scopes # marks active scopes
llmenv status tags # marks active tags
  • Network scopes match on gateway_mac, cidr, or ssid. A VPN or captive network can change or hide the gateway, leaving the scope unmatched. An ssid scope never matches when the platform hides the SSID (macOS 15 prints <redacted>), and llmenv doctor says so. 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 status tags.
  • 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

Claude Code says CLAUDE.md is large​

(added in v3.12.0)

Claude Code warns at startup when the instruction files it loads are large, and the warning names files under ~/.cache/llmenv/. Do not edit those files, because llmenv regenerate overwrites them. Run llmenv doctor and read the Instruction size (Claude Code): section. It prints the total and the largest contributors with their bundle names. Trim the named bundle's CLAUDE.md text or rule, or add a paths: list to a rule that only matters for some files. Then run llmenv regenerate.

Doctor warns about retired Claude Code settings​

(added in v3.12.0)

Claude Code retires settings keys, environment variables, and tools over time. A retired entry in your config does nothing, and Claude Code does not tell you. llmenv doctor reads the rendered settings.json and .claude.json in the folder that CLAUDE_CONFIG_DIR points to, and prints a Retired Claude Code settings: section when it finds one. Run doctor in a shell that has the llmenv hook; without CLAUDE_CONFIG_DIR, doctor skips the check and says so. Each line names where the entry is, the Claude Code version that dropped it, and what to use instead:

âš  settings.json env: CLAUDE_CODE_CONNECT_TIMEOUT_MS has no effect since Claude Code 2.1.186.
Use API_TIMEOUT_MS instead.
âš  settings.json: voiceEnabled is deprecated. Use voice.enabled.

A settings.json that an older llmenv rendered can hold advisorSize. llmenv wrote that key before v3.12.0 and Claude Code never read it, so doctor warns and names advisorModel. Run llmenv regenerate to clear it.

The list follows Claude Code's own settings reference, environment-variable reference, and changelog. The rendered files collect entries from native.claude_code, capabilities.env, permission rules, MCP entries, and keys that Claude Code writes itself, so remove the entry from whichever of those holds it. The check only warns: it never changes the exit status, and llmenv validate does not report it. If a rendered file is not valid JSON, doctor names the file and skips it.

Four token-efficiency checks also changed in v3.12.0:

  • CLAUDE_AUTOCOMPACT_PCT_OVERRIDE must be a whole number from 1 to 100. Doctor warns about a value outside that range, and about a value above 70, because PreCompact hooks then have too little room to run.
  • When native.claude_code.autoCompactEnabled is false, doctor reports info and recommends no CLAUDE_AUTOCOMPACT_PCT_OVERRIDE, because automatic compaction is off. When autoCompactWindow is set, at top level or under modelSettings, the message names the window the percentage applies to. The unset warning says the variable only matters in sessions that compact before the model's limit.
  • When native.claude_code.bashOutputMaxChars is set, doctor reports it and skips BASH_MAX_OUTPUT_LENGTH, because Claude Code ignores the variable while the setting is set.
  • CLAUDE_CODE_PROMPT_CACHE_TTL takes precedence over ENABLE_PROMPT_CACHING_1H. The prompt-cache check passes when the TTL is 1h, and warns when it is set to another value. When the TTL is unset, it checks ENABLE_PROMPT_CACHING_1H. When neither is set, it prints an info line instead of a warning: subscription plans get the 1-hour TTL on the main conversation without any variable.

Background work that did not finish​

(added in v3.12.0)

llmenv runs four jobs in a detached child: post-session consolidation, the ICM store of a web fetch, the transcript record of a session event, and the codebase-memory-mcp index. Each job writes a checkpoint file to <state dir>/checkpoints/ first, and deletes it when the job succeeds. A file that stays means the job did not finish, for example because the memory proxy was down.

llmenv doctor prints a Background work: section with one line for each checkpoint: the job, its age, the attempts so far out of 3, its phase, and the log to read (<state dir>/detached-hook.log). A checkpoint that is not yet old enough to be stale shows as info, because its child may still be running.

At each new session start, llmenv runs a stale job again, up to 3 attempts, and at most 20 jobs. A job runs in the directory of its first run. If that directory is gone, llmenv does not run it. After the third attempt the file stays, so doctor keeps showing it. To abandon a job, delete its file under checkpoints/. llmenv deletes checkpoints that are older than 7 days.

settings.json.corrupt appears in the config folder​

(added in v3.12.0)

llmenv found a settings.json that is not valid JSON, for example after a half-written save. It moved the file to settings.json.corrupt and wrote a new one, and it printed a warning with the parse error. Compare the two files, copy any key that you need back into your llmenv config, and delete the .corrupt file. If llmenv cannot move the file, the render stops and names the file. Fix or remove it, and run llmenv regenerate.

A bundle hook warns that a path is not in the bundle files​

(changed in v3.12.0)

A bundle hook command can name a script. llmenv copies a script from the bundle to the cache folder and points the command at the copy. The warning says the command names a script path that is not in the bundle files, so llmenv did not copy it. It names the bundle and the path, and it prints once for each bundle and path.

  1. Put the script in the bundle, in a folder such as hooks/.
  2. Use a path inside the bundle, such as bash hooks/guard.sh.

The warning does not fire for an inline shell command. A / that is part of shell syntax, such as the jq // operator, 2>/dev/null, or a URL, is not a script path. A path in a system folder, such as /usr/bin/env, is not a script path either.

A task command refuses​

(added in v3.12.0)

The task tracker refuses some commands, to keep the task list true. Each refusal names the fix.

  • task start says a task is queued behind another task. Top-level tasks run one at a time, in the order you added them. Finish the task ahead with llmenv task done <slug>, or park it with llmenv task wait <slug> "<reason>". Pass --force, or add the task with --parallel, when it can run beside the other task.
  • task done says a task was never started. Run llmenv task start <slug> first, or pass --force when the work is done without tracking.
  • task done on a parent lists sub-tasks that are not done. Finish them, or pass --force.
  • task session finish lists the tasks that are open, wip, or waiting. Finish them, clear one with llmenv task clear <slug>, or pass --abandon-open.
  • git commit or gh pr create is denied once with no task in progress. Run the commands in the message, then run the same command again. Set features.task_tracker.enforce_commit: false to turn the deny off.
  • An agent follows an instruction that says the task tools are blocked. Run llmenv doctor and read the Task tracker instructions: section. It names the bundle file that holds the text. Reword it in the source bundle, then run llmenv regenerate.

See task for the rules.

Memory commands list too much​

(added in v3.12.0)

llmenv memory list and llmenv memory diff ask ICM for the memories of the project that the current folder belongs to. When llmenv cannot tell the project, it warns cannot tell the project of this folder, so memories of all projects are used. Run the command from inside the project folder. llmenv memory prune reads only the project of the current folder, and it refuses to run when it cannot tell the project. It also refuses to run while the active features.memory entry sets retention. See memory.

Memory backend issues​

  • Server not activating — it renders only when one of memory.tags is active. Check llmenv status tags.
  • Client can't reach the server — confirm the host: address resolves and the port is open: nc -vz <addr> <port>.
  • A managed MCP server is down or stuck (added in v3.12.0) — the session start output starts with llmenv: MCP health check failed at session start. and names each server that did not answer an MCP initialize within 5 seconds, with the reason and the fix. llmenv doctor prints the same result under MCP servers:. A stopped ICM proxy on the server host restarts by itself. For a proxy that holds its port but does not answer, run llmenv doctor --restart-memory-proxy (added in v3.12.0). It sends SIGTERM to the one proxy that the pidfile names, only when that process runs mcp-proxy ... -- icm serve, then starts the proxy again. Do not use pkill: it stops the proxy that other sessions use. Each proxy start writes one line to mcp-proxy.log next to the pidfile: llmenv: started mcp-proxy pid=... source=export|session-start|restart with the pid, session, and process group of the spawner. After an unexplained shutdown, compare the time of Shutting down with the last such line. For a stuck codebase-memory-mcp daemon, run pkill -f cbm-daemon-internal, then /mcp to reconnect. See Session-start health check.
  • Old ICM server (added in v3.12.0) — on the host that serves memory, llmenv doctor reports the icm version under ICM server:. It warns below 0.10.60, where recall filters by keyword and topic after the limit, so adaptive recall returns little or nothing. It warns again below 0.10.64, where recall ranking is weaker. Run icm upgrade --apply on that host. On a memory client, doctor cannot read the server version, because mcp-proxy does not forward it; run icm --version on the server host.
  • 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.