agent-skills-nix
Declarative management of Agent Skills (directories containing SKILL.md) with flake-pinned sources, discovery, selection, bundling, and Home Manager integration.
Concepts
- sources: Named inputs (flake or path) pointing at a skills root (
subdir). OptionalidPrefixnamespaces discovered skill IDs to avoid collisions across sources. - discover: Recursively scans sources for directories that contain
SKILL.md, producing a catalog. Skills can be nested (e.g.ecosystem/c-ecosystem/) and their IDs use/as separator. - skills.enable / skills.enableAll / skills.explicit: Declaratively pick discovered skills, enable-all (global or by source list), and explicitly specified ones; no accidental auto-install unless you opt in.
- targets: Agent-specific destinations synced from a store bundle (structure:
link,symlink-tree,copy-tree). Targets are opt-in (enable = falseby default). Thedestoption supports shell variable expansion at runtime (e.g.${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills). See Default target paths below.
Source filters
Each source supports optional filters to control discovery:
idPrefix(null | string, default:null): Prefix prepended to discovered skill IDs. Useful when multiple sources expose the same relative path, e.g.idPrefix = "openai";turnspdfintoopenai/pdf.filter.maxDepth(null | int, default:null): Maximum recursion depth for SKILL.md discovery.null= unlimited (capped internally at 100 to guard against symlink loops),1= immediate children only,2= one level of nesting. Set to1to restore pre-recursive (flat-only) behavior.filter.nameRegex(null | string, default:null): Regex matched against the skill's relative path (e.g.cat-a/skill-1) to restrict discovery.
If two sources both expose pdf, prefix them explicitly to keep IDs unique:
sources.openai = {
input = "openai-skills";
subdir = "skills";
idPrefix = "openai";
};
sources.anthropic = {
input = "anthropic-skills";
subdir = "skills";
idPrefix = "anthropic";
};
skills.enable = [ "openai/pdf" "anthropic/pdf" ];
Default target paths
| Target | Global path | Local path |
|---|---|---|
| agents | $HOME/.agents/skills |
.agents/skills |
| codex | ${CODEX_HOME:-$HOME/.codex}/skills |
.codex/skills |
| opencode | $HOME/.config/opencode/skills |
.opencode/skills |
| claude | ${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills |
.claude/skills |
| copilot | $HOME/.copilot/skills |
.github/skills |
| cursor | $HOME/.cursor/skills |
.cursor/skills |
| windsurf | $HOME/.codeium/windsurf/skills |
.windsurf/skills |
| antigravity | $HOME/.gemini/antigravity/skills |
.agent/skills |
| gemini | $HOME/.gemini/skills |
.gemini/skills |
Quick start (child flake + Home Manager)
Put skills config in a small child flake so the only pinned inputs there are skill sources.
Use the quickstart example:
- Overview:
examples/quickstart/README.md - Main (tightly coupled):
examples/quickstart/main/flake.nix - Child (separated catalog):
examples/quickstart/child/flake.nix
Notes:
- In
main,agent-skillsand skill sources are listed directly in the top-level inputs. - In
child, top-level only depends onskills-catalog = path:./skills; skills inputs live under./skills/flake.nix. - If you use source
inputreferences in your module config, pass flakeinputsto Home Manager viaextraSpecialArgs. - To enable a default target, set
targets.<name>.enable = true;(e.g.targets.claude.enable = true;). structure = "link"useshome.filesymlinks;symlink-treeandcopy-treerun inhome.activation.symlink-treeusesrsync -a --delete(preserve symlinks);copy-treeusesrsync -aL --delete(dereference symlinks).destsupports shell variable expansion at runtime (e.g.${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills). Note:linkstructure does not support shell variables and will use the fallback path.- Symlinks inside skills are preserved when their target stays inside the source root; escaping or dangling ones are dropped. See Symlinks inside skills.
Flake outputs
packages.<system>.agent-skills-bundle: Store bundle of selected skills (empty by default; configure in consumers).apps.<system>.skills-install: Sync bundle to enabled global targets (see Default target paths). Override destinations withAGENT_SKILLS_DESTS.apps.<system>.skills-install-local: Sync bundle to enabled local targets (see Default target paths) usingcopy-tree. Override root withAGENT_SKILLS_ROOT, destinations withAGENT_SKILLS_LOCAL_DESTS.apps.<system>.skills-list: JSON view of the default catalog.checks.<system>.skills: Sanity check that the bundle builds.homeManagerModules.default: Home Manager module implementing the DSL above.lib.agent-skills: Helper functions (discoverCatalog,selectSkills,mkBundle,mkLocalInstallScript,mkShellHook,catalogJson,defaultConfig).
Library functions
See examples/library-functions/snippet.nix.
discoverCatalog recursively discovers SKILL.md directories and generates /-separated IDs for nested skills (e.g. cat-a/skill-1). Set idPrefix on a source to namespace discovered IDs (for example, openai/pdf). It enforces SKILL.md presence and rejects duplicate IDs after prefixing (error messages include absolute paths for both conflicting sources). selectSkills errors on unknown allowlist entries or missing files, preventing accidental drift. (Home Manager maps skills.enable → allowlist and skills.explicit → skills.)
Skill customisation
Explicit skills support transform and packages options to customise SKILL.md and bundle dependencies:
See examples/skill-customization/explicit-transform.nix.
This generates:
my-skill/
├── SKILL.md
├── jq -> /nix/store/xxx-jq/bin/jq
└── curl/ -> /nix/store/xxx-curl/bin/ (for packages with multiple binaries)
With SKILL.md containing the transformed content.
Transform function arguments:
original: The original SKILL.md contentdependencies: A markdown table of package dependencies with local paths (e.g.,./jq)
Default behaviour (no transform):
- If only
packagesis specified, the default isdependencies + original - If neither is specified, the original SKILL.md is used as-is
Package binaries are referenced with local paths (./jq or ./pkg/ for multi-binary packages) to reduce context consumption when agents load the skill.
Apps usage
Global skills (Home Manager)
- List catalog:
nix run .#skills-list - Sync bundle to
$HOME:nix run .#skills-install(override destinations viaAGENT_SKILLS_DESTS="~/tmp/skills1 ~/tmp/skills2")
Local skills (project-local)
- Sync bundle to current directory:
nix run .#skills-install-local
Local skills are installed to enabled local targets in Default target paths relative to the current working directory (or AGENT_SKILLS_ROOT if set). Override destinations via AGENT_SKILLS_LOCAL_DESTS.
Targets respect enable, systems, and structure (default copy-tree). To exclude a target, disable it or provide custom targets to mkLocalInstallScript.
Local install skips non-Nix-managed existing paths to avoid clobbering user data; set AGENT_SKILLS_FORCE=1 to overwrite.
Both apps operate on the flake's default (empty) config; point at your own flake/module for real catalogs.
Local skills in your project
To install skills locally in your project, use mkLocalInstallScript in your flake:
See examples/local-install/flake.nix.
Then run nix run .#skills-install-local from your project root to install skills to enabled local targets in Default target paths.
Auto-install with devShell
Use mkShellHook to automatically install skills when entering a dev shell:
See examples/devshell/flake.nix.
Now nix develop will automatically install skills to your project directory.
Symlinks inside skills
Symlinks inside skill directories are kept when their textual target stays inside the declared source root (e.g. ../shared to a sibling at the root). Symlinks whose target escapes the root are dropped, along with any links left dangling by that drop.
- The entire source root is imported into the store, not just each skill subdirectory. Scope the source via
path = ./skillsorsubdirif the root contains unrelated heavy trees (.git, build artefacts). --safe-linkschecks the textual target, not the resolved path. Keep symlinks source-root-relative.- Sources resolving to the same physical directory share one store path and one safe-tree derivation.
Checks / safety
- Disallows skill IDs containing
/..or leading/. - Disallows source
idPrefixvalues ending with/. - Verifies
SKILL.mdfor discovered and explicit skills. - Fails on duplicate IDs across sources.
- Preserves symlinks that stay inside a declared source root and drops escaping or dangling symlinks when materializing bundles.
- Rejects
..traversal in sourcesubdirand explicit skillpathvalues. - Caps recursion at 100 levels when maxDepth is null to guard against symlink loops.
- Activation scripts always
mkdir -pand usersync -a --deleteby default.
Breaking changes
filter.maxDepth default changed from 1 to null
Skill discovery now recurses into nested directories by default. If your source layout relies on flat-only discovery (one level of directories under the source root), add filter.maxDepth = 1; to your source configuration:
sources.my-skills = {
path = ./skills;
filter.maxDepth = 1; # restore flat-only behavior
};