Authoring an overlay
An overlay is just a directory with the files you want installed into a scope, plus an
optional overlay.toml manifest. Anyone can write one; name its parent directory (a
directory of overlays), a registry, or a git repo with --repo — see the
overlays guide. Keep the source outside <store>/overlays/:
overlays remove deletes the installed directory outright.
Minimal overlay
my-overlay/
overlay.toml
kb/MY_TOPIC.md
# my-overlay/overlay.toml
name = "my-overlay"
description = "My team's conventions for topic X."
routing = [
"- Working on topic X → `$MY_OVERLAY_OVERLAY_ROOT/kb/MY_TOPIC.md`",
]
Install it:
dotagents overlays add my-overlay --repo /path/to/parent -g
The overlay installs as a directory, ~/.agents/overlays/my-overlay/ here, and each
routing line is appended to the core's "Load on demand" table so agents know when to
read the file. Refer to your own files through the overlay's root variable,
$MY_OVERLAY_OVERLAY_ROOT (the name upper-cased, - → _, plus _OVERLAY_ROOT):
dotagents env exports it and dotagents context expands it, so the line resolves
wherever the store lives. A bare kb/MY_TOPIC.md would name a file that is not there.
Contributing rules
To add always-on rules (not just routing), point rules at overlay-relative
markdown files; their - **… bullet blocks are appended to the core's "Always-on
rules" section:
rules = ["rules/my-rules.md"]
Only the leading run of bullets is taken, up to the file's next ## heading, so the
file can explain itself below that. Keep the core small — rules are always loaded, so
they cost context every session. Put detail behind routing instead.
Priority
When several overlays contribute to the same merged region, priority decides order
(lower sorts earlier; the unprioritized default is 500). Set it only when order
matters.
Requirements
requires = ["engineering"] makes overlays add install the named overlays first
(unless the scope already has them), and overlays remove refuse to remove one while
yours needs it.
Libraries and launchers
Put importable Python in lib/. Every Python a session starts sees every overlay's
lib/ on PYTHONPATH (dotagents env puts them there, highest precedence first,
and lists them in $AGENTS_PYTHONPATH), so a skill script or another overlay can
import your module. So can your setup script and your command modules, which
dotagents starts with the libs in place whatever shell it runs from. A lib/ module
comes before site-packages and the standard library: do not reuse a package name you
do not mean to replace.
Commands on PATH: bin/*.py and [scripts]
A Python command needs no launcher of your own. Every top-level bin/<name>.py
gets one named <name>: <name>.exe on Windows (distlib's ScriptMaker, the
launcher pip gives console scripts) and a /bin/sh script elsewhere, made in the
agents' venv, in <venv>/overlays/, whenever overlays add / sync / remove or
init brings the venv up to date. dotagents env puts that directory first on
PATH for the venv $AGENTS_PYTHON names, so bin/curl.py answers to curl
ahead of the system's, and drops any other venv's.
The launcher runs your script under the venv's interpreter, as python
bin/<name>.py would, found when it runs through $<NAME>_OVERLAY_ROOT: editing
the script or moving the store needs nothing regenerated, only a new interpreter
does. Unset, or with the script missing, it exits 2 and says so. A file whose name
starts with _ is not a command. Name other scripts, rename one, or opt one out
in overlay.toml:
[scripts]
tool = "lib/tool/main.py" # a launcher `tool` for another path
helper = false # none for bin/helper.py
Two overlays with one script name is an error naming both. A launcher no overlay has any more is deleted at the next sync.
Never ship a .cmd or .bat that takes arguments. cmd.exe parses its command
line before the first line runs, so an argument's &, |, > or %VAR% acts:
from PowerShell, curl "http://h/?a=1|hostname" would run hostname. The .exe
above hands the arguments over as passed.
Your script may run outside a dotagents env session too, so set PYTHONPATH
for what it starts, if anything needs the libs: its own lib/, then
$AGENTS_PYTHONPATH, then the caller's, leaving empty parts out (an empty entry
means the current directory). The net overlay's bin/curl.py does this.
Setup scripts
Ship an idempotent setup.py at the overlay root to run install-time wiring
automatically — the recommended, OS-agnostic form (it runs under the same Python as
dotagents, so it works on every platform). See the contract in
Overlays → Setup scripts. The essentials:
- idempotent, check-then-act;
- cwd is your installed overlay dir;
- the environment is the scope's assembled
dotagents env(every overlay'slib/onPYTHONPATH,bin/onPATH), plus the user store (AGENTS_HOME), the store you are installed into (AGENTS_SCOPE_ROOT, withAGENTS_SCOPE=user/project) and your own installed dir (AGENTS_OVERLAY_DIR) — never hardcode a home path; - it has 300 seconds unless
setup_timeoutinoverlay.toml(or the user's--setup-timeout) says otherwise;0means no limit; - a non-zero exit fails the install loudly; confirm any irreversible action yourself.
Custom commands
A command module is a *.py file defining a duho command class with a
__call__ entry point, and each one becomes a
dotagents <name> subcommand, discovered at run time with no registration. An overlay
ships its modules in its own cmds/ dir. For your own, create dotagents/cmds/ in a
store: ~/.agents/dotagents/cmds/ for every session, <project>/.agents/dotagents/cmds/
for one project. init does not create that directory; you create it when you add your
first module.
# ~/.agents/dotagents/cmds/hello.py
from dotagents.cli import DotAgentsArgs
class Hello(DotAgentsArgs):
"""Say hello, and name the store this command would act on."""
_parsername_ = "hello"
who: str = "world"
"Who to greet."
("--who",)
def __call__(self) -> int:
scope = self.resolve_scope() # honours -g / --agents-dir, like every command
print("hello, %s (store: %s)" % (self.who, scope.agents_root))
return 0
Then dotagents hello --who you. DotAgentsArgs brings the -g / --agents-dir pair
and resolve_scope(), so a command that works on a store never redeclares them; a
command that needs neither can subclass duho's LoggingArgs and Cmd directly. A
field's help is the string after it and its flags the tuple after that. Files whose
name starts with _ are skipped — use that prefix for shared helper modules. Only
module-level command classes become commands: nest the subcommands of an umbrella
command inside it, or they register as top-level commands too.
Sources layer so that a later one overrides a same-named command: the built-ins, then
the bundled findings and launch, then store by store — the system store (see
below), the user store, the project's .agents/ — each store's overlays' cmds/
before the store's own dotagents/cmds/, and last $AGENTS_CMDS_PATH entries
(os.pathsep-split) and --cmdspath entries. The system store is /etc/agents on
POSIX or $AGENTS_SYSTEM_ROOT (the only way to have one on Windows), and it counts
only when it exists and only administrators can write it. So your user-store command overrides one shipped by an
overlay installed in the user (or system) store, and a project's overlays and
.agents/dotagents/cmds/ override yours. A module that fails to
import (a syntax error, an exception at import time) is skipped with a warning naming
it; it never takes the other commands down with it, and neither does a command whose
parser cannot be built. The bundled findings and launch are ordinary command
modules discovered from the package, and the private-sync overlay ships
link-project / sync-project from its own cmds/ the same way.
Prefer a command module to a loose script. A script under tools/ is something
an agent has to find, quote and spawn. A command module is discovered, shows up in
dotagents --help, and is served as an MCP tool with a typed schema when dotagents
runs with DOTAGENTS_MCP=stdio (see
Commands → As tools for an agent). Write it to be
a good tool:
- the logic lives in the command class —
__call__and its methods — not in one function that__call__forwards every field to; - each field's help says what the value is, since it becomes the schema's description;
- the output is short: one line for a change, the smallest useful part for a read,
--jsonfor a program; - it never prompts, and a command that deletes or publishes needs an explicit flag.
Skills
Put skills/<skill-name>/ directories in your overlay to publish shared skills into
the scope on install. See Overlays → Skills.
A skill that relies on commands says so, in its own text: which dotagents commands
it uses, that they are available as MCP tools (dotagents.<command>) when the
server is registered, and the command-line form to fall back to when it is not. An
agent reading the skill should not have to discover either.