Skip to main content

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:

  1. Generic capability — an engine-neutral declaration, translated per adapter. Lives under capabilities: (permissions, hooks, plugins) and under mcp: for servers.
  2. Per-engine native_<feature> override — a raw fragment in the engine's own language, emitted verbatim. Named as a top-level sibling under capabilities:: 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):

FileFrom
CLAUDE.mdthe merged AGENTS.md / rules content
settings.jsonpermissions, hooks, plugins (+ native_* overrides, + native: catch-all)
mcp.jsonresolved MCP servers (+ native_mcp)

It also:

  • sets CLAUDE_CONFIG_DIR to the materialized directory so Claude Code uses it;
  • derives enabledMcpjsonServers from every server llmenv emits (so the agent never re-prompts to approve a server llmenv configured) — a native_mcp override of the key replaces the derived list;
  • emits autoMemoryEnabled: false when the ICM memory server is present, so ICM and Claude's native auto-memory don't both write (a native override wins);
  • registers a SessionStart hook running llmenv check-stale for drift detection.

Where capabilities are declared

Capabilities can be declared at two levels with identical shape:

  • Globally under capabilities: in config.yaml.
  • Per bundle in an optional bundle.yaml inside 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.