MCP Servers and the Memory Backend
llmenv treats MCP (Model Context Protocol) servers as a first-class config
concept. Servers are declared once under mcp:, attached to scopes via tags
(the same selection model as bundles), and rendered by each adapter into its
agent-native config (for Claude Code: upserted into mcpServers in .claude.json).
llmenv's own memory backend is configured separately under memory:. It is a
single networked service, not a generic MCP entry β its implementation (ICM,
Infinite Context Memory) is deliberately hidden behind the memory: vocabulary.
For the config-field reference, see
Configuration β mcp: and
memory:. This page covers the runtime model: the
selection mechanism, the memory topology, the security model, and the
tag-scoped-memory env var contract.
Selection modelβ
Every mcp entry carries tags. A server is included in the materialized
output when any of its tags is present in the active tag set for the current
environment β identical to how bundle entries fire. Scopes (network/host/
user/project) emit the tags; the intersection decides what is active.
mcp:
- name: playwright
when: [base] # active whenever the `base` tag is
command: npx
args: ["-y", "@playwright/mcp@latest"]
Server kindsβ
A static server is either stdio (a local launch command) or remote (an HTTP/SSE URL):
mcp:
- name: playwright
when: [base]
type: stdio # default
command: npx
args: ["-y", "@playwright/mcp@latest"]
env:
DISPLAY: ":0"
- name: weather
when: [base]
type: http # http | sse
url: "https://weather.example.com/mcp"
Memory backend (memory:)β
(added in v1.0.0)
The memory backend is a single service that one host runs locally while every
host β including the one running it β reaches over the network. The daemon
(icm serve) is stdio-only, so on the server host llmenv wraps it in
mcp-proxy to expose it on a TCP port; agents everywhere connect to that port.
- On the designated server host, llmenv launches a local
mcp-proxybound to<listen_host>:<port>that bridges the stdio daemon onto the network.listen_hostdefaults to127.0.0.1(loopback), so setlisten_host: 0.0.0.0(or one interface address) for agents on other hosts to connect. - Every agent, on every host, is configured with a remote client
pointed at the server host's address:
http://<addr>:<port>.
The server host needs mcp-proxy
available β it's the stdioβnetwork bridge that exposes the icm serve daemon on
a TCP port. llmenv resolves it one of two ways:
- if
mcp-proxyis onPATH, it's run directly (e.g.uv tool install mcp-proxy,pipx install mcp-proxy, or any install that lands it onPATH); - otherwise llmenv runs it on demand via
uvx(uvx mcp-proxy), which fetches and caches it without a persistent install.
So the server host needs either mcp-proxy or uvx installed. If
neither is present, llmenv export prints a warning that tells you to install
one, and the proxy does not start. Client hosts need neither β they only open an
HTTP connection to the server.
(changed in v3.12.0) llmenv starts mcp-proxy and icm serve in the
filesystem root, /. ICM takes two defaults from its working directory:
- Since ICM 0.10.64,
icm servewith noICM_DBand no global[store].pathopens<git root>/.icm/memories.dbof its working directory./has no git root, so the served store is the one your own ICM configuration names, whereverllmenv exportfirst started the proxy. To pick the database, setICM_DBor[store].pathin ICM's config. - An
icm_memory_recalloricm_wake_upcall with noprojectargument is filtered to the project that ICM names after its working directory./gives no name, so such a call searches all projects, the same asproject: "". llmenv's own hooks always sendproject; this default applies to an agent that calls the ICM tools directly.
If /.git exists, or ICM_DB is a relative path, llmenv refuses to start the
proxy, because ICM would treat / as a repository or open the database in /.
llmenv also removes GIT_DIR, GIT_WORK_TREE, and the other git location
variables from the environment of icm serve, so an inherited value cannot point
ICM at a repository. This applies only to the icm serve that llmenv starts; an
ICM server that you run yourself keeps the defaults of its own working
directory.
The server host's address comes from the top-level host: table:
host:
fixed:
addr: "fixed.local" # IP or resolvable hostname
features:
memory:
- server_host: fixed # key into the `host:` table
port: 7878
listen_host: 0.0.0.0 # default 127.0.0.1; needed for other hosts to connect
when: [base] # activates the backend (same model as bundles)
default_topics: ["context-{project}", preferences]
(changed in v3.12.1) A server host that resolves to several addresses, such as a .local mDNS name that also advertises fe80:: link-local addresses, is accepted.
llmenv drops the link-local addresses, connects only to the others, and fails only when no other address is left.
Before v3.12.1 any link-local address made the SessionStart health check report memory as dead.
Tool search and the ICM tools (added in v3.12.0)β
Claude Code defers the tools of an MCP server behind tool search:
before the model can call a deferred tool, it must call ToolSearch.
The instructions tell the model to use the icm_* tools on most prompts,
so llmenv renders alwaysLoad: true for the ICM server by default, and its tools arrive with the prompt.
Set always_load: false on the features.memory entry to defer them again.
A server in mcp: takes the same field; see mcp:.
Only Claude Code has alwaysLoad. opencode and Crush ignore the field.
How the topology is resolvedβ
- Scopes are evaluated against the current environment; the active host-scope ids and the active tag set are computed.
- If any of
memory.whenis active, the backend is selected: every agent gets a remote client athttp://<addr>:<port>built from the host-table address. - If this host matches
server_host(its id is among the matched host scopes), the CLI also launches the localmcp-proxybound to<listen_host>:<port>. (changed in v3.12.0) Thememory:entry can come fromconfig.yamlor from a firing bundle'sbundle.yaml;llmenv exportreads the same merged list that the hooks use.
Proxy lifecycle on the server hostβ
(added in v3.8.0)
Every llmenv export on the server host checks that the proxy is up, so the
check runs on each shell prompt and has to be both cheap and correct about what
"up" means.
The port decides whether the proxy is running. llmenv opens a TCP connection
to the bind address; if something answers, the proxy is up and nothing is
started. The pidfile at $XDG_STATE_HOME/llmenv/mcp-proxy.pid records which
process to signal β it is never treated as evidence that the proxy is alive. A
missing or stale pidfile alongside a live proxy therefore doesn't cause a second
one to be launched, and a pidfile naming a process that is no longer running is
cleared rather than left to mislead.
A freshly spawned proxy is polled, not slept on. llmenv waits up to 5 s for
the new process to open its socket, checking every 50 ms, so a fast start returns
immediately and a slow one still succeeds. The budget is sized for the uvx
path, which pays uv's resolve cost on top of interpreter startup (~2 s, more on a
cold cache) β the direct PATH install binds in well under a second. If the
proxy exits before binding, that's reported at once instead of waiting the budget
out.
A spawn is guarded by a lockfile next to the pidfile, so several shells
redrawing their prompts at once start one proxy rather than one each. The
lockfile records the pid that holds it; if that process is gone β killed with
^C mid-start, say β the next export reclaims the lock instead of failing
against it.
listen_host may be IPv6. llmenv brackets it when building the bind address
(::1 and port 9092 become [::1]:9092), which is what both mcp-proxy and the
liveness probe expect. Write the plain address in config; don't bracket it
yourself.
The proxy's stderr is kept, at $XDG_STATE_HOME/llmenv/mcp-proxy.log
(owner-readable only, and llmenv refuses to write through a symlink or FIFO left
at that path). It rotates to mcp-proxy.log.1 once it passes 1 MiB, keeping one
generation of history. When the proxy fails to start, llmenv quotes the last
lines of that log in the warning, which is usually enough to see the cause
directly:
warning: failed to ensure mcp-proxy running: mcp-proxy (pid 32097) exited
(exit status: 1) before binding to 0.0.0.0:9092; last lines of
/Users/you/.local/state/llmenv/mcp-proxy.log:
Traceback (most recent call last):
ImportError: cannot import name 'request_ctx' from 'mcp.server.lowlevel.server'
A failure here is a warning, not an error: llmenv export still emits its
environment variables so the shell hook keeps working without the memory backend.
Each start is attributed (added in v3.12.0).
Every time llmenv starts the proxy, it appends one line to mcp-proxy.log.
The line names the new pid, the time, the pid, session, and process group of the process that started it, and whether that process had a terminal.
The source= field says which llmenv path started the proxy: export (a shell prompt), session-start (the session-start health check), or restart (the command below).
Use the line to find what started a proxy that later stopped without a reason.
Restart the proxy with llmenv, not with pkill (added in v3.12.0).
llmenv doctor --restart-memory-proxy sends SIGTERM to the one process that the pidfile names, waits up to 5 seconds for it to exit, and starts a new proxy.
It first checks that the command line of that process is an mcp-proxy for icm serve, so a reused pid is never signaled.
pkill -f mcp-proxy stops the proxy of every session on the machine, so do not use it.
The command fails with the fix when the old proxy does not exit, when the pid is another program, or when a process that llmenv does not track holds the port.
Placing a host on a network manuallyβ
Network auto-detection (by gateway MAC, CIDR, or SSID) doesn't always work β a VPN, a captive network, or an unrecognized gateway can all leave the network scope unmatched, so the memory tag never activates and clients can't find the server.
Because the memory backend activates on any active tag, you can attach its tag to a host scope instead of relying on the network scope. A host scope matches by hostname (always reliable) and can emit the same tag the network scope would have:
scope:
network:
- id: home
match: { gateway_mac: "aa:bb:cc:dd:ee:ff" }
tags: [home] # fires when the gateway is detected
host:
- id: laptop
match: { hostname: laptop }
tags: [home] # always fires on this host β manual fallback
features:
memory:
- server_host: fixed
port: 7878
when: [home] # active via either route
With this, laptop always emits home, so its agents always get the memory
client URL β even when the network can't be auto-detected. The host that
matches server_host additionally launches the local proxy.
Codebase memory (codebase_memory:)β
(added in v3.6.0)
codebase-memory-mcp is a
local code-intelligence MCP server β a knowledge graph of a codebase's
functions, classes, and call chains. Unlike the memory backend above, it has
no remote-serve mode: it always runs as a local stdio process per
project, so features.codebase_memory: entries carry no server_host/port
β just activation tags and an optional index-path override.
features:
codebase_memory:
- when: [my-project]
index_path: null # optional; unset defers to codebase-memory-mcp's own default
(changed in v3.11.1) llmenv sets environment variables for the launched server only when there's something explicit to set:
CBM_CACHE_DIRβ set toindex_pathwhen configured; otherwise left unset socodebase-memory-mcpfalls back to its own default cache locationCBM_ALLOWED_ROOTβ no longer set at all. Earlier versions pinned this to the current working directory soindex_repositorycouldn't be steered outside the intended project; llmenv no longer imposes that restriction β restricting the tool's scope is now the end user's call, made directly throughcodebase-memory-mcp's own config, not llmenv's default. See Configuration reference for the full detail on this change.
(added in v3.12.0) mem_budget_mb sets CBM_MEM_BUDGET_MB for both the server and the
SessionStart index, so the two agree:
features:
codebase_memory:
- when: [my-project]
mem_budget_mb: 4096
codebase-memory-mcp stops an index that goes over its budget, keeps the previous index, and
reports the numbers.
llmenv saves that result for each project next to index.log, in index-result-<key>.json.
llmenv doctor reads it and prints one line:
last index finished <time>when the index worked- a warning with the budget, the peak, and the
mem_budget_mbto set, when the index stopped at the budget - a warning with the status, the reason, and the log path, when it failed another way
no index result for this project yetbefore the first run
The time is in UTC.
(added in v3.12.0) codebase-memory-mcp 0.11.0 indexes only the roots in its allowed_roots file, once any root is recorded.
At each SessionStart llmenv records its roots through codebase-memory-mcp allow-root <path>,
with the same CBM_CACHE_DIR as the server.
The roots are:
- the project root
- the llmenv config, cache, and state folders
${NBL_DIAG_CACHE:-~/.cache/nbl-diag}/repos, the code-explorer cache- the entries of
allowed_roots
features:
codebase_memory:
- when: [my-project]
allowed_roots:
- ~/git
- $WORK_DIR/repos
~ and $VAR expand at session start.
An entry with an unset variable is dropped.
A relative entry or a ~user entry is rejected by llmenv validate.
llmenv skips a default root that does not exist yet, and warns about a configured entry that is not a folder.
The SessionStart notice and llmenv doctor list the roots, and warn about a root that the server did not accept.
A PreToolUse guard denies an index_repository call whose repo_path is outside the configured roots
and the roots the server lists.
The deny text names the config fix.
A root stays recorded after you remove it from the config, because codebase-memory-mcp has no command to remove one.
To revoke a root, delete its line from <cache dir>/allowed_roots.
Do not run allow-root by hand: the config is the source of the roots.
The background watcher is a setting of codebase-memory-mcp itself, not of llmenv.
Turn it off with codebase-memory-mcp config set watcher_enabled false.
Multiple codebase_memory entries may be active simultaneously β each is an
independent local process, not a shared resource like the memory backend, so
there's no "at most one active" restriction.
On SessionStart, llmenv fires a fire-and-forget
codebase-memory-mcp cli index_repository call for the active project. This
registers it with the server's own background auto-watch (auto_watch,
upstream default true), which re-indexes on git changes automatically β
llmenv doesn't implement its own reindex scheduling on top of that.
(changed in v3.11.2) llmenv's model guidance covers codebase-memory-mcp 0.11.0
tools, including get_file_outline and compare_graphs for detailed code
inspection and change detection.
After upgrading codebase-memory-mcp from 0.10.8 or earlier, the first session
in each project rebuilds that project's index once (index format change);
llmenv starts that index at session start, so large repositories are slow once.
The index_repository name guardβ
(added in v3.11.0)
index_repository takes an optional name that overrides the project key
its index is stored under. codebase-memory-mcp doesn't check whether that key
already belongs to a different repository, and a full reindex deletes and
recreates the index file β so one call can replace an unrelated project's
index with the current repo's data
(upstream #1578).
A scoping root β if configured β wouldn't prevent this either way: it bounds
the tree that gets read, not the key that gets written, and
codebase-memory-mcp's own default cache directory is one directory shared by
every project you've indexed.
When codebase-memory-mcp is active, llmenv registers a PreToolUse hook that
denies any index_repository call carrying a name, explaining why in
the deny reason. Calls without name β including llmenv's own
SessionStart auto-index β are unaffected, so the tool stays auto-allowed
and no per-session prompt appears.
This covers Claude Code and opencode, both of which receive the MCP server. Claude Code matches the hook to that one tool; opencode's plugin API has no per-tool matcher, so the hook runs on every tool call there and filters by name itself β which is why it is registered only when the MCP is wired.
If you genuinely need a custom project key, run codebase-memory-mcp yourself
so overwriting an existing index is a deliberate choice:
codebase-memory-mcp cli index_repository '{"repo_path": "/path/to/repo", "name": "custom-key"}'
(changed in v3.11.2) The same hook also denies index_repository with
persistence: true. That option makes codebase-memory-mcp write
.codebase-memory/graph.db.zst into the indexed repository, and because the
tool is auto-allowed, a model could otherwise add that artifact to a repo
without a prompt. To share a graph artifact on purpose, run
codebase-memory-mcp from a shell.
codebase_memory and memory (ICM) are fully independent: both can be
active at once, and llmenv does not coordinate between them.
See Configuration β features.codebase_memory:
for the full field reference.
Security considerationsβ
The memory backend has no transport security and no access control:
- The proxy binds to
<listen_host>:<port>. Withlisten_host: 0.0.0.0that is every interface, and llmenv warns when it starts such a proxy. Every client connects over plaintexthttp://β there is no TLS, so anything stored in memory crosses the wire in the clear. - There is no authentication. Any host that can reach
<addr>:<port>can read and write the memory backend. Access is gated only by network reachability β that is the trust model.
llmenv checks the address of an MCP endpoint against its private-network and SSRF rules before it connects, and the client follows no redirect (changed in v3.12.0).
Before v3.12.0 a client with no configured headers followed up to 10 redirects, and a redirect could reach an address that the check had not approved.
A tool call that the server answers with isError: true fails in llmenv, so a failed store is not recorded as stored (changed in v3.12.0).
The client refuses a plain http:// URL when any address that it resolves to is public (added in v3.12.0).
Plain http:// stays allowed for loopback, private (RFC 1918), unique-local (ULA), and CGNAT (100.64.0.0/10, used by Tailscale) addresses.
For a remote icm serve on a public address, put it behind https://.
llmenv applies the same rule when it renders the MCP config for the agent (added in v3.12.0).
A host: addr for the memory server, or an mcp: entry with an http:// URL, that is a public IP literal fails llmenv export, llmenv regenerate, and llmenv doctor.
The error names the server and says to use https://.
Before v3.12.0, only llmenv's own client refused cleartext to a public address, and the agent still connected in clear text with its bearer headers.
llmenv doctor also warns when the hostname of an http:// URL resolves to a public address.
llmenv does not look up hostnames at render time, so only doctor can find this case.
Deploy it only on a network you trust (home LAN, a private VPN, a firewalled
subnet). Do not expose the port to the public internet, and do not point the
host: addr at a publicly routable address. If you need to bridge hosts
across an untrusted network, tunnel the port over SSH or a VPN rather than
opening it directly.
Diagnosticsβ
List the MCP servers that resolve for the current environment:
llmenv status mcps
llmenv doctor flags orphaned MCP config:
- a server (or the memory backend) whose tags are never emitted by any scope (it can never activate),
- a memory
server_hostwith no entry in thehost:table.
(added in v3.12.0) On the host that serves memory, llmenv doctor also reports the ICM server version.
It warns below 0.10.60, where adaptive recall returns little, and below 0.10.64, where the ranking is weaker.
llmenv doctor
Session-start health check (added in v3.12.0)β
llmenv manages two MCP servers: the memory server (ICM) and codebase-memory-mcp.
A server that is stopped, or that holds its socket but never answers, used to leave the session
without memory or a code graph, with no sign of it.
At session start, llmenv now sends a real MCP initialize request to each of these servers.
It sends the requests in parallel and waits up to 5 seconds for each answer.
A remote server such as ICM gets an HTTP request.
A local server such as codebase-memory-mcp is started for the check and is stopped afterward.
If a server does not answer, the session start output names the server, the reason, what stops working, and the command that fixes it. A server that answers adds nothing to the output.
When this host serves memory and the proxy is stopped, llmenv starts the proxy first, as llmenv export does.
Then it asks again before it reports anything.
llmenv does not restart a proxy that holds its port but does not answer.
The notice tells you how to stop that proxy instead.
llmenv doctor runs the same check.
Its MCP servers: section prints one line for each managed server: a pass, or a warning with the reason and the fix.
Background work: request ids and checkpoints (added in v3.12.0)β
Four jobs run in a detached child so a hook returns at once:
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 under <state dir>/checkpoints/ before it starts,
and deletes the file when it succeeds.
If the child dies, or ICM is down, the file stays.
The next session start runs the job again, up to 3 attempts and at most 20 jobs for each start, and llmenv doctor lists what is left.
llmenv deletes a checkpoint after 7 days.
Consolidation keeps the model summary in its checkpoint, so a resume never pays for the model twice.
The index job is not resumed, because every session start runs the indexer anyway.
See Background work.
A resumed memory store or transcript record must not store the same thing twice.
llmenv derives a request id from the event, so a resume sends the same id.
The child records each stored id in <state dir>/idempotency/<session>.json (the last 1000 ids)
and skips an id that is already there.
The id also goes to ICM as the keyword request:<id> on a memory,
and in the metadata of a transcript record.
ICM accepts no request id yet, so a write whose response was lost can still land twice.
Troubleshootingβ
Wrong role on a hostβ
Which host runs the memory server keys off whether the current host matches a
host-scope whose id equals server_host. Verify the active scopes and tags:
llmenv status scopes
llmenv status tags
Client can't reach the serverβ
Confirm the host: entry resolves and the port is open on the server host:
nc -vz fixed.local 7878
Server not activatingβ
The server only renders when one of its tags is active. Check that a scope in
the current environment emits a matching tag (llmenv status tags).
Proxy won't startβ
(added in v3.8.0)
The warning from llmenv export quotes the tail of the proxy's log. For the full
output, read it directly:
tail -50 "${XDG_STATE_HOME:-$HOME/.local/state}/llmenv/mcp-proxy.log"
A common cause is a dependency resolution mcp-proxy can't import β it declares
an open-ended mcp requirement, so uvx mcp-proxy can pick a combination that
fails at import time. Pinning the install sidesteps it:
uv tool install mcp-proxy --with "mcp<2"
To reproduce a cold start deliberately, restart the proxy through llmenv (added in v3.12.0):
llmenv doctor --restart-memory-proxy
Tag-scoped memory and the env var contractβ
llmenv bridges the active scope into memory so that context can be stored once and recalled in any environment sharing the same tags β even across different projects. Two mechanisms carry this:
LLMENV_ICM_CONTEXTβ
On every llmenv export, llmenv emits LLMENV_ICM_CONTEXT: a markdown chunk
encoding the active tags, the firing bundles, and (when a project marker is
active) the project name and description. Its shape:
## llmenv context
Active tags: `office`, `rust`
Bundles: `base`, `office-tools`
Store scope-specific memory under keyword `llmenv-tag:<tag>` (per tag)
or `llmenv-bundle:<bundle>` (per bundle) so it is retrievable across
projects. On each turn, llmenv auto-recalls memory under these tags'
`llmenv-tag:<tag>` and bundles' `llmenv-bundle:<bundle>` keywords
across all projects.
**Project:** MyApp β Customer-facing API
Agents read this to learn which tags are live and how to key memory so it follows the tag rather than the project.
Keyword conventionβ
llmenv-tag:<tag>β memory keyed to a tag. Stored once, retrieved in any environment where that tag is active. The TurnStart hook recalls this keyword automatically across all projects (see Lifecycle hooks).llmenv-bundle:<bundle>β memory keyed to a bundle, retrieved whenever that bundle fires. The TurnStart hook recalls this keyword automatically across all projects (parallel tollmenv-tag:<tag>).
Lifecycle hooksβ
llmenv provides engine-neutral lifecycle hooks (hook-run command) for three
neutral events:
- SessionStart β
hook-run session_startinjects the session wake-up pack (icm_wake_up) containing your critical memories (by importance and recency). (changed in v3.12.0) Claude Code shows it under[ICM MEMORY CONTEXT (session start)]; a resumed or forked session skips the call, because its conversation already holds the earlier pack. - TurnStart β
hook-run turn_startinjects recalled context at the start of each agent turn (icm_memory_recall). It issues one project-unfiltered recall per active tag keyed onllmenv-tag:<tag>, and one per active bundle keyed onllmenv-bundle:<bundle>β so memory stored under a tag or bundle in one project surfaces when the same tag or bundle activates in another. It finishes with a natural-language recall on the active tags, filtered to the session's project (changed in v3.12.0: before, ICM filtered it by the working directory of theicm serveprocess, which names an unrelated project when ICM runs on another host). Withadaptive_recallon, the adaptive flow replaces this recall. (changed in v3.11.2) Recalls run from most to least specific scope: tags from the project's.llmenv.yamland$LLMENV_EXTRA_TAGS, then bundles, then tags from content, network, user and host scopes, then tags no scope supplied (such as the OS tag), then the natural-language recall. The injected text is capped at 8,000 bytes of whole memory records, duplicates are dropped, and one line reports how many lower-priority memories were left out. Claude Code saves a hook output above about 10 KB to a file and shows the model only a 2 KB preview, so without the cap the most specific memories could be cut off. - SessionEnd β
hook-run session_endstores the active scope context (icm_memory_store) when the session closes
The Claude Code adapter registers SessionStart, SessionEnd, and (added in v3.12.0)
PostModelSwitch unconditionally β
hook-run itself no-ops cheaply when no memory backend is configured, so this
costs nothing for users who only want session logging
and not ICM memory.
It registers TurnStart (a UserPromptSubmit hook) only when the ICM memory server is
one of the resolved MCP servers, because that hook runs on every prompt.
Only the Claude Code adapter registers these hook-run hooks by itself.
PostModelSwitch exists in Claude Code only.
Each hook talks to the memory backend over MCP. Failures degrade gracefully: a
missing or unreachable backend logs a warning and exits cleanly (exit code 0) so
hooks never block the agent. See docs/commands.md for
details.
Adaptive recall (added in v3.12.0)β
By default, llmenv sends each memory once per model context.
It chooses memories from the prompt, the recent tool calls, the newest tool error, and the task of a new subagent.
A compaction or /clear resets the state, so the scope-tagged memories go out again.
Set features.memory[].adaptive_recall: false to send the same scope-tagged memories on every prompt.
See memory: and hook-run.
Post-session consolidation (added in v3.12.0)β
Consolidation runs from the session_end hook.
It reads only the memories of the current project and stores each new rule under llmenv-consolidation-<project>.
See Post-session consolidation.
Session loggingβ
hook-run session_start/hook-run session_end also drive
session_log: β llmenv's separate
event-stream feature that records lifecycle/scope (and, with verbose: true,
every prompt and tool call) to a local file and/or ICM's transcript store. It
shares the same MCP-only-access rule as the memory backend above, but is
otherwise independent: it has its own on/off switch, doesn't require
features.memory: to be configured, and a down ICM never blocks its file
sink. See the session_log: reference for the
full field list and icm_transcript_search query recipes.
SessionStart injectionβ
The Claude Code adapter registers a SessionStart hook. Alongside
check-stale (drift detection), llmenv records the active tag/bundle set to a
0600 state file (icm.json in the state dir) so the hook can surface the
keyword convention to the agent at startup. The hook-run session_start command
is also invoked at session start to inject ICM memory.
Related introspection varsβ
LLMENV_ICM_CONTEXT is one of several vars export emits. The full set β
LLMENV_ACTIVE_SCOPES, LLMENV_ACTIVE_TAGS, LLMENV_ACTIVE_BUNDLES,
LLMENV_ACTIVE_PROJECT, LLMENV_PROJECT_ROOT, LLMENV_ICM_CONTEXT β is
documented in the README
and Concepts.