dotagents keeps your agent skills and global instructions in a single source tree, renders them for the machine you are currently on, and symlinks the result into whichever directories your tools read.
It exists so one repository can serve several computers without the machines having to be identical. A branch prefix that differs between machines, a skill only one of them needs, a path that exists on one and not the other: each of those is a declared difference in the source tree rather than a divergent copy of a file.
- Works as a template for your personal agents repository. Use this template to centralize your skills and instructions accross multiple machines.
- Renders per machine. Variables written as
{{MY_VARIABLE}}are replaced at build time from a per machine JSON file layered over shared defaults. - Layers content per machine. A machine can add skills the others do not have, replace a shared skill entirely, and append its own paragraphs to the global instructions.
- Installs anywhere. The directories it links into are declared in
targets.json. Any tool that reads skills from a directory is supported by adding a line. - Fails loudly. A variable with no value stops the build and names the file and line, so a typo can never silently delete an instruction.
./install.sh src/ -> dist/ -> every path in targets.json when the set of skills changes
./build.sh src/ -> dist/ when their content changes
src/ is what you edit. The build resolves the active machine, merges that
machine's variables over the shared defaults, renders the tree, and writes
dist/. It writes nothing outside the repository, so it is always safe to run.
install.sh builds and then symlinks dist/ into the declared targets, so a
fresh clone needs nothing else.
Each target holds one symlink per skill, not a single link to the skills
directory. Editing a skill therefore needs only build.sh, because its link
already exists and resolves into dist/. Adding or removing one needs
install.sh again, to create or prune that skill's link. You do not have to
remember which case you are in: build.sh warns when a skill it just built has
nothing pointing at it.
Needs bash and Node.js 18 or newer. Nothing to install beyond that.
git clone git@github.com:souyahia/dotagents.git
cd dotagents
echo "my-laptop" > .machine # this computer's identity, gitignored
mkdir -p src/machines/my-laptop # custom configuration for this machine
./install.sh --dry-run # builds, then shows what it would link
./install.shThen put your skills in src/skills/<name>/SKILL.md and run ./install.sh, so
each new skill gets its symlink. Once a skill exists, ./build.sh picks up your
edits to it.
targets.json where dist/ gets symlinked
src/
instructions.md shared global instructions
vars.default.json declares every variable and its default
skills/<name>/SKILL.md shared skills
machines/<id>/
vars.json this machine's variable overrides
instructions.md appended to the shared instructions
skills/<name>/SKILL.md machine only skills, override shared by name
vars.local.json gitignored, for secrets and one-offs
targets.local.json gitignored, replaces targets.json on this machine
.machine gitignored, this computer's identity
dist/ gitignored, generated by build.sh
Everything about one machine lives in one directory, so two computers never edit the same lines and never conflict.
The active machine is resolved in this order:
- the
DOTAGENTS_MACHINEenvironment variable - the
.machinefile - the short hostname
If 1 or 2 names a machine with no directory under src/machines/, the build
fails, because you asked for something that does not exist. If the hostname
simply matches nothing, the build succeeds with shared content only, so a fresh
clone works before you have configured anything.
Write {{UPPER_SNAKE}} anywhere in a .md or .txt file. Values are resolved
by merging three layers, each overriding the one above it:
src/vars.default.jsonsrc/machines/<id>/vars.jsonvars.local.json
All values are strings. Keep them small: a path, a prefix, a sentence. If a machine difference needs a whole paragraph of new behavior, make it a machine only skill instead, so the agent reads one coherent document rather than a document arguing with itself.
Two rules keep this honest:
- A variable with no value is a build error. It is never silently replaced with an empty string, so a typo stops the build instead of quietly dropping an instruction your agent was meant to follow.
- A variable set in a machine or local layer that nothing references is a
warning. That catches the other half of the same typo, where the template
says
INTRUCTIONSand the JSON saysINSTRUCTIONS.
To write a literal {{NAME}} without substituting it, put a ! immediately
after the opening braces. The build strips the ! and leaves the rest alone.
Shared skills live in src/skills/. A skill in src/machines/<id>/skills/ with
the same name replaces the shared one entirely on that machine; with a new name,
it exists only on that machine.
Adding or deleting a skill changes the set of symlinks each target needs, so run
./install.sh rather than ./build.sh after either. It creates the links that
are missing and prunes the ones whose skill is gone.
targets.json declares where the build gets linked:
{
"skills": [
"~/.agents/skills",
"~/.claude/skills"
],
"instructions": [
"~/.claude/CLAUDE.md"
]
}A skills target is a directory that receives one symlink per built skill. An
instructions target is a single symlink to the rendered instructions file, so
the path includes the filename the tool expects. ~ and $VARIABLES are
expanded.
Everything listed is installed, and missing directories are created. When a
machine needs a different set, an uncommitted targets.local.json replaces
targets.json entirely.
The defaults cover Claude Code and Codex. Adding another tool means adding the directory it reads.
install.sh never overwrites anything it did not create. Entries belonging to
something else are reported and skipped, and --force backs them up first.
There is no dependency free, cross platform way to run a command whenever
anything in a directory tree changes. macOS launchd WatchPaths does not
recurse into subdirectories, Linux inotify is not recursive either, and the tools
that paper over this (fswatch, watchexec, watchman) are binaries every user
would have to install before this one worked at all. Polling on a timer would
work, but it is a background process running forever to serve edits you make a
few times a month.
So you run ./build.sh when you change a skill's content, and ./install.sh
when you add or remove one. Agents that watch their skills directory pick the
result up on their own. Claude Code "picks up the change within the current
session, without a restart", and Codex "detects skill changes automatically. If
an update doesn't appear, restart Codex."
One caveat no tool can work around: a skill already invoked in the current session keeps the copy loaded in context, so editing it and invoking it again in that same session may not reflect the change. This behaves identically for real files and symlinks, and is not something dotagents causes or can fix.
To keep the machinery public and your actual skills private, put them in a
separate directory and point DOTAGENTS_HOME at it:
export DOTAGENTS_HOME=~/Projects/my-private-agents
./install.shDOTAGENTS_HOME is where src/, dist/, targets.json, .machine and
vars.local.json live. The scripts stay in this repository, so you can pull
updates to the tool without touching your content.
| Command | What it does |
|---|---|
./build.sh |
renders src/ into dist/, writing nothing outside DOTAGENTS_HOME |
./build.sh --no-link-check |
renders without warning about built skills that are linked nowhere |
./install.sh |
builds, then symlinks dist/ into every applicable target |
./install.sh --dry-run |
builds, then prints what it would link without touching anything outside the repository |
./install.sh --force |
replaces entries dotagents did not create, backing up real files |
./install.sh --no-build |
installs the existing dist/ instead of rebuilding it first |
bash and Node.js 18 or newer on your PATH. There is nothing to install
beyond that: the scripts use only the Node standard library, so there is no
package.json, no dependencies and no npm install step.