
Write HTML. Render video. Built for agents.
This repo turns Agent Skills into a declarative Nix workflow. It discovers `SKILL.md` directories, lets you opt in to specific skills, bundles them, and installs them into agent-specific skill folders or exports them as a skills-only plugin. It also includes source registries, Home Manager integration, local project installs, and a small set of safety checks for duplicate IDs, invalid paths, and managed destination ownership.
Builders who want one Nix-managed place to define which agent skills get installed where.
You can keep agent skills pinned, selected, and synced without hand-copying folders between projects and tools.
Recursively scans source roots for `SKILL.md` directories and builds a catalog with `/`-separated IDs.
Lets you enable all skills, pick explicit skills, or allowlist selected entries without accidental auto-install.
Loads per-source Nix manifests and a generated JSON lock so multiple skill repos can be pinned together.
Syncs bundles into destination folders for Claude, Codex, Cursor, Copilot, Windsurf, Gemini, and other targets.
Builds a portable plugin directory with selected skills mapped to flat export names.
Provides a module so skill installs can be wired into a user environment or dev shell.
registry/ ├── sources/ │ ├── anthropic.nix │ └── internal.nix └── sources.lock.json
nix run .#skills-sources-lock
my-skill/ ├── SKILL.md ├── jq -> /nix/store/xxx-jq/bin/jq └── curl/ -> /nix/store/xxx-curl/bin/ (for packages with multiple binaries)
Declarative management of Agent Skills (directories containing SKILL.md) with flake-pinned sources, discovery, selection, bundling, Home Manager integration, and skills-only Agent Plugin export.
subdir). They can be written directly as before, or generated from the optional source registry. Optional idPrefix namespaces discovered skill IDs to avoid collisions across sources.SKILL.md, producing a catalog. Skills can be nested (e.g. ecosystem/c-ecosystem/) and their IDs use / as separator.link, symlink-tree, copy-tree). Targets are opt-in (enable = false by default). Runtime destinations support $HOME, ~, and ${VAR:-$HOME/...} fallback forms without general shell evaluation. See Default target paths below.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"; turns pdf into openai/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 to 1 to 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" ];
The source registry removes the need to declare every skill repository as a
flake input. It uses npins, the pin manager
recommended by the nix.dev remote-source guide,
for revision resolution and hashing. Existing sources.<name>.input and
sources.<name>.path declarations remain supported and can be mixed with this
approach.
Keep one human-edited Nix manifest per source and commit the generated JSON lock:
registry/
├── sources/
│ ├── anthropic.nix
│ └── internal.nix
└── sources.lock.json
For a GitHub source that follows a branch:
# registry/sources/anthropic.nix
{
pin = {
type = "github";
owner = "anthropics";
repo = "skills";
branch = "main";
};
subdir = "skills";
idPrefix = "anthropic";
filter.maxDepth = 2;
}
pin.type can be github, git, or tarball. Git and GitHub pins accept
branch, at, submodules, preReleases, versionUpperBound, and
releasePrefix; generic Git pins additionally require url and accept
forge. If branch is omitted, npins follows releases and writes a
GitRelease pin. Tarball pins require url and optionally accept mutable.
The remaining fields (subdir, idPrefix, and filter) are the same source
discovery settings as the existing DSL.
Load the lock as an ordinary path-backed sources value:
sources = agentLib.sourcesFromLock {
manifestsDir = ./registry/sources;
lockFile = ./registry/sources.lock.json;
};
catalog = agentLib.discoverCatalog sources;
Expose the updater in a consumer flake:
sourceLockProgram = agentLib.mkSourceLockProgram { inherit pkgs; };
apps.${system}.skills-sources-lock = {
type = "app";
program = "${sourceLockProgram}/bin/skills-sources-lock";
};
Then update all sources together:
nix run .#skills-sources-lock
The command validates and normalizes every manifest through the same Nix
loader used by consumers, passes every npins argument without shell evaluation,
and resolves all pins in a temporary sibling directory. The generated lock has
an agent-skills schema-v1 wrapper containing the normalized manifests and an
npins version-8 lock. sourcesFromLock requires the current normalized
manifests to match that snapshot exactly, so changing a repository, branch, or
discovery option without updating the lock fails evaluation. The updater sorts
JSON keys and only then atomically renames the completed lock into place. A
failed manifest or fetch leaves the old lock byte-for-byte unchanged; rerunning
with unchanged upstream revisions produces the same lock bytes. See the complete
examples/source-registry flake.
| 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 | .agents/skills |
| gemini | $HOME/.gemini/skills | .gemini/skills |
| pi | $HOME/.pi/agent/skills | .pi/skills |
Put skills config in a small child flake so the only pinned inputs there are skill sources.
Use the quickstart example:
examples/quickstart/README.mdexamples/quickstart/main/flake.nixexamples/quickstart/child/flake.nixNotes:
main, agent-skills and skill sources are listed directly in the top-level inputs.child, top-level only depends on skills-catalog = path:./skills; skills inputs live under ./skills/flake.nix.input references in your module config, pass flake inputs to Home Manager via extraSpecialArgs.targets.<name>.enable = true; (e.g. targets.claude.enable = true;).structure = "link" uses home.file symlinks; symlink-tree and copy-tree run in home.activation.symlink-tree uses rsync -a --delete (preserve symlinks); copy-tree uses rsync -aL --delete (dereference symlinks).dest values support $HOME, ~, and ${VAR:-$HOME/...} fallback forms (e.g. ${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills). Home Manager's link structure requires a static path and uses the fallback path.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 with AGENT_SKILLS_DESTS.apps.<system>.skills-install-local: Sync bundle to enabled local targets (see Default target paths) using copy-tree. Override root with AGENT_SKILLS_ROOT, destinations with AGENT_SKILLS_LOCAL_DESTS.apps.<system>.skills-list: JSON view of the default catalog.apps.<system>.skills-sources-lock: Resolve registry/sources/*.nix and atomically update registry/sources.lock.json.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, mkAgentPlugin, loadSourceManifests, sourcesFromLock, mkSourceLockProgram, mkSyncProgram, mkLocalInstallProgram, compatibility wrappers mkSyncScript / mkLocalInstallScript, mkShellHook, catalogJson, defaultConfig).The root flake.nix only declares inputs and invokes Blueprint. Blueprint discovers packages, checks, and the public library under nix/; nix/flake-outputs.nix projects those pieces onto the existing public output names and wires the apps and Home Manager module. Runtime and domain logic remain in scripts/, lib/, modules/, and test/.
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.)
loadSourceManifests loads, validates, and normalizes a directory of per-source
Nix manifests. sourcesFromLock verifies them against an agent-skills
schema-v1 JSON lock (containing an npins version-8 pin set) and returns the same
path-backed source shape accepted by discoverCatalog. mkSourceLockProgram
builds the updater used by the skills-sources-lock app.
mkSyncProgram returns a skills-install executable after filtering enabled targets for the requested system. mkLocalInstallProgram is its project-local wrapper and returns skills-install-local. Both serialize a versioned JSON configuration and invoke the shared synchronization runtime; they do not return inline shell source. Existing consumer flakes can continue using mkSyncScript and mkLocalInstallScript; these compatibility wrappers preserve their original return types while delegating to the shared runtime. mkShellHook runs the local program from a development shell.
mkAgentPlugin turns an explicit map of selected skills into a skills-only
plugin for ChatGPT and Codex. It follows OpenAI's current
plugin packaging format:
the manifest is written to .codex-plugin/plugin.json, while the exported
skills live directly under skills/.
plugin = agentLib.mkAgentPlugin {
inherit pkgs;
manifest = {
name = "document-tools";
version = "0.1.0";
description = "Portable document workflows";
};
skills = {
pdf = selection."openai/pdf";
};
};
The attribute names in skills become the exported skill directory names.
This explicit mapping flattens catalog IDs such as openai/pdf into portable
names such as pdf; the matching SKILL.md frontmatter must use that same
name. The exporter adds "skills": "./skills/" to the manifest.
The initial exporter deliberately supports skills only. A selected skill with
packages or a transform is rejected because those bundle features can
create output that is not portable outside the Nix store. Each accepted skill
is copied with its payload into the result, internal links are materialized,
and the completed plugin contains no symlinks. See the runnable
examples/agent-plugin flake.
The exporter owns the manifest's skills field and rejects apps,
mcpServers, and hooks. Plugin-level asset fields such as composerIcon,
logo, logoDark, and screenshots are also outside this initial API; keep
visual assets inside individual skills instead. If present, defaultPrompt
supports at most three entries of 128 characters each. Plugin-bundled skills
must follow the Agent Skills naming and description rules, and cannot set
disable-model-invocation to true.
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):
packages is specified, the default is dependencies + originalPackage binaries are referenced with local paths (./jq or ./pkg/ for multi-binary packages) to reduce context consumption when agents load the skill.
nix run .#skills-list$HOME: nix run .#skills-install (override destinations via AGENT_SKILLS_DESTS="~/tmp/skills1 ~/tmp/skills2")nix run .#skills-install-localLocal 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 mkLocalInstallProgram.
The synchronizer refuses to replace a non-empty, unmarked directory. A successful tree sync records ownership in .agent-skills-managed.json; set AGENT_SKILLS_FORCE=1 only when you intentionally want agent-skills to take over an existing destination.
Both apps operate on the flake's default (empty) config; point at your own flake/module for real catalogs.
To install skills locally in your project, use mkLocalInstallProgram 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.
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.
The hook prints one line per installed target. Under direnv the hook is
re-evaluated on every shell entry, so pass quiet = true; to mkShellHook to
drop those lines; warnings and failures still go to stderr. AGENT_SKILLS_QUIET=1
does the same for any synchronization program at runtime.
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.
path = ./skills or subdir if the root contains unrelated heavy trees (.git, build artefacts).--safe-links checks the textual target, not the resolved path. Keep symlinks source-root-relative./.. or leading /.idPrefix values ending with /.SKILL.md for discovered and explicit skills... traversal in source subdir and explicit skill path values.rsync --delete; managed destinations carry a .agent-skills-managed.json marker.AGENT_SKILLS_ROOT (or the current directory) and paths that escape that root are rejected.Tree synchronization now requires a valid .agent-skills-managed.json marker before replacing a non-empty destination. For an existing destination created by an older release, inspect it first and set AGENT_SKILLS_FORCE=1 for the one-time takeover.
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
};
Sign in to join the discussion.
No comments yet. Be the first to say what this is good for.

Write HTML. Render video. Built for agents.
Ultra-lightweight, open-source, self-hosted personal AI agent framework in Python with WebUI, tools, memory, MCP, multi-agent workflows, automation, and chat apps
SkillOpt is a text-space optimizer that trains reusable natural-language skills for frozen LLM agents through trajectory-driven edits, validation-gated updates, and deployable best_skill.md artifacts.

Omnigent is an open-source AI agent framework and meta-harness: orchestrate Claude Code, Codex, Cursor, Pi, and custom agents — swap harnesses without rewriting, enforce policies and sandboxing, and collaborate in real time from any device.
A theoretical reconstruction of the Claude Mythos architecture, built from first principles using the available research literature.
🕷️ An adaptive Web Scraping framework that handles everything from a single request to a full-scale crawl!