From 6aff25434309881ad49c16f0228ef7952bd18cdd Mon Sep 17 00:00:00 2001 From: t-monaghan Date: Tue, 25 Nov 2025 13:45:10 +1100 Subject: [PATCH] claude-code: added 'skills' option to specify skills from filesystem --- modules/programs/claude-code.nix | 70 ++++++++++++++++++- .../programs/claude-code/assertion.nix | 7 ++ .../modules/programs/claude-code/default.nix | 3 + .../programs/claude-code/mixed-content.nix | 13 ++++ .../claude-code/skill-subdir/convert.md | 8 +++ .../claude-code/skill-subdir/extract.md | 8 +++ .../programs/claude-code/skills-dir.nix | 14 ++++ .../programs/claude-code/skills-path.nix | 14 ++++ .../programs/claude-code/skills-subdir.nix | 21 ++++++ .../programs/claude-code/skills/test-skill.md | 6 ++ .../programs/claude-code/test-skill.md | 6 ++ 11 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 tests/modules/programs/claude-code/skill-subdir/convert.md create mode 100644 tests/modules/programs/claude-code/skill-subdir/extract.md create mode 100644 tests/modules/programs/claude-code/skills-dir.nix create mode 100644 tests/modules/programs/claude-code/skills-path.nix create mode 100644 tests/modules/programs/claude-code/skills-subdir.nix create mode 100644 tests/modules/programs/claude-code/skills/test-skill.md create mode 100644 tests/modules/programs/claude-code/test-skill.md diff --git a/modules/programs/claude-code.nix b/modules/programs/claude-code.nix index 0beaf7a2d..f5b16ac74 100644 --- a/modules/programs/claude-code.nix +++ b/modules/programs/claude-code.nix @@ -227,6 +227,53 @@ in example = lib.literalExpression "./hooks"; }; + skills = lib.mkOption { + type = lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path); + default = { }; + description = '' + Custom skills for Claude Code. + The attribute name becomes the skill filename or directory name, and the value is either: + - Inline content as a string (creates .claude/skills/.md) + - A path to a file (creates .claude/skills/.md) + - A path to a directory (creates .claude/skills// with all files) + ''; + example = lib.literalExpression '' + { + xlsx = ./skills/xlsx.md; + data-analysis = ./skills/data-analysis; + pdf-processing = ''' + --- + name: pdf-processing + description: Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction. + --- + + # PDF Processing + + ## Quick start + + Use pdfplumber to extract text from PDFs: + + ```python + import pdfplumber + + with pdfplumber.open("document.pdf") as pdf: + text = pdf.pages[0].extract_text() + ``` + '''; + } + ''; + }; + + skillsDir = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + Path to a directory containing skill files for Claude Code. + Skill files from this directory will be symlinked to .claude/skills/. + ''; + example = lib.literalExpression "./skills"; + }; + mcpServers = lib.mkOption { type = lib.types.attrsOf jsonFormat.type; default = { }; @@ -290,6 +337,10 @@ in assertion = !(cfg.hooks != { } && cfg.hooksDir != null); message = "Cannot specify both `programs.claude-code.hooks` and `programs.claude-code.hooksDir`"; } + { + assertion = !(cfg.skills != { } && cfg.skillsDir != null); + message = "Cannot specify both `programs.claude-code.skills` and `programs.claude-code.skillsDir`"; + } ]; programs.claude-code.finalPackage = @@ -349,6 +400,11 @@ in source = cfg.hooksDir; recursive = true; }; + + ".claude/skills" = lib.mkIf (cfg.skillsDir != null) { + source = cfg.skillsDir; + recursive = true; + }; } // lib.mapAttrs' ( name: content: @@ -367,7 +423,19 @@ in lib.nameValuePair ".claude/hooks/${name}" { text = content; } - ) cfg.hooks; + ) cfg.hooks + // lib.mapAttrs' ( + name: content: + if lib.isPath content && lib.pathIsDirectory content then + lib.nameValuePair ".claude/skills/${name}" { + source = content; + recursive = true; + } + else + lib.nameValuePair ".claude/skills/${name}.md" ( + if lib.isPath content then { source = content; } else { text = content; } + ) + ) cfg.skills; }; }; } diff --git a/tests/modules/programs/claude-code/assertion.nix b/tests/modules/programs/claude-code/assertion.nix index 73f936904..77c002f3a 100644 --- a/tests/modules/programs/claude-code/assertion.nix +++ b/tests/modules/programs/claude-code/assertion.nix @@ -38,6 +38,12 @@ test-hook = "test content"; }; hooksDir = ./hooks; + + # assert fail: cannot set skills and skillsDir at the same time. + skills = { + test-skill = "test content"; + }; + skillsDir = ./skills; }; test.asserts.assertions.expected = [ @@ -46,5 +52,6 @@ "Cannot specify both `programs.claude-code.agents` and `programs.claude-code.agentsDir`" "Cannot specify both `programs.claude-code.commands` and `programs.claude-code.commandsDir`" "Cannot specify both `programs.claude-code.hooks` and `programs.claude-code.hooksDir`" + "Cannot specify both `programs.claude-code.skills` and `programs.claude-code.skillsDir`" ]; } diff --git a/tests/modules/programs/claude-code/default.nix b/tests/modules/programs/claude-code/default.nix index 2e17a6be1..7ff4a9977 100644 --- a/tests/modules/programs/claude-code/default.nix +++ b/tests/modules/programs/claude-code/default.nix @@ -8,7 +8,10 @@ claude-code-agents-dir = ./agents-dir.nix; claude-code-commands-dir = ./commands-dir.nix; claude-code-hooks-dir = ./hooks-dir.nix; + claude-code-skills-dir = ./skills-dir.nix; + claude-code-skills-subdir = ./skills-subdir.nix; claude-code-agents-path = ./agents-path.nix; claude-code-commands-path = ./commands-path.nix; + claude-code-skills-path = ./skills-path.nix; claude-code-mixed-content = ./mixed-content.nix; } diff --git a/tests/modules/programs/claude-code/mixed-content.nix b/tests/modules/programs/claude-code/mixed-content.nix index b02dfd99e..74864200c 100644 --- a/tests/modules/programs/claude-code/mixed-content.nix +++ b/tests/modules/programs/claude-code/mixed-content.nix @@ -22,6 +22,15 @@ ''; path-agent = ./test-agent.md; }; + skills = { + inline-skill = '' + Use this skill when the user asks you to perform an inline operation. + + ## Instructions + This skill is defined inline. + ''; + path-skill = ./test-skill.md; + }; }; nmt.script = '' @@ -29,10 +38,14 @@ assertFileExists home-files/.claude/commands/path-command.md assertFileExists home-files/.claude/agents/inline-agent.md assertFileExists home-files/.claude/agents/path-agent.md + assertFileExists home-files/.claude/skills/inline-skill.md + assertFileExists home-files/.claude/skills/path-skill.md assertFileContent home-files/.claude/commands/path-command.md \ ${./test-command.md} assertFileContent home-files/.claude/agents/path-agent.md \ ${./test-agent.md} + assertFileContent home-files/.claude/skills/path-skill.md \ + ${./test-skill.md} ''; } diff --git a/tests/modules/programs/claude-code/skill-subdir/convert.md b/tests/modules/programs/claude-code/skill-subdir/convert.md new file mode 100644 index 000000000..d7e2e845b --- /dev/null +++ b/tests/modules/programs/claude-code/skill-subdir/convert.md @@ -0,0 +1,8 @@ +--- +name: convert +description: Convert between file formats +--- + +# Convert Skill + +Convert files between different formats. diff --git a/tests/modules/programs/claude-code/skill-subdir/extract.md b/tests/modules/programs/claude-code/skill-subdir/extract.md new file mode 100644 index 000000000..2bb1a050e --- /dev/null +++ b/tests/modules/programs/claude-code/skill-subdir/extract.md @@ -0,0 +1,8 @@ +--- +name: extract +description: Extract data from files +--- + +# Extract Skill + +Extract data from various file formats. diff --git a/tests/modules/programs/claude-code/skills-dir.nix b/tests/modules/programs/claude-code/skills-dir.nix new file mode 100644 index 000000000..4f15a5580 --- /dev/null +++ b/tests/modules/programs/claude-code/skills-dir.nix @@ -0,0 +1,14 @@ +{ + programs.claude-code = { + enable = true; + skillsDir = ./skills; + }; + + nmt.script = '' + assertFileExists home-files/.claude/skills/test-skill.md + assertLinkExists home-files/.claude/skills/test-skill.md + assertFileContent \ + home-files/.claude/skills/test-skill.md \ + ${./skills/test-skill.md} + ''; +} diff --git a/tests/modules/programs/claude-code/skills-path.nix b/tests/modules/programs/claude-code/skills-path.nix new file mode 100644 index 000000000..374e97a0b --- /dev/null +++ b/tests/modules/programs/claude-code/skills-path.nix @@ -0,0 +1,14 @@ +{ + programs.claude-code = { + enable = true; + skills = { + test-skill = ./test-skill.md; + }; + }; + + nmt.script = '' + assertFileExists home-files/.claude/skills/test-skill.md + assertFileContent home-files/.claude/skills/test-skill.md \ + ${./test-skill.md} + ''; +} diff --git a/tests/modules/programs/claude-code/skills-subdir.nix b/tests/modules/programs/claude-code/skills-subdir.nix new file mode 100644 index 000000000..3c7567fd9 --- /dev/null +++ b/tests/modules/programs/claude-code/skills-subdir.nix @@ -0,0 +1,21 @@ +{ + programs.claude-code = { + enable = true; + skills = { + data-processing = ./skill-subdir; + }; + }; + + nmt.script = '' + assertFileExists home-files/.claude/skills/data-processing/extract.md + assertFileExists home-files/.claude/skills/data-processing/convert.md + assertLinkExists home-files/.claude/skills/data-processing/extract.md + assertLinkExists home-files/.claude/skills/data-processing/convert.md + assertFileContent \ + home-files/.claude/skills/data-processing/extract.md \ + ${./skill-subdir/extract.md} + assertFileContent \ + home-files/.claude/skills/data-processing/convert.md \ + ${./skill-subdir/convert.md} + ''; +} diff --git a/tests/modules/programs/claude-code/skills/test-skill.md b/tests/modules/programs/claude-code/skills/test-skill.md new file mode 100644 index 000000000..b56ac9df5 --- /dev/null +++ b/tests/modules/programs/claude-code/skills/test-skill.md @@ -0,0 +1,6 @@ +Use this skill when the user asks you to perform a test operation. + +## Instructions +1. Parse user input +2. Execute test operation +3. Return results diff --git a/tests/modules/programs/claude-code/test-skill.md b/tests/modules/programs/claude-code/test-skill.md new file mode 100644 index 000000000..b56ac9df5 --- /dev/null +++ b/tests/modules/programs/claude-code/test-skill.md @@ -0,0 +1,6 @@ +Use this skill when the user asks you to perform a test operation. + +## Instructions +1. Parse user input +2. Execute test operation +3. Return results