# agent-environments — design Status: self-authored during an autonomous run (user asked to work autonomously and consult Codex). Reviewed by Codex (review-1: blocking points folded in), not yet by the user. ## Goal A Nix flake **library** for declaring reproducible configurations ("environments") for coding agents — Claude Code and Codex for now — and launching them under separate identities (logins). Other flakes consume it; `examples/` shows how. ## Requirements (from the user) - Module-based: modules add text to the instruction file (AGENTS.md / CLAUDE.md) and enable skills, e.g. `modules.skills.xyz.enable = true`. - Several launchers such as `claude-work-acc`, `claude-pers-acc`. They may share one environment but log in separately. Each launcher gets its own state directory `~/.local/agent-environments/`, passed in with `CLAUDE_CONFIG_DIR` / `CODEX_HOME`. - Each environment is its own set of modules. - Skills are packaged. A skill can need runtime tools and other skills. - Flakes. `claude-code` and `codex` come in as inputs: `github:sadjow/claude-code-nix`, `github:sadjow/codex-cli-nix`. - Standalone only (no home-manager). The root flake exposes the library plus one default environment with no skills. ## Architecture ``` flake.nix inputs: nixpkgs, claude-code, codex lib/default.nix mkEnvironment, mkSkill, mkSkillModule modules/core.nix instructions, skills, launchers, assertions modules/claude.nix claude.{enable,package,settings} modules/codex.nix codex.{enable,package,settings} modules/launcher.nix wrapper script generation checks/ behaviour tests (stub agents, temp HOME) examples//flake.nix consumer flakes ``` ### Evaluation `lib.mkEnvironment { pkgs, modules ? [] }` runs `lib.evalModules` over the core modules plus the user's modules, with `pkgs` passed as a module argument. It returns the evaluated `config` along with `package` (a `symlinkJoin` of every launcher) and `launchers` (an attrset of per-launcher packages). The flake's `lib` is system-independent. `mkEnvironment` receives `pkgs`, and the flake sets default agent packages for `pkgs.stdenv.hostPlatform.system` from its own inputs. Consumers can override those with `claude.package` / `codex.package`. ### Core options - `instructions. = { text : lines; order : int = 1000; }` — fragments sorted by `(order, name)` and joined with a blank line into one document. That document becomes `CLAUDE.md` for Claude and `AGENTS.md` for Codex. Modules add a fragment by defining an entry. - `skills.packages : listOf package` — skill derivations made by `mkSkill`. The effective set is the transitive closure over `passthru.skillDeps`, deduplicated by skill name. Two different derivations with the same name → evaluation error. - `claude.enable`, `claude.package`, `claude.settings : attrs` (JSON). - `codex.enable`, `codex.package`, `codex.settings : attrs` (TOML). - `launchers. = { agent : enum ["claude" "codex"]; id : str = ; stateDir : str = "~/.local/agent-environments/${id}"; }`. The name is the command name. `stateDir` must be absolute or start with `~/` (assertion); it is shell-quoted as a literal, and only the leading `~/` becomes `$HOME`. A launcher's agent must be enabled (assertion). `id` and launcher names must match `^[A-Za-z0-9][A-Za-z0-9._-]*$` (assertion). - Launchers are always declared explicitly. There are no implicit default launchers, because attrset options merge by key and a hidden default could not be dropped cleanly. ### Modules namespace `modules.*` is where optional features live, e.g. `modules.skills..enable`. The helper `lib.mkSkillModule skill` produces a module declaring `options.modules.skills..enable` and, when it is enabled, adding the skill to `skills.packages`. Options cannot be generated from `config` (that would recurse infinitely), so consumers import one module per skill they want to make available. Instruction modules are plain modules that define `instructions.`. ### Skill packaging `lib.mkSkill { pkgs, name, src, runtimeInputs ? [], skills ? [] }`: - `name` must match `^[A-Za-z0-9][A-Za-z0-9._-]*$` (evaluation-time check). - Output: `$out/` is the skill directory (it must contain `SKILL.md`; checked at build time). The same directory works for both agents, since Claude Code and Codex share the `SKILL.md` format. - `passthru = { skillName = name; runtimeInputs; skillDeps = skills; }`. - Runtime inputs: the launcher puts the union of all effective skills' `runtimeInputs` on `PATH` (prepended). Skills run inside the agent's shell, so this is the smallest mechanism that works for both agents. ### Launcher (runtime) The wrapper is a `writeShellApplication` per launcher. Everything it manages is listed in a manifest, `$state/.agent-environments-managed` (one relative path per line). On every start it: 1. `mkdir -p "$state/skills"`, then takes `flock` on `$state/.agent-environments.lock` for the setup phase only; the lock fd is closed before `exec`. 2. Computes the wanted set of managed links: the instruction file (`CLAUDE.md` for claude, `AGENTS.md` for codex, when there are instructions), `skills/` for each effective skill, and the codex profile TOML (when there are settings). 3. Removes every path in the old manifest that is not wanted any more, but only when it is still a symlink. Anything else is left in place. 4. For each wanted path that exists: unless it is listed in the old manifest and is still a symlink, stop with an error naming the path (user data, including user symlinks, is never replaced and there are no automatic backups). Otherwise replace it atomically (`ln -s` to a temp name, then `mv -T`). 5. Writes the new manifest atomically. Codex's own `skills/.system` and any user-installed skills are never touched, because they are not in the manifest. 6. Settings, layered so the agents stay free to write their own files: - Claude: `--settings ` when `claude.settings != {}`. `settings.json`, `.claude.json` and `.credentials.json` in the state dir stay owned by Claude. - Codex: `$state/agent-environments.config.toml` (a managed link to the store TOML) plus `--profile agent-environments`, which layers `$CODEX_HOME/.config.toml` over `config.toml`. `config.toml` (which holds trust entries) and `auth.json` stay owned by Codex. Skipped when `codex.settings == {}`. 7. `export CLAUDE_CONFIG_DIR="$state"` / `export CODEX_HOME="$state"`, prepend the runtime inputs to `PATH`, then `exec "$@"`. Logins are therefore per launcher: `claude-work-acc` and `claude-pers-acc` have different ids, so different state dirs, so different credentials, even when they come from the same environment. Two launchers that should share a login can set the same `id`. They then also share managed content: the last launch wins, so they should come from the same environment. This is documented, not enforced. ### Root flake outputs - `lib` — `mkEnvironment`, `mkSkill`, `mkSkillModule`. - `packages..default` — the default environment: claude + codex enabled, launchers `claude` and `codex` with ids `default-claude` / `default-codex`, no skills and no instructions. - `checks..*` — behaviour tests. - `formatter` — `nixfmt`. ### Examples - `examples/basic` — one environment, one instruction fragment, one local skill with a runtime dependency and a skill dependency. - `examples/multi-account` — one module set, launchers `claude-work-acc`, `claude-pers-acc`, `codex-work-acc`, plus a second environment with a different module set. Each example is a flake with `inputs.agent-environments.url = "path:../..";`. ## Error handling - Evaluation-time assertions: a launcher's agent is disabled; an unsafe id, launcher or skill name; conflicting skill names. A skill without `SKILL.md` fails at build time. - Runtime: never delete non-symlink files and never replace unmanaged ones; fail loudly (`set -euo pipefail`) and say which path conflicts. ## Testing `nix flake check` runs derivation checks that execute the launchers with stub `claude`/`codex` packages (scripts that dump argv, env and state-dir contents), using a temporary `HOME`: - the state dir is created under `$HOME/.local/agent-environments/` and the right env var points at it; - two launchers from one environment get different state dirs (separate logins); - the instruction file carries the fragments in order; - dependency skills are linked transitively; a skill's runtime tool is on `PATH`; - a skill that is no longer enabled is unlinked on the next launch, while user skill dirs, user symlinks and `.system` are kept; - a pre-existing user `CLAUDE.md` makes the launcher fail and is left untouched; - the `--settings` / `--profile` flags appear only when settings are non-empty. One integration check runs the real Codex package offline to confirm that it discovers `$CODEX_HOME/AGENTS.md`, the skills and the profile, if Codex has a non-interactive way to show this. If it does not, that is recorded as a coverage gap and checked by hand. Examples are built with `nix build ./examples/ --override-input agent-environments path:$PWD`. ## Out of scope (YAGNI) home-manager integration, MCP server modules, plugins, per-project config, agents other than Claude Code and Codex.