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.
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.
modules.skills.xyz.enable = true.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/<id>, passed in with
CLAUDE_CONFIG_DIR / CODEX_HOME.claude-code and codex come in as inputs:
github:sadjow/claude-code-nix, github:sadjow/codex-cli-nix.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/<name>/flake.nix consumer flakes
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.
instructions.<name> = { 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.<name> = { agent : enum ["claude" "codex"]; id : str = <name>; 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).modules.* is where optional features live, e.g. modules.skills.<name>.enable.
The helper lib.mkSkillModule skill produces a module declaring
options.modules.skills.<skill.skillName>.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.<name>.
lib.mkSkill { pkgs, name, src, runtimeInputs ? [], skills ? [] }:
name must match ^[A-Za-z0-9][A-Za-z0-9._-]*$ (evaluation-time check).$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; }.runtimeInputs on PATH (prepended). Skills run inside the agent's shell, so
this is the smallest mechanism that works for both agents.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:
mkdir -p "$state/skills", then takes flock on
$state/.agent-environments.lock for the setup phase only; the lock fd is
closed before exec.CLAUDE.md for claude, AGENTS.md for codex, when there are instructions),
skills/<name> for each effective skill, and the codex profile TOML (when
there are settings).ln -s to a temp name, then mv -T).skills/.system and any
user-installed skills are never touched, because they are not in the manifest.--settings <store settings.json> when claude.settings != {}.
settings.json, .claude.json and .credentials.json in the state dir
stay owned by Claude.$state/agent-environments.config.toml (a managed link to the store
TOML) plus --profile agent-environments, which layers
$CODEX_HOME/<profile>.config.toml over config.toml. config.toml
(which holds trust entries) and auth.json stay owned by Codex. Skipped
when codex.settings == {}.export CLAUDE_CONFIG_DIR="$state" / export CODEX_HOME="$state", prepend
the runtime inputs to PATH, then exec <agent> <managed flags> "$@".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.
lib — mkEnvironment, mkSkill, mkSkillModule.packages.<system>.default — the default environment: claude + codex enabled,
launchers claude and codex with ids default-claude / default-codex,
no skills and no instructions.checks.<system>.* — behaviour tests.formatter — nixfmt.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:../..";.
SKILL.md
fails at build time.set -euo pipefail) and say which path conflicts.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:
$HOME/.local/agent-environments/<id> and the
right env var points at it;PATH;.system are kept;CLAUDE.md makes the launcher fail and is left untouched;--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/<x> --override-input agent-environments path:$PWD.
home-manager integration, MCP server modules, plugins, per-project config, agents other than Claude Code and Codex.