From 7126ec6a559661be933662c848b4fb7c6dedacec Mon Sep 17 00:00:00 2001 From: "Pat L." Date: Thu, 26 Mar 2026 23:59:48 -0400 Subject: [PATCH] claude-code: Add support for installing plugins (#8934) Allow for configuring marketplaces or individual plugins. Both are provided as a list of either paths or packages. The lone plugins are enabled by adding a --plugin-dir argument to the wrapper script. Marketplaces are saved to the nix store and enabled by adding to the claude settings and known_marketplaces files. With the marketplace "installed", the plugin can just be enabled via the enabledPlugins setting. --- modules/programs/claude-code.nix | 88 +++++++++++++++++-- .../programs/claude-code/assertion.nix | 2 +- .../modules/programs/claude-code/default.nix | 2 + .../expected-known-marketplaces.json | 18 ++++ .../claude-code/expected-plugin-wrapper | 2 + .../expected-settings-marketplaces.json | 17 ++++ .../programs/claude-code/marketplaces.nix | 29 ++++++ .../modules/programs/claude-code/plugins.nix | 28 ++++++ .../.claude-plugin/marketplace.json | 8 ++ .../test-tool/.claude-plugin/plugin.json | 4 + .../test-plugin/.claude-plugin/plugin.json | 4 + 11 files changed, 196 insertions(+), 6 deletions(-) create mode 100644 tests/modules/programs/claude-code/expected-known-marketplaces.json create mode 100644 tests/modules/programs/claude-code/expected-plugin-wrapper create mode 100644 tests/modules/programs/claude-code/expected-settings-marketplaces.json create mode 100644 tests/modules/programs/claude-code/marketplaces.nix create mode 100644 tests/modules/programs/claude-code/plugins.nix create mode 100644 tests/modules/programs/claude-code/test-marketplace/.claude-plugin/marketplace.json create mode 100644 tests/modules/programs/claude-code/test-marketplace/plugins/test-tool/.claude-plugin/plugin.json create mode 100644 tests/modules/programs/claude-code/test-plugin/.claude-plugin/plugin.json diff --git a/modules/programs/claude-code.nix b/modules/programs/claude-code.nix index a05f6e6f..583ddb43 100644 --- a/modules/programs/claude-code.nix +++ b/modules/programs/claude-code.nix @@ -50,6 +50,7 @@ let default = null; inherit description example; }; + in { meta.maintainers = [ lib.maintainers.khaneliman ]; @@ -136,6 +137,51 @@ in description = "JSON configuration for Claude Code settings.json"; }; + plugins = lib.mkOption { + type = with lib.types; listOf (either package path); + default = [ ]; + description = '' + List of plugins to use when running Claude Code. + Each entry is either: + - A path to the plugin directory + - The plugin package, whether a nix package or the output of a fetcher + Plugins are enabled via a `--plugin-dir` argument in the wrapper script. + ''; + example = literalExpression '' + [ + ./my-local-plugin + fetchFromGithub { + owner = "some-github-org"; + repo = "claude-plugin"; + rev = "779a68ebc2a75e4a184d2c87e5a43a758e6458a1"; + sha256 = "228fdd7e5908ea1d2f65218ecd9c71e1eefa0834d200d55fbb8bf8b5563acec0"; + } + ] + ''; + }; + + marketplaces = lib.mkOption { + type = with lib.types; attrsOf (either package path); + default = { }; + description = '' + Custom marketplaces for Claude Code plugins. + The attribute name becomes the marketplace name, and the value is either: + - A path to the marketplace directory + - The marketplace package, whether a nix package or the output of a fetcher + ''; + example = literalExpression '' + { + local-marketplace = ./my-local-marketplace; + gh-marketplace = fetchFromGithub { + owner = "some-github-org"; + repo = "claude-marketplace"; + rev = "8a873a220b8427b25b03ce1a821593a24e098c34"; + sha256 = "5c2dce95122b5bb73fa547edabbb6c3061c2d193d11e51faecd4d22659e67279"; + }; + } + ''; + }; + agents = mkContentOption { description = '' Custom agents for Claude Code. @@ -476,6 +522,22 @@ in } else nameValuePair ".claude/skills/${name}/SKILL.md" (mkSourceEntry content); + + mkMarketplaceEntry = name: content: { + source = { + source = "directory"; + path = content; + }; + }; + + mkInstalledMarketplaceEntry = + name: content: + (mkMarketplaceEntry name content) + // { + installLocation = content; + lastUpdated = "1970-01-01T00:00:00Z"; + }; + in lib.mkIf cfg.enable { assertions = @@ -496,9 +558,9 @@ in [ { assertion = - (cfg.mcpServers == { } && cfg.lspServers == { } && !cfg.enableMcpIntegration) + (cfg.mcpServers == { } && cfg.lspServers == { } && !cfg.enableMcpIntegration && cfg.plugins == [ ]) || cfg.package != null; - message = "`programs.claude-code.package` cannot be null when `mcpServers`, `lspServers`, or `enableMcpIntegration` is configured"; + message = "`programs.claude-code.package` cannot be null when `mcpServers`, `lspServers`, `enableMcpIntegration`, or `plugins` is configured"; } { assertion = !(cfg.memory.text != null && cfg.memory.source != null); @@ -531,8 +593,15 @@ in map (pluginFile: "install -Dm644 ${pluginFile.path} $out/${pluginFile.name}") pluginFiles ) ); + allPluginPaths = (if pluginFiles != [ ] then [ pluginDir ] else [ ]) ++ cfg.plugins; + wrapperArgs = lib.flatten ( + map (p: [ + "--plugin-dir" + "${p}" + ]) allPluginPaths + ); in - if pluginFiles != [ ] then + if allPluginPaths != [ ] then pkgs.symlinkJoin { name = "claude-code"; paths = [ cfg.package ]; @@ -540,7 +609,7 @@ in mv $out/bin/claude $out/bin/.claude-wrapped cat > $out/bin/claude <