From 33260b4cb5856a62bf2442e8eac8521885c4c290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Boh=C3=A1=C4=8Dek?= Date: Tue, 20 Oct 2026 18:04:02 +0200 Subject: [PATCH] Add launchers..sharedDirs: share a state-dir directory between launchers Launchers with different ids share nothing, which is right for logins but not for Claude's projects (sessions and memory) or Codex's memories. A shared dir is a managed symlink to the other launcher's copy, created on demand so the link holds whichever launcher starts first, and removed with the declaration while the data stays. --- README.md | 1 + modules/launcher.nix | 29 ++++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c50285371d85bf931a835e6faba0b57f9928e915..3f8ffc9df112e039b600db4bab43f2dfbbfddd6c 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ Runnable examples: [`examples/basic`](examples/basic) and | `packages` | Packages put on every launcher's `PATH`, e.g. tools that hooks in `claude.settings` call. | | `modules.skills..enable` | Declared by `mkSkillModule`; adds that skill. | | `launchers..{agent,id,stateDir}` | A command. `id` defaults to the command name; `stateDir` defaults to `~/.local/agent-environments/`; it must be absolute or start with `~/`, and is taken literally. | +| `launchers..sharedDirs.` | Share one directory of the state dir with another launcher, e.g. `projects = "claude-work"` for sessions and memory across logins. A managed symlink to that launcher's `/`, created if missing. | | `companions.enable` | Put every launcher of the environment on the agent's `PATH` (as companions, which cannot start companions themselves) and add a generated `companions` skill listing each one with its non-interactive call. Launchers export `AGENT_ENV_LAUNCHER` (their command name). | ## What a launcher does diff --git a/modules/launcher.nix b/modules/launcher.nix index 65ed8388e76db42cfb8234cf86d5fd5b38f44ea8..06947a67c39d311f4746dc7f826d55ca3b137ffa 100644 --- a/modules/launcher.nix +++ b/modules/launcher.nix @@ -41,6 +41,11 @@ let let agent = build.agents.${launcher.agent}; links = managedLinks agent; + # Shared directories: relative path -> shell expression for the other + # launcher's copy, which may live under $HOME. + shared = lib.mapAttrs ( + dir: other: "${stateDirShell config.launchers.${other}.stateDir}/${lib.escapeShellArg dir}" + ) launcher.sharedDirs; runtimePath = lib.makeBinPath ( lib.optional (companionBin != null) companionBin ++ build.runtimeInputs ); @@ -65,6 +70,7 @@ let ${ lib.concatStrings ( lib.mapAttrsToList (p: target: " [${lib.escapeShellArg p}]=${lib.escapeShellArg target}\n") links + ++ lib.mapAttrsToList (p: target: " [${lib.escapeShellArg p}]=${target}\n") shared ) }) declare -A owned=() @@ -92,6 +98,10 @@ let fi done + # A shared directory is created where it lives, so the link never dangles. + ${lib.concatStrings ( + lib.mapAttrsToList (p: _: "${mkdir} -p \"\${wanted[${lib.escapeShellArg p}]}\"\n") shared + )} for p in "''${!wanted[@]}"; do # No -f: a unique temp name that already exists is an error, not something to delete. tmp="$state/$p.agent-env-tmp.$$" @@ -141,6 +151,11 @@ in default = "~/.local/agent-environments/${config.id}"; description = "State dir (CLAUDE_CONFIG_DIR / CODEX_HOME): an absolute path, or one starting with ~/ for $HOME. Taken literally."; }; + sharedDirs = mkOption { + type = types.attrsOf types.str; + default = { }; + description = "Directories of the state dir shared with another launcher: ` = \"\"` links to that launcher's , creating it if missing."; + }; }; } ) @@ -184,7 +199,19 @@ in assertion = config.build.agents ? ${l.agent}; message = "launcher '${name}' uses ${l.agent}, but ${l.agent}.enable is false"; } - ]) config.launchers + ] + ++ lib.concatLists ( + lib.mapAttrsToList (dir: other: [ + { + assertion = agentEnvLib.safeName dir; + message = "launcher '${name}': shared dir '${dir}' is not a safe path component"; + } + { + assertion = other != name && config.launchers ? ${other}; + message = "launcher '${name}': shared dir '${dir}' names '${other}', which is not another launcher"; + } + ]) l.sharedDirs + )) config.launchers ); }; }