| title | Skills |
|---|---|
| description | The unified extension model: commands, subagents, tools, and event subscriptions in one ergonomic primitive |
| sidebar_order | 1 |
A skill is the unit of extension in Nanocoder — the umbrella that unifies commands, subagents, and tools, plus the event subscriptions that fire them. If you're adding any kind of custom behaviour to Nanocoder, this is the page to start with; the individual command / subagent / tool pages drill into the per-member details.
Skills come in two ergonomic forms - a single .md file in
.nanocoder/commands|agents|tools/ (the single-file form) or a
directory under .nanocoder/skills/ containing a skill.yaml and any
subset of commands/, agents/, tools/ subdirs (the bundle form).
Both forms produce the same runtime data model, register into the same
registries, and surface through the same /skills slash command.
Most extensions are a single piece: a command, a subagent, or a tool.
For those, the single-file form keeps the cost at exactly one file -
exactly how .nanocoder/commands/, .nanocoder/agents/, and
.nanocoder/tools/ already work today.
A bundle skill is for multi-piece features. A "PR reviewer" skill wants
its subagent (the reviewer), a tool (gh_pr_diff), and a command
(/review) to ship and version together. The bundle form gives that a
home with one manifest and one shareable artifact.
Drop one .md into the right flat dir. Frontmatter declares the
member; the file basename is the skill name (a namespaced command such
as commands/refactor/dry.md is the skill refactor:dry). A command,
an agent and a tool may share a name - they show up as separate
entries in /skills.
<!-- .nanocoder/agents/docs-agent.md -->
---
name: docs-agent
description: Watch docs and refresh outputs when source changes.
subscribe:
- kind: file.changed
paths: ["docs/**"]
eventKinds: [add, change]
---
You watch the docs directory for changes...The subscribe: block is new. When the per-project daemon is running,
it wakes this subagent whenever a file under docs/** changes.
A directory under .nanocoder/skills/<name>/ with skill.yaml:
.nanocoder/skills/k8s/
skill.yaml
commands/k8s.md
agents/k8s-agent.md
tools/
k8s_pods.md
k8s_logs.md
# skill.yaml
name: k8s # required, kebab-case: ^[a-z][a-z0-9-]*$
description: Kubernetes operational helpers. # required
version: 0.2.0 # optional
author: you@example.com # optional
tags: [kubernetes, ops] # optional, informational (not used for matching)
subscribe:
- kind: file.changed
target: agent:k8s-agent
paths: ["k8s/**/*.yaml"]
tools_visibility:
default: scopedtools_visibility: scoped (or global) is shorthand for the
default: mapping above.
Inside a bundle, members are aware of each other:
- A bundle's subagent automatically gets its sibling tools in its
effective tool list, even when its frontmatter has a
tools:allowlist. You do not listk8s_podsandk8s_logsinagents/k8s-agent.md'stools:field - they are siblings. - Scoped tools (
tools_visibility.default: scoped, the default for bundles) are hidden from the global tool list - only the bundle's own subagent sees them. - Set
tools_visibility.default: globalto expose tools alongside built-ins, MCP tools, and the existing.nanocoder/tools/*.mdflat-form tools.
commands/— any number. Each file auto-namespaces under the bundle name, socommands/status.mdin bundlek8sinvokes as/k8s:status. Shortcut:commands/<bundleName>.md(e.g.commands/k8s.md) keeps the bare bundle name (/k8s).agents/— exactly one (or zero). The agent is the bundle's brain; if you need a second one, that's a second skill. Extra.mdfiles inagents/are ignored and reported as a load error.tools/— any number. Tools are named by their frontmattername:(snake_case), independent of the bundle name.
A multi-verb bundle is natural:
.nanocoder/skills/git/
skill.yaml # name: git
commands/
status.md # invokes as /git:status
commit.md # invokes as /git:commit
push.md # invokes as /git:push
agents/git-agent.md # one shared agent
tools/
git_log.md
git_diff.md
Subscriptions can declare on member frontmatter (target is implicit
self) or on the bundle manifest (target is explicit kind:name):
# bundle manifest, multiple targets in one place
subscribe:
- kind: file.changed
target: agent:docs-agent
paths: ["docs/**"]
- kind: schedule.cron
target: command:weekly-report
cron: "0 9 * * MON"Inside a bundle, member frontmatter must not set target: itself - a
member's subscription always targets that member. Put explicit targets
in skill.yaml.
Target kinds:
agent:- the daemon runs the subagent with a prompt describing the event and its payload.command:- the daemon renders the command (with no arguments, so parameter defaults apply) and runs it as an unattended agent run, with the event payload appended. In a bundle manifest, name the member as it appears incommands/(command:status, notcommand:k8s:status).tool:- rejected. A triggered tool call has nothing to fill in its arguments, so the subscription reports an error at load time. Point the subscription at an agent or command that calls the tool instead.skill:- parsed for forward compatibility, but registering it today raises a clear "not supported yet" error.
Event kinds:
file.changed- filters:paths(glob list; omit it to match every file) andeventKinds(any ofadd,change,unlink; omit it to match all three). Globs support*,**,?and{a,b}; negation (!) is not supported. Changes under.git/,node_modules/and.nanocoder/are never reported.schedule.cron- filter:cron, a standard 5-field expression (or 6-field with seconds). An invalid expression is rejected when the file loads instead of reaching the daemon.ci.job.failed- filter:branches, glob-matched against the failing run's branch; omitted matches every branch. Only ever fires when the daemon's CI watch is active - see CI Watch Configuration.
A malformed subscribe: block on a single-file command, agent or tool
is logged and dropped: the member still loads, just without triggers.
In skill.yaml it fails the bundle's load, with the error shown by
/skills and in the daemon log.
Default: triggered runs execute in headless mode (autonomous, no
foreground prompts). Set confirm: true on a subscription to make the
run execute in plan mode instead - the subagent proposes changes
without applying them.
subscribe:
- kind: file.changed
target: agent:docs-agent
paths: ["docs/**"]
confirm: trueEvents only fire while a process is running to host them. The interactive TUI does NOT start file watchers or cron tickers — that's the daemon's job.
nanocoder daemon start # spawn the per-project daemon detached
nanocoder daemon stop # SIGTERM the daemon, wait for lockfile cleanup
nanocoder daemon status # report running / not running, PID, uptime
nanocoder daemon logs # tail .nanocoder/daemon.log (last 64KB)
nanocoder daemon install # install per-user auto-start
nanocoder daemon uninstall # remove the auto-start unit
daemon start refuses to start in a directory you haven't trusted, because triggered runs execute tools without confirmation. Trust the directory by running nanocoder interactively there once, pass --trust-directory to bypass the check for that one start (not saved), or set NANOCODER_TRUST_DIRECTORY=1 to trust it and save that for future runs.
daemon install is not gated: it only writes the auto-start unit, and the
gate applies when the daemon actually boots. Installing in an untrusted
directory therefore succeeds and says so — install prints a warning naming
both remedies — and every autostart boot after it is refused until you
trust the directory. That refusal comes from daemon start itself, before
the daemon child that owns daemon.log is spawned, so it goes to the
daemon start command's own output — under an autostart service, wherever
the service manager sends it — not to daemon.log.
The daemon writes a JSON lockfile at .nanocoder/daemon.json (PID,
socket path, start time) and an append-only log at
.nanocoder/daemon.log. Stale lockfiles (PID no longer alive) are
reaped automatically on the next daemon start or daemon status.
Auto-start ships for macOS (LaunchAgent under
~/Library/LaunchAgents/), Linux (systemd user unit under
~/.config/systemd/user/), and Windows (Task Scheduler task
registered via schtasks /Create /XML with an ONLOGON trigger; the XML
manifest is stored under %LOCALAPPDATA%\nanocoder\tasks\). All three
are namespaced by a short hash of the project path so multiple projects
each get their own daemon.
The daemon's IPC surface uses an AF_UNIX socket at
.nanocoder/daemon.sock on macOS/Linux and a named pipe at
\\.\pipe\nanocoder-daemon-<hash> on Windows. Unix socket paths are
capped by sockaddr_un.sun_path (104 bytes on macOS, 108 on Linux), and
libuv silently truncates anything longer, so for deeply nested projects
the socket moves to <tmpdir>/nanocoder-daemon-<hash>.sock instead. The
bound path is recorded in the lockfile, so clients always read it back
rather than recomputing it. nanocoder daemon stop
prefers an IPC shutdown request (clean drain of the event loop) and
falls back to SIGTERM only if the daemon is unreachable, so stops are
graceful on Windows too where SIGTERM would otherwise be force-kill.
Internally, the daemon runs every triggered agent or command in
headless mode (no foreground prompts, no ask_user, no agent,
and only tools that never need approval). The
confirm: true opt-in below switches a specific subscription to plan
mode instead.
Triggered runs are subagent runs, so the
maxRepeatedToolCalls cap
applies: a triggered skill whose model gets stuck repeating the same
tool call stops with an error instead of burning tokens unattended (see
Loop Protection).
/skills list every loaded skill
/skills show k8s details for one skill (members, subscriptions, source)
/skills create k8s scaffold a new bundle at .nanocoder/skills/k8s/
/skills check k8s validate a bundle without loading it
/skills promote k8s copy a project skill up to the global level
/skills demote k8s copy a global skill down into this project
/skills create only scaffolds bundles. For single-file skills, use
the existing creators:
/commands create my-cmd scaffold .nanocoder/commands/my-cmd.md
/agents create my-agent scaffold .nanocoder/agents/my-agent.md
/tools create my-tool scaffold .nanocoder/tools/my-tool.md
Each of those drops a stub file in the right flat dir and chains into an AI-assisted design conversation so the model can help fill in the frontmatter and body.
A bundle is one shareable artifact, so it can be installed straight from a git repository:
nanocoder skills add owner/repo owner/repo shorthand
nanocoder skills add https://example.com/skills.git any git URL
nanocoder skills add ./local-checkout a local directory, git repo or not
nanocoder skills add pr-reviewer resolve the name through the index
Flags:
--ref <ref> branch, tag, or commit to clone
--subdir <path> where the bundle sits inside the repo
--global install into the config dir instead of this project
--force replace an existing skill of the same name
--yes skip the trust prompt (for scripts and CI)
--index <url> use a different skills.json for name resolution
Without --subdir, the manifest is looked for at the repo root, then at
skills/<name>/, then across a one-directory-per-bundle layout. A repo with
several bundles and no --subdir lists the candidates rather than guessing.
Installing a skill means running its code: a bundle tool is a shell script,
approval: never skips confirmation entirely, and a subscribe: block makes
the daemon fire it unattended. Nothing is written into the project until you
have seen what that means, so the prompt names every tool with its approval
policy and every subscription with its trigger:
Skill "pr-reviewer" v0.1.0 by nano-collective
Reviews pull requests and posts findings.
from: https://github.com/Nano-Collective/nanocoder-skills
into: /repo/.nanocoder/skills/pr-reviewer
Commands: /pr-reviewer:review
Agent: reviewer
Tools (visible only to this skill's agent):
- gh_pr_diff · shell script · approval: never (runs WITHOUT asking) · read-only
Event subscriptions (the daemon fires these unattended):
- file.changed → agent:reviewer · paths src/**
Installing a skill means running its code. Only install skills you trust.
Install this skill? [y/N]
Before you ever see that prompt the bundle has been shallow-cloned into a
temp dir, had its .git stripped, been rejected if it contains a symlink (a
bundle is markdown and YAML; a symlink is only ever an escape attempt), and
been validated by the same linter /skills check runs. Declining leaves the
project untouched. Accepting lands the bundle through the same copy the
promote / demote commands use, so it refuses to overwrite an existing
skill unless you pass --force.
Bare names resolve through a plain skills.json file - a list, hosted in a
git repo, not a registry service:
{
"skills": [
{
"name": "pr-reviewer",
"description": "Reviews pull requests and posts findings.",
"repo": "https://github.com/Nano-Collective/nanocoder-skills",
"subdir": "skills/pr-reviewer"
}
]
}Point NANOCODER_SKILLS_INDEX (or --index) at another URL, or at a local
file, to use a different list - a team can check one into its own repo. If
the index promises one name and the cloned manifest declares another, the
install is refused.
The default index above isn't published yet, so a bare name won't resolve
until it is; install by owner/repo, a git URL, or a local path in the
meantime, or point NANOCODER_SKILLS_INDEX at your own list.
Skills load from three levels, highest priority first:
- project -
.nanocoder/skills/(and the flatcommands|agents|tools/dirs) in the working directory. Scoped to this repo, committed with it. - personal / global - the same layout under your platform config dir
(
~/.config/nanocoder/on Linux,~/Library/Preferences/nanocoder/on macOS, or$NANOCODER_CONFIG_DIR). Available in every repo on the machine. - built-in - shipped with nanocoder. Today that is only the
exploresubagent; no built-in bundles ship yet.
A project skill shadows a personal one of the same name, which shadows a built-in. To reuse a skill you wrote in one repo everywhere, move it up a level; to vendor a global or built-in skill into a repo so teammates get it, move it down:
/skills promote pr-review copy project -> global, keep both
/skills promote pr-review --move move project -> global, remove the original
/skills demote pr-review copy global -> project, keep both
/skills demote pr-review --move move global -> project, remove the original
Notes:
- Both default to copy, leaving the original in place. After a copy you
have the skill at both levels and the higher-priority one wins on the next
load. Pass
--moveto remove the source instead. - The copy refuses to overwrite an existing skill at the destination. Re-run
with
--forceto replace it. promoteonly works on a project skill;demoteworks on a global or built-in skill (demoting a built-in vendors a copy you can then edit).- Both write to disk only. Restart nanocoder for the new copy to load.
The legacy scheduler (the ScheduleRunner that read
.nanocoder/schedules.json) has been removed. Cron-driven runs now
happen exclusively through skill subscriptions executed by the daemon.
Move each entry into the targeted command's frontmatter, or into a bundle
manifest. When the cron fires, the daemon renders the command and runs it
as an unattended agent run in headless mode:
Before:
[{"cron": "0 9 * * MON", "command": "/weekly-report"}]After (frontmatter form):
<!-- .nanocoder/commands/weekly-report.md -->
---
description: Monday morning summary.
subscribe:
- kind: schedule.cron
cron: "0 9 * * MON"
---
Summarize last week's commits...After (manifest form, inside a bundle):
subscribe:
- kind: schedule.cron
target: command:weekly-report
cron: "0 9 * * MON"If .nanocoder/schedules.json is present when Nanocoder boots, you see
a loud warning pointing at this migration guide. The file itself is no
longer read - only its presence triggers the warning.
After migrating, run nanocoder daemon start (or nanocoder daemon install for auto-start across reboots) to make the cron subscriptions
actually fire.