From 248534bc35268df6c6f087e667f8a2ef77c20efd Mon Sep 17 00:00:00 2001 From: Dominik Polakovics Date: Fri, 6 Feb 2026 22:49:55 +0100 Subject: [PATCH] feat(dev): deploy claude-code config via systemd instead of home-manager Home-manager fails on the dev microVM because nix-env --set needs writable nix state dirs, but the microVM shares /nix/store read-only via virtiofs. Extract shared claude-code settings into settings.nix, add a NixOS module (nixos.nix) that deploys the same files via a systemd oneshot service with RequiresMountsFor to handle virtiofs mount ordering. The nb host continues using home-manager unchanged. --- hosts/dev/configuration.nix | 1 + hosts/dev/users/default.nix | 3 ++ hosts/dev/users/dominik.nix | 6 +++ hosts/nb/users/dominik.nix | 4 +- utils/home-manager/claude-code/default.nix | 22 +++++++++++ utils/home-manager/claude-code/nixos.nix | 35 ++++++++++++++++ utils/home-manager/claude-code/settings.nix | 44 +++++++++++++++++++++ 7 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 hosts/dev/users/default.nix create mode 100644 hosts/dev/users/dominik.nix create mode 100644 utils/home-manager/claude-code/default.nix create mode 100644 utils/home-manager/claude-code/nixos.nix create mode 100644 utils/home-manager/claude-code/settings.nix diff --git a/hosts/dev/configuration.nix b/hosts/dev/configuration.nix index 5351e90..ae62573 100644 --- a/hosts/dev/configuration.nix +++ b/hosts/dev/configuration.nix @@ -29,6 +29,7 @@ in { imports = [ ./modules/dev-tools.nix + ./users ]; networking.hostName = "dev"; diff --git a/hosts/dev/users/default.nix b/hosts/dev/users/default.nix new file mode 100644 index 0000000..4bd7e2c --- /dev/null +++ b/hosts/dev/users/default.nix @@ -0,0 +1,3 @@ +{ + imports = [ ./dominik.nix ]; +} diff --git a/hosts/dev/users/dominik.nix b/hosts/dev/users/dominik.nix new file mode 100644 index 0000000..399b218 --- /dev/null +++ b/hosts/dev/users/dominik.nix @@ -0,0 +1,6 @@ +{ config, lib, pkgs, ... }: +{ + imports = [ + ../utils/home-manager/claude-code/nixos.nix + ]; +} diff --git a/hosts/nb/users/dominik.nix b/hosts/nb/users/dominik.nix index 87eb660..586a19c 100644 --- a/hosts/nb/users/dominik.nix +++ b/hosts/nb/users/dominik.nix @@ -188,7 +188,9 @@ in }; home-manager.users.dominik = { lib, pkgs, ... }: { - # imports = [ "${impermanence}/home-manager.nix" ]; + imports = [ + ../utils/home-manager/claude-code + ]; /* The home.stateVersion option does not have a default and must be set */ home.stateVersion = "25.05"; home.enableNixpkgsReleaseCheck = false; diff --git a/utils/home-manager/claude-code/default.nix b/utils/home-manager/claude-code/default.nix new file mode 100644 index 0000000..6f43c70 --- /dev/null +++ b/utils/home-manager/claude-code/default.nix @@ -0,0 +1,22 @@ +{ config, lib, pkgs, ... }: +let + settings = import ./settings.nix { homeDir = config.home.homeDirectory; }; +in +{ + home.file = { + # Agents + ".claude/agents/devil-advocate.md".source = ./agents/devil-advocate.md; + ".claude/agents/lint-fixer.md".source = ./agents/lint-fixer.md; + ".claude/agents/secret-scanner.md".source = ./agents/secret-scanner.md; + ".claude/agents/test-runner.md".source = ./agents/test-runner.md; + + # Statusline script + ".claude/statusline-command.sh" = { + source = ./statusline-command.sh; + executable = true; + }; + + # Settings (local override — leaves settings.json writable for Claude) + ".claude/settings.local.json".text = builtins.toJSON settings; + }; +} diff --git a/utils/home-manager/claude-code/nixos.nix b/utils/home-manager/claude-code/nixos.nix new file mode 100644 index 0000000..42776b0 --- /dev/null +++ b/utils/home-manager/claude-code/nixos.nix @@ -0,0 +1,35 @@ +{ pkgs, ... }: +let + agentsDir = ./agents; + statuslineScript = ./statusline-command.sh; + settings = import ./settings.nix { homeDir = "/home/dominik"; }; + settingsJson = pkgs.writeText "claude-settings-local.json" (builtins.toJSON settings); + + deployScript = pkgs.writeShellScript "deploy-claude-code" '' + install -d -m 755 -o 1000 -g 100 /home/dominik/.claude + install -d -m 755 -o 1000 -g 100 /home/dominik/.claude/agents + install -m 644 -o 1000 -g 100 ${agentsDir}/devil-advocate.md /home/dominik/.claude/agents/ + install -m 644 -o 1000 -g 100 ${agentsDir}/lint-fixer.md /home/dominik/.claude/agents/ + install -m 644 -o 1000 -g 100 ${agentsDir}/secret-scanner.md /home/dominik/.claude/agents/ + install -m 644 -o 1000 -g 100 ${agentsDir}/test-runner.md /home/dominik/.claude/agents/ + install -m 755 -o 1000 -g 100 ${statuslineScript} /home/dominik/.claude/statusline-command.sh + install -m 644 -o 1000 -g 100 ${settingsJson} /home/dominik/.claude/settings.local.json + ''; +in +{ + # Deploy claude-code config files via a systemd service instead of home-manager. + # This avoids the nix-env --set call that fails on microVMs with read-only /nix/store. + systemd.services.claude-code-dominik = { + description = "Deploy Claude Code config for dominik"; + wantedBy = [ "multi-user.target" ]; + # Wait for /home to be mounted (virtiofs on microVMs) + unitConfig.RequiresMountsFor = "/home/dominik"; + # Rerun on config changes during nixos-rebuild switch + restartTriggers = [ deployScript ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = deployScript; + }; + }; +} diff --git a/utils/home-manager/claude-code/settings.nix b/utils/home-manager/claude-code/settings.nix new file mode 100644 index 0000000..e40557f --- /dev/null +++ b/utils/home-manager/claude-code/settings.nix @@ -0,0 +1,44 @@ +{ homeDir }: +{ + env = { + CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1"; + }; + statusLine = { + type = "command"; + command = "${homeDir}/.claude/statusline-command.sh"; + }; + hooks.Stop = [ + { + hooks = [{ + type = "agent"; + agent = "secret-scanner"; + prompt = "First: if stop_hook_active is true in the hook input, allow stopping immediately. Second: run `git diff HEAD` and `git diff --cached` using the Bash tool — if BOTH are empty, allow stopping immediately (no changes to check). Otherwise: Scan the diff for accidentally committed secrets. Check .claude/secret-scanner.md for project-specific allowlists. If secrets are found, they must be removed before the session can end. If no secrets found, allow stopping."; + timeout = 120; + }]; + } + { + hooks = [{ + type = "agent"; + agent = "lint-fixer"; + prompt = "First: if stop_hook_active is true in the hook input, allow stopping immediately. Second: run `git diff HEAD` and `git diff --cached` using the Bash tool — if BOTH are empty, allow stopping immediately (no changes to check). Otherwise: Run the project's linter/formatter. Check .claude/lint-fixer.md for project-specific config. If that file doesn't exist, auto-detect the linter and run it. Auto-fix what you can, report unfixable errors as blocking. If no linter detected, allow stopping."; + timeout = 180; + }]; + } + { + hooks = [{ + type = "agent"; + agent = "test-runner"; + prompt = "First: if stop_hook_active is true in the hook input, allow stopping immediately. Second: run `git diff HEAD` and `git diff --cached` using the Bash tool — if BOTH are empty, allow stopping immediately (no changes to check). Otherwise: Check if .claude/test-runner.md exists in the current working directory. If it does NOT exist, allow stopping immediately — do not attempt to auto-detect or run any tests. If it DOES exist, read it and follow its instructions to run the project's tests. If tests fail, they must be fixed before the session can end."; + timeout = 300; + }]; + } + { + hooks = [{ + type = "agent"; + agent = "devil-advocate"; + prompt = "First: if stop_hook_active is true in the hook input, allow stopping immediately. Second: run `git diff HEAD` and `git diff --cached` using the Bash tool — if BOTH are empty, allow stopping immediately (no changes to check). Otherwise: Review all code changes. Read the project's .claude/devil-advocate.md for project-specific conventions. Report any CRITICAL or HIGH issues found. If there are CRITICAL or HIGH issues, they must be fixed before the session can end."; + timeout = 600; + }]; + } + ]; +}