Engines
llmenv emits agent-native configuration through pluggable adapters. The configuration you write is engine-neutral; each adapter translates it into one engine's native shape. Anything that can't be expressed neutrally drops through a per-engine escape hatch.
The current adapter targets Claude Code. The design doc behind this model is
docs/design/engine-capabilities.md (related: #34, #59).
The principle
Don't model the container. Model the capabilities inside it.
The portable concepts — which tools are allowed, which paths are reachable, which
hooks fire on which events, which plugins load — are engine-agnostic. Each
adapter renders them into its native config. Everything non-portable goes through
a per-engine native passthrough.
Two layers
Every modeled feature has both of these:
- Generic capability — an engine-neutral declaration, translated per
adapter. Lives under
capabilities:(permissions,hooks,plugins) and undermcp:for servers. - Per-engine
native_<feature>override — a raw fragment in the engine's own language, emitted verbatim. Named as a top-level sibling undercapabilities::native_permissions,native_hooks,native_plugins,native_mcp.
A feature with only layer 1 is considered incomplete — there is always some platform-specific need (a Claude-only permission grammar, a Codex-only hook event) that requires the override.
capabilities:
permissions:
default_mode: acceptEdits
deny:
- { tool: Read, paths: ["./.env", "./.env.*"] }
native_permissions:
claude_code:
deny: ["WebFetch(domain:internal.example.com)"]
The neutral {tool, pattern} / {tool, paths} form covers the common case; the
adapter generates Claude's Bash(...) / Read(...) string grammar — you never
author it. native_permissions appends raw rule strings for the long tail.
The catch-all native: block
Separately, the top-level native: block is a per-engine catch-all for keys that
belong to no modeled feature (e.g. alwaysThinkingEnabled, outputStyle):
native:
claude_code:
alwaysThinkingEnabled: true
It is overlaid onto the engine's config last. Putting a modeled-feature key
(permissions, hooks) here is a hard error — that belongs in the matching
native_<feature> sibling, so the security-rendered output is never silently
clobbered.
What the Claude Code adapter emits
For each materialized environment, the adapter writes (all with 0600
permissions):
| File | From |
|---|---|
CLAUDE.md | the merged AGENTS.md / rules content |
settings.json | permissions, hooks, plugins (+ native_* overrides, + native: catch-all) |
mcp.json | resolved MCP servers (+ native_mcp) |
It also:
- sets
CLAUDE_CONFIG_DIRto the materialized directory so Claude Code uses it; - derives
enabledMcpjsonServersfrom every server llmenv emits (so the agent never re-prompts to approve a server llmenv configured) — anative_mcpoverride of the key replaces the derived list; - emits
autoMemoryEnabled: falsewhen the ICM memory server is present, so ICM and Claude's native auto-memory don't both write (anativeoverride wins); - registers a
SessionStarthook runningllmenv check-stalefor drift detection.
Where capabilities are declared
Capabilities can be declared at two levels with identical shape:
- Globally under
capabilities:inconfig.yaml. - Per bundle in an optional
bundle.yamlinside the bundle's content directory — keeping a hook's script and its registration together so the bundle versions as a unit.
Contributors merge by value shape: scalars (like default_mode) resolve by
scope precedence (network → host → user → project); lists (allow/ask/deny, hooks,
plugins) concatenate and de-duplicate.
Other engines
The capability model is engine-neutral by design, so additional adapters (e.g.
Codex) can render the same neutral config into their own shape and expose their
own native_* overrides. Only the Claude Code adapter ships today.