1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 agentsFile = "${config.home.homeDirectory}/config/users/clover/agents/AGENTS.md";
9 simpleBrowserBridge = "${config.home.homeDirectory}/config/users/clover/packages/open-in-vscode-browser/extension";
10 claudeStatusline = "${config.home.homeDirectory}/config/users/clover/agents/statusline.sh";
11 claudeAgents = "${config.home.homeDirectory}/config/users/clover/agents/subagents";
12 codexAgents = "${config.home.homeDirectory}/config/users/clover/agents/codex-agents";
13 cloverSkills = "${config.home.homeDirectory}/config/users/clover/agents/skills";
14 codexUpsert = "${config.home.homeDirectory}/config/users/clover/scripts/codex-config-upsert.py";
15 codexConfig = "${config.home.homeDirectory}/config/users/clover/codex-config.toml";
16 codexPython = pkgs.python3.withPackages (ps: [ ps.tomlkit ]);
17in
18{
19 imports = [
20 ./zsh
21 ./jujutsu.nix
22 ];
23
24 home = {
25 stateVersion = "23.05"; # Please read the comment before changing.
26 packages = with pkgs; [
27 ffmpeg-full
28 ripgrep
29 uv
30 nh
31 autofmt
32 gnupg
33 just
34 tmux
35 pnpm
36 nodejs_26
37 (callPackage ./packages/agent-linear { })
38 # `open` shim that routes URLs into the VS Code Simple Browser; see
39 # the home.file symlink for the companion extension.
40 (callPackage ./packages/open-in-vscode-browser { })
41 ];
42 # nix node's default global prefix is its read-only store path.
43 sessionVariables.NPM_CONFIG_PREFIX = "$HOME/.local";
44 sessionPath = [ "$HOME/.local/share/pnpm/bin" ];
45 };
46
47 xdg.configFile."opencode/AGENTS.md".source = config.lib.file.mkOutOfStoreSymlink agentsFile;
48
49 # claude code's global instructions live at ~/.claude/CLAUDE.md (the analog
50 # of opencode's global AGENTS.md); point it at the same file.
51 home.file.".claude/CLAUDE.md".source = config.lib.file.mkOutOfStoreSymlink agentsFile;
52
53 # Subagent definitions the global CLAUDE.md delegation section routes to;
54 # the built-in agents are denied in ~/.claude/settings.json so these are the
55 # only ones reachable.
56 home.file.".claude/agents".source = config.lib.file.mkOutOfStoreSymlink claudeAgents;
57
58 # codex reads its global instructions from $CODEX_HOME/AGENTS.md.
59 home.file.".codex/AGENTS.md".source = config.lib.file.mkOutOfStoreSymlink agentsFile;
60
61 home.file.".codex/agents".source = config.lib.file.mkOutOfStoreSymlink codexAgents;
62
63 # One SKILL.md in the shared format; both harnesses read it from their own
64 # personal skills directory, linked per-skill to leave unmanaged ones alone.
65 home.file.".claude/skills/ui-copy".source =
66 config.lib.file.mkOutOfStoreSymlink "${cloverSkills}/ui-copy";
67 home.file.".codex/skills/ui-copy".source =
68 config.lib.file.mkOutOfStoreSymlink "${cloverSkills}/ui-copy";
69 home.file.".claude/skills/ui-prototype".source =
70 config.lib.file.mkOutOfStoreSymlink "${cloverSkills}/ui-prototype";
71 home.file.".codex/skills/ui-prototype".source =
72 config.lib.file.mkOutOfStoreSymlink "${cloverSkills}/ui-prototype";
73 home.file.".claude/skills/ux-flows".source =
74 config.lib.file.mkOutOfStoreSymlink "${cloverSkills}/ux-flows";
75 home.file.".codex/skills/ux-flows".source =
76 config.lib.file.mkOutOfStoreSymlink "${cloverSkills}/ux-flows";
77 home.file.".claude/skills/ui-craft".source =
78 config.lib.file.mkOutOfStoreSymlink "${cloverSkills}/ui-craft";
79 home.file.".codex/skills/ui-craft".source =
80 config.lib.file.mkOutOfStoreSymlink "${cloverSkills}/ui-craft";
81 home.file.".claude/skills/ux-testing".source =
82 config.lib.file.mkOutOfStoreSymlink "${cloverSkills}/ux-testing";
83 home.file.".codex/skills/ux-testing".source =
84 config.lib.file.mkOutOfStoreSymlink "${cloverSkills}/ux-testing";
85 home.file.".claude/skills/ui-review-loop".source =
86 config.lib.file.mkOutOfStoreSymlink "${cloverSkills}/ui-review-loop";
87 home.file.".codex/skills/ui-review-loop".source =
88 config.lib.file.mkOutOfStoreSymlink "${cloverSkills}/ui-review-loop";
89 home.file.".claude/skills/maintain-it".source =
90 config.lib.file.mkOutOfStoreSymlink "${cloverSkills}/maintain-it";
91 home.file.".codex/skills/maintain-it".source =
92 config.lib.file.mkOutOfStoreSymlink "${cloverSkills}/maintain-it";
93 home.file.".claude/skills/accessibility".source =
94 config.lib.file.mkOutOfStoreSymlink "${cloverSkills}/accessibility";
95 home.file.".codex/skills/accessibility".source =
96 config.lib.file.mkOutOfStoreSymlink "${cloverSkills}/accessibility";
97 home.file.".codex/skills/shale-issues".source =
98 config.lib.file.mkOutOfStoreSymlink "${cloverSkills}/shale-issues";
99 home.file.".claude/skills/openai-docs".source =
100 config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/.codex/skills/.system/openai-docs";
101 home.file.".codex/skills/update-config".source =
102 config.lib.file.mkOutOfStoreSymlink "${cloverSkills}/update-config";
103
104 # codex rewrites ~/.codex/config.toml at runtime (project trust, plugins,
105 # skills), so the managed keys are upserted into it instead of symlinked.
106 home.activation.codexConfig = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
107 if [ -d "${config.home.homeDirectory}/.codex" ]; then
108 run ${codexPython}/bin/python3 ${codexUpsert} ${codexConfig} \
109 "${config.home.homeDirectory}/.codex/config.toml"
110 fi
111 '';
112
113 # Referenced by the `statusLine.command` entry in ~/.claude/settings.json,
114 # which claude code rewrites itself and so stays unmanaged.
115 home.file.".claude/statusline-command.sh".source =
116 config.lib.file.mkOutOfStoreSymlink claudeStatusline;
117
118 home.file.".tmux.conf".text = ''
119 set -g focus-events on
120 '';
121
122 # Companion extension for the `open-in-vscode-browser` shim. Symlinked
123 # (rather than copied) so edits to the source are live after a window reload.
124 home.file.".vscode/extensions/clover.simple-browser-bridge".source =
125 config.lib.file.mkOutOfStoreSymlink simpleBrowserBridge;
126
127 programs = {
128 # sort-lines:start
129 # bat.enable = true;
130 btop.enable = true;
131 fd.enable = true;
132 hyfetch.enable = true;
133 # sort-lines:end
134
135 ghostty = {
136 enable = true;
137 shader = "cursor-smear-black.glsl";
138 package = null;
139 settings = {
140 theme = "light:Farmhouse Light,dark:Challenger Deep";
141 font-family = "AT Name Mono";
142 adjust-cursor-thickness = 2;
143 background-opacity = 0.95;
144 background-blur = true;
145 unfocused-split-opacity = 1;
146 };
147 };
148
149 git = {
150 ignores = [ "AGENTS.md" ];
151 # use a git-specific email
152 settings.user.email = "git@paperclover.net";
153 settings.gpg.ssh.allowedSignersFile = "${pkgs.writeText "allowed_signers" ''
154 git@paperclover.net ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAVa1aq1LEj/VXHJYlR5HJpK65lS/9jOL/dAB9ZS1chd
155 ''}";
156 # signing
157 signing = {
158 signByDefault = true;
159 key = "~/.ssh/id_ed25519";
160 format = "ssh";
161 };
162 };
163
164 ssh = {
165 enable = true;
166 enableDefaultConfig = false;
167 extraConfig = ''
168 SetEnv TERM=xterm-256color
169 '';
170 # extraConfig asserts a default host block exists
171 settings."*" = {
172 AddKeysToAgent = "yes";
173 } // lib.optionalAttrs pkgs.stdenv.isDarwin {
174 UseKeychain = true;
175 };
176 };
177 };
178}