From e365db1c6491655005a1e99c3f07e798f82029f5 Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Wed, 24 Jun 2026 16:31:08 -0700 Subject: [PATCH] claude-remote-control: run a session per project directory Generalize the always-on Remote Control module from a single session pinned to /srv/dotfiles into one systemd user service per directory, so new conversations can be started remotely (claude.ai/code / mobile) in any of them without running `claude rc` by hand. Each directory gets its own tmux socket, session, and Remote Control name derived from its base name, plus a `claude-rc-attach-` alias. Directories that don't exist on a given host are skipped (impure pathExists check) so the module can be shared across machines. Initial set: dotfiles, subtr-actor, rocket-sense, mova. Enabled on both railbird-sf and ryzen-shine. Co-Authored-By: Claude Opus 4.8 (1M context) --- nixos/claude-remote-control.nix | 96 +++++++++++++++++++++------------ nixos/machines/ryzen-shine.nix | 1 + 2 files changed, 64 insertions(+), 33 deletions(-) diff --git a/nixos/claude-remote-control.nix b/nixos/claude-remote-control.nix index a1c3e0bf..b1cbad28 100644 --- a/nixos/claude-remote-control.nix +++ b/nixos/claude-remote-control.nix @@ -5,18 +5,26 @@ makeEnable, ... }: let - # Working directory the always-on session is pinned to. Ad-hoc sessions in - # other directories are handled by the `tmclaude` shell function instead. - workingDirectory = "/srv/dotfiles"; + # Directories that each get an always-on Remote Control session, so new + # conversations can be started in any of them from claude.ai/code or the + # Claude mobile app (without having to run `claude rc` by hand first). + # + # Each directory gets its own systemd user service, tmux server (dedicated + # `-L` socket) and Remote Control session name derived from the directory's + # base name. Attach locally with the generated `claude-rc-attach-` + # alias, e.g. `claude-rc-attach-dotfiles`. + candidateDirectories = [ + "/srv/dotfiles" + "/home/imalison/Projects/subtr-actor" + "/home/imalison/Projects/rocket-sense" + "/home/imalison/code/mova" + ]; - # Dedicated tmux socket + session name so the service owns its own tmux - # server (independent of the interactive one). Attach locally with: - # tmux -L claude-rc attach -t claude-rc - socket = "claude-rc"; - sessionName = "claude-rc"; - - # Name the session registers under for native Remote Control (phone/web). - remoteName = config.networking.hostName; + # Only run a session for directories that actually exist on this host, so the + # same module can be enabled across machines where some projects live at + # different paths (or not at all). Relies on impure evaluation, which the + # `just switch` workflow already uses (`nixos-rebuild ... --impure`). + directories = builtins.filter builtins.pathExists candidateDirectories; # claude shells out to these for its tools; give the service a clean PATH. servicePath = lib.makeBinPath (with pkgs; [ @@ -34,31 +42,53 @@ ripgrep zsh ]); + + mkName = dir: baseNameOf dir; + + mkService = dir: let + name = mkName dir; + # Dedicated tmux socket + session name so each service owns its own tmux + # server, independent of the interactive one and of every other session. + socket = "claude-rc-${name}"; + sessionName = "claude-rc-${name}"; + # Name the session registers under for native Remote Control (phone/web). + remoteName = "${config.networking.hostName}-${name}"; + in { + name = "claude-remote-control-${name}"; + value = { + Unit = { + Description = "Claude Code remote-control session (${dir})"; + After = ["network.target"]; + }; + Service = { + # tmux new-session -d daemonizes the server and returns. + Type = "forking"; + Environment = ["PATH=${servicePath}"]; + ExecStart = lib.concatStringsSep " " [ + "${pkgs.tmux}/bin/tmux -L ${socket} new-session -d" + "-s ${sessionName} -c ${dir}" + "${pkgs.claude-code}/bin/claude --remote-control ${remoteName} --dangerously-skip-permissions" + ]; + ExecStop = "${pkgs.tmux}/bin/tmux -L ${socket} kill-server"; + Restart = "on-failure"; + RestartSec = 5; + }; + Install.WantedBy = ["default.target"]; + }; + }; + + mkAlias = dir: let + name = mkName dir; + in { + name = "claude-rc-attach-${name}"; + value = "tmux -L claude-rc-${name} attach -t claude-rc-${name}"; + }; in makeEnable config "myModules.claudeRemoteControl" false { home-manager.users.imalison = { - systemd.user.services.claude-remote-control = { - Unit = { - Description = "Claude Code remote-control session"; - After = ["network.target"]; - }; - Service = { - # tmux new-session -d daemonizes the server and returns. - Type = "forking"; - Environment = ["PATH=${servicePath}"]; - ExecStart = lib.concatStringsSep " " [ - "${pkgs.tmux}/bin/tmux -L ${socket} new-session -d" - "-s ${sessionName} -c ${workingDirectory}" - "${pkgs.claude-code}/bin/claude --remote-control ${remoteName} --dangerously-skip-permissions" - ]; - ExecStop = "${pkgs.tmux}/bin/tmux -L ${socket} kill-server"; - Restart = "on-failure"; - RestartSec = 5; - }; - Install.WantedBy = ["default.target"]; - }; + systemd.user.services = builtins.listToAttrs (map mkService directories); - # Convenience: attach to the always-on session from any directory. - home.shellAliases.claude-rc-attach = "tmux -L ${socket} attach -t ${sessionName}"; + # Convenience: attach to any always-on session from any directory. + home.shellAliases = builtins.listToAttrs (map mkAlias directories); }; } diff --git a/nixos/machines/ryzen-shine.nix b/nixos/machines/ryzen-shine.nix index aed4d98e..3ef42575 100644 --- a/nixos/machines/ryzen-shine.nix +++ b/nixos/machines/ryzen-shine.nix @@ -12,6 +12,7 @@ ]; features.full.enable = true; + myModules.claudeRemoteControl.enable = true; myModules.games.enable = lib.mkForce true; myModules.kubelet.enable = false; myModules.nvidia.enable = true;