This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

1{ self, ... }: 2{ 3 flake.wrapperModules.claude-code = 4 { 5 config, 6 lib, 7 ... 8 }: 9 let 10 pkgs = config.pkgs; 11 jsonFormat = pkgs.formats.json { }; 12 13 # Assemble a single Claude Code plugin directory containing skills, 14 # commands, and agents. Claude Code discovers a directory as a plugin 15 # when it contains `.claude-plugin/plugin.json`. 16 pluginDir = 17 pkgs.runCommand "claude-plugin-${config.pluginName}" 18 { 19 passthru = { inherit (config) pluginManifest; }; 20 } 21 '' 22 mkdir -p $out/.claude-plugin 23 cp ${jsonFormat.generate "plugin.json" config.pluginManifest} \ 24 $out/.claude-plugin/plugin.json 25 26 ${lib.optionalString (config.skills != { }) '' 27 mkdir -p $out/skills 28 ${lib.concatStringsSep "\n" ( 29 lib.mapAttrsToList (name: src: "ln -s ${src} $out/skills/${name}") config.skills 30 )} 31 ''} 32 33 ${lib.optionalString (config.commands != { }) '' 34 mkdir -p $out/commands 35 ${lib.concatStringsSep "\n" ( 36 lib.mapAttrsToList (name: src: "ln -s ${src} $out/commands/${name}") config.commands 37 )} 38 ''} 39 40 ${lib.optionalString (config.agents != { }) '' 41 mkdir -p $out/agents 42 ${lib.concatStringsSep "\n" ( 43 lib.mapAttrsToList (name: src: "ln -s ${src} $out/agents/${name}") config.agents 44 )} 45 ''} 46 47 ${lib.optionalString (config.pluginExtraFiles != { }) '' 48 ${lib.concatStringsSep "\n" ( 49 lib.mapAttrsToList (path: src: '' 50 mkdir -p "$out/$(dirname ${path})" 51 ln -s ${src} "$out/${path}" 52 '') config.pluginExtraFiles 53 )} 54 ''} 55 ''; 56 57 # Compose a settings.json from the typed options + freeform overrides. 58 # Order matters: typed options take precedence over freeform `settings` 59 # for the keys they own, but freeform can still set anything else. 60 mergedSettings = lib.foldl' lib.recursiveUpdate config.settings ( 61 lib.optional (config.mcpServers != { }) { mcpServers = config.mcpServers; } 62 ++ lib.optional (config.hooks != { }) { hooks = config.hooks; } 63 ++ lib.optional (config.permissions != { }) { permissions = config.permissions; } 64 ); 65 66 settingsFile = jsonFormat.generate "claude-settings.json" mergedSettings; 67 68 levenshtein = self.packages.${pkgs.system}.levenshtein-merge; 69 in 70 { 71 _class = "wrapper"; 72 73 options = { 74 pluginName = lib.mkOption { 75 type = lib.types.str; 76 default = "superconfig"; 77 description = "Name of the bundled plugin directory."; 78 }; 79 80 pluginManifest = lib.mkOption { 81 type = jsonFormat.type; 82 default = { 83 name = config.pluginName; 84 version = "0.0.0"; 85 description = "Nix-managed Claude Code plugin bundle."; 86 }; 87 defaultText = lib.literalExpression '' 88 { name = config.pluginName; version = "0.0.0"; description = "..."; } 89 ''; 90 description = "Contents of `.claude-plugin/plugin.json` for the bundled plugin."; 91 }; 92 93 skills = lib.mkOption { 94 type = lib.types.attrsOf lib.types.path; 95 default = { }; 96 example = lib.literalExpression '' 97 { 98 websearch = ./skills/websearch; 99 } 100 ''; 101 description = '' 102 Skills to bundle. Each entry is a directory containing `SKILL.md` 103 and any helper scripts/resources. Symlinked into 104 `<plugin>/skills/<name>` and discovered by Claude Code automatically. 105 ''; 106 }; 107 108 commands = lib.mkOption { 109 type = lib.types.attrsOf lib.types.path; 110 default = { }; 111 description = '' 112 Slash commands. Each entry is a markdown file (or directory) 113 symlinked into `<plugin>/commands/<name>`. 114 ''; 115 }; 116 117 agents = lib.mkOption { 118 type = lib.types.attrsOf lib.types.path; 119 default = { }; 120 description = '' 121 Sub-agents. Each entry is a markdown file (or directory) symlinked 122 into `<plugin>/agents/<name>`. 123 ''; 124 }; 125 126 pluginExtraFiles = lib.mkOption { 127 type = lib.types.attrsOf lib.types.path; 128 default = { }; 129 example = lib.literalExpression '' 130 { 131 "hooks/format.sh" = ./hooks/format.sh; 132 } 133 ''; 134 description = '' 135 Additional files to drop into the plugin directory. Keys are 136 relative paths inside the plugin directory; values are paths to 137 symlink in. Use this for hook scripts or anything else not covered 138 by the typed options above. 139 ''; 140 }; 141 142 plugins = lib.mkOption { 143 type = lib.types.listOf lib.types.path; 144 default = [ ]; 145 example = lib.literalExpression '' 146 [ 147 # A fetched plugin from GitHub: 148 (pkgs.fetchFromGitHub { 149 owner = "Myr-Aya"; 150 repo = "GouvernAI-claude-code-plugin"; 151 rev = "main"; 152 hash = "sha256-AAAA..."; 153 # The plugin directory is a subdirectory of the repo: 154 } + "/gouvernai") 155 156 # A local plugin directory (must contain .claude-plugin/plugin.json): 157 ./my-local-plugin 158 159 # A plugin built by a Nix derivation: 160 my-plugin-package 161 ] 162 ''; 163 description = '' 164 Additional Claude Code plugin directories to load. Each entry 165 must be a path to a directory containing `.claude-plugin/plugin.json`. 166 Passed as extra `--plugin-dir` flags alongside the bundled plugin. 167 168 Plugins can provide skills, commands, agents, and hooks. Use this 169 to declaratively install third-party plugins (e.g. from GitHub) 170 or locally developed ones without going through the Claude Code 171 marketplace. 172 ''; 173 }; 174 175 mcpServers = lib.mkOption { 176 type = jsonFormat.type; 177 default = { }; 178 example = lib.literalExpression '' 179 { 180 git-mcp = { 181 command = "''${pkgs.git-mcp}/bin/git-mcp"; 182 }; 183 tmux-mcp = { 184 type = "stdio"; 185 command = "''${pkgs.tmux-mcp}/bin/tmux-mcp"; 186 args = [ ]; 187 env = { }; 188 }; 189 } 190 ''; 191 description = '' 192 MCP servers to register. Merged into `settings.json` under 193 `mcpServers`. Each entry follows Claude Code's MCP server schema 194 (stdio: `{ command, args, env }`; http/sse: `{ type, url, headers }`). 195 ''; 196 }; 197 198 hooks = lib.mkOption { 199 type = jsonFormat.type; 200 default = { }; 201 example = lib.literalExpression '' 202 { 203 PreToolUse = [ 204 { 205 matcher = "*"; 206 hooks = [ 207 { 208 type = "command"; 209 command = "''${pkgs.my-permission-bridge}/bin/permission-forward"; 210 } 211 ]; 212 } 213 ]; 214 } 215 ''; 216 description = '' 217 Hook definitions. Shape matches Claude Code's `settings.json` hooks 218 field. Supported events include `PreToolUse`, `PostToolUse`, 219 `UserPromptSubmit`, `Stop`, `SessionStart`, `Notification`, and 220 `PreCompact`. 221 ''; 222 }; 223 224 permissions = lib.mkOption { 225 type = jsonFormat.type; 226 default = { }; 227 example = lib.literalExpression '' 228 { 229 defaultMode = "acceptEdits"; 230 allow = [ 231 "Bash(nix build:*)" 232 "Bash(nix eval:*)" 233 "Bash(nix fmt:*)" 234 "Bash(git status:*)" 235 "Bash(git diff:*)" 236 ]; 237 deny = [ ]; 238 ask = [ ]; 239 } 240 ''; 241 description = '' 242 Permission rules. Shape matches Claude Code's `settings.json` 243 `permissions` field. Use `allow`/`deny`/`ask` pattern lists to 244 short-circuit the permission flow before any hook fires. 245 ''; 246 }; 247 248 settings = lib.mkOption { 249 type = jsonFormat.type; 250 default = { }; 251 description = '' 252 Freeform additional `settings.json` content. Merged with the 253 typed options above (which take precedence on conflict). Use this 254 for anything not covered by a dedicated option. 255 ''; 256 }; 257 258 claudeMd = lib.mkOption { 259 type = lib.types.nullOr lib.types.path; 260 default = null; 261 description = '' 262 Optional `CLAUDE.md` whose lines are fuzzy-merged into 263 `$CLAUDE_MD_DIR/CLAUDE.md` (default `~/.claude/CLAUDE.md`) on 264 every invocation. Only adds lines, never removes user content. 265 Each non-blank managed line is skipped when a line within 266 `claudeMdMergeThreshold` (Levenshtein ratio) already exists in 267 the target file. 268 ''; 269 }; 270 271 projectStateWipeKeys = lib.mkOption { 272 type = lib.types.listOf lib.types.str; 273 default = [ "mcpServers" ]; 274 description = '' 275 Keys to delete from every `projects.<dir>` entry of 276 `~/.claude.json` on each invocation. Use this to purge 277 unmanaged runtime drift that the wrapper otherwise can't 278 override — most notably stale `mcpServers` registrations 279 created by `claude mcp add`, which pin GC-able `/nix/store` 280 paths and shadow the canonical set the wrapper writes via 281 `--settings`. 282 283 Anything not listed here is left alone, so new fields 284 claude-code may add in future versions are preserved by 285 default. Set to `[ ]` to disable the cleanup step. 286 ''; 287 }; 288 289 claudeMdMergeThreshold = lib.mkOption { 290 type = lib.types.float; 291 default = 0.15; 292 description = '' 293 Levenshtein distance ratio (distance / max-length) below which 294 two normalized lines are considered the same during 295 `claudeMd` merging. 0.0 requires exact match; higher values 296 tolerate more typos. Default 0.15 allows roughly one typo per 297 seven characters. 298 ''; 299 }; 300 301 pluginDir = lib.mkOption { 302 type = lib.types.package; 303 readOnly = true; 304 default = pluginDir; 305 defaultText = "<assembled plugin directory>"; 306 description = "The assembled plugin directory passed via `--plugin-dir`."; 307 }; 308 309 settingsFile = lib.mkOption { 310 type = lib.types.package; 311 readOnly = true; 312 default = settingsFile; 313 defaultText = "<assembled settings.json>"; 314 description = "The assembled settings.json passed via `--settings`."; 315 }; 316 }; 317 318 config.package = lib.mkDefault (self.lib.halalify pkgs.claude-code); 319 320 # Always inject our plugin dir + settings file into every invocation. 321 # `--plugin-dir` is repeatable so users can still pass more on the CLI. 322 config.flags."--plugin-dir" = { 323 value = [ "${pluginDir}" ] ++ map toString config.plugins; 324 order = 100; 325 }; 326 config.flags."--settings" = { 327 value = "${settingsFile}"; 328 order = 101; 329 }; 330 331 config.preHook = 332 lib.optionalString (config.claudeMd != null) '' 333 CLAUDE_MD_DIR="''${CLAUDE_MD_DIR:-$HOME/.claude}" 334 mkdir -p "$CLAUDE_MD_DIR" 335 touch "$CLAUDE_MD_DIR/CLAUDE.md" 336 ${levenshtein}/bin/levenshtein-merge merge \ 337 ${config.claudeMd} \ 338 "$CLAUDE_MD_DIR/CLAUDE.md" \ 339 ${toString config.claudeMdMergeThreshold} \ 340 || echo "levenshtein merge failed; leaving CLAUDE.md untouched" >&2 341 '' 342 + lib.optionalString (config.projectStateWipeKeys != [ ]) '' 343 CLAUDE_JSON="$HOME/.claude.json" 344 if [ -f "$CLAUDE_JSON" ]; then 345 CLAUDE_JSON_TMP=$(mktemp "$CLAUDE_JSON.XXXXXX") 346 if ${pkgs.jq}/bin/jq \ 347 --argjson wipe ${lib.escapeShellArg (builtins.toJSON config.projectStateWipeKeys)} ' 348 if .projects then 349 .projects |= with_entries( 350 .value |= reduce $wipe[] as $k (.; del(.[$k])) 351 ) 352 else . end 353 ' "$CLAUDE_JSON" > "$CLAUDE_JSON_TMP"; then 354 mv "$CLAUDE_JSON_TMP" "$CLAUDE_JSON" 355 else 356 rm -f "$CLAUDE_JSON_TMP" 357 echo "claude.json projects-state cleanup failed; leaving file untouched" >&2 358 fi 359 fi 360 ''; 361 362 config.meta.maintainers = [ 363 { 364 name = "lassulus"; 365 github = "lassulus"; 366 githubId = 621375; 367 } 368 ]; 369 config.meta.platforms = lib.platforms.all; 370 }; 371}