Skills
June 25, 2026 · View on GitHub
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.
Why two forms
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.
Single-file form
Drop one .md into the right flat dir. Frontmatter declares the
member; the file basename is the skill name.
<!-- .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.
Bundle form
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
description: Kubernetes operational helpers.
version: 0.2.0
author: you@example.com
subscribe:
- kind: file.changed
target: agent:k8s-agent
paths: ["k8s/**/*.yaml"]
tools_visibility:
default: scoped
Inside a bundle, members are aware of each other:
- A bundle's subagent automatically gets its sibling tools in its
effective tool list. You do not list
k8s_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.
How many of each member kind?
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.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
Event subscriptions
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"
v1 event kinds: file.changed (filter: paths, eventKinds) and
schedule.cron (filter: cron).
confirm: true
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: true
The daemon
Events 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
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. 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 subagent in headless
mode (no foreground prompts, no ask_user, no agent). The
confirm: true opt-in below switches a specific subscription to plan
mode instead.
Inspecting and creating skills
/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.
Sharing skills across repos
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.
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.
Migration from schedules.json
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:
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.