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-<name>` 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) <noreply@anthropic.com>
This commit is contained in:
2026-06-24 16:31:08 -07:00
parent 2567d2828f
commit e365db1c64
2 changed files with 64 additions and 33 deletions

View File

@@ -5,18 +5,26 @@
makeEnable, makeEnable,
... ...
}: let }: let
# Working directory the always-on session is pinned to. Ad-hoc sessions in # Directories that each get an always-on Remote Control session, so new
# other directories are handled by the `tmclaude` shell function instead. # conversations can be started in any of them from claude.ai/code or the
workingDirectory = "/srv/dotfiles"; # 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-<name>`
# 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 # Only run a session for directories that actually exist on this host, so the
# server (independent of the interactive one). Attach locally with: # same module can be enabled across machines where some projects live at
# tmux -L claude-rc attach -t claude-rc # different paths (or not at all). Relies on impure evaluation, which the
socket = "claude-rc"; # `just switch` workflow already uses (`nixos-rebuild ... --impure`).
sessionName = "claude-rc"; directories = builtins.filter builtins.pathExists candidateDirectories;
# Name the session registers under for native Remote Control (phone/web).
remoteName = config.networking.hostName;
# claude shells out to these for its tools; give the service a clean PATH. # claude shells out to these for its tools; give the service a clean PATH.
servicePath = lib.makeBinPath (with pkgs; [ servicePath = lib.makeBinPath (with pkgs; [
@@ -34,31 +42,53 @@
ripgrep ripgrep
zsh 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 in
makeEnable config "myModules.claudeRemoteControl" false { makeEnable config "myModules.claudeRemoteControl" false {
home-manager.users.imalison = { home-manager.users.imalison = {
systemd.user.services.claude-remote-control = { systemd.user.services = builtins.listToAttrs (map mkService directories);
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"];
};
# Convenience: attach to the always-on session from any directory. # Convenience: attach to any always-on session from any directory.
home.shellAliases.claude-rc-attach = "tmux -L ${socket} attach -t ${sessionName}"; home.shellAliases = builtins.listToAttrs (map mkAlias directories);
}; };
} }

View File

@@ -12,6 +12,7 @@
]; ];
features.full.enable = true; features.full.enable = true;
myModules.claudeRemoteControl.enable = true;
myModules.games.enable = lib.mkForce true; myModules.games.enable = lib.mkForce true;
myModules.kubelet.enable = false; myModules.kubelet.enable = false;
myModules.nvidia.enable = true; myModules.nvidia.enable = true;