Skip to content
Abdul Moiz Shahzad

Writing · Updated

What an agent-skill installer has to get right

I’m Abdul Moiz Shahzad, and I have maintained Ai-Agent-Skills, a skill installer for AI coding agents, since December 2025. This is the list of things the job turned out to require: where the files go, what a copy owes the repository it came from, why a manifest beats a folder, why installed and current are different states, and how to test any of it without a mock lying to you. None of it is specific to my tool. All of it cost me something to learn.

A skill is a folder, and that is where the trouble starts

A skill is a folder with a SKILL.md at the top. The file opens with a few lines of frontmatter, a name and a description, and continues with the instructions an agent should follow when the description matches what it is doing. Next to it you can put scripts the agent may run and reference documents it may read. That is the whole format. Anthropic published it in October 2025, most of the coding agents adopted it within a few months, and the simplicity is the point: a person can write one in an afternoon and a model can read one in a second.

So installing one should be cp -r. For about a week it was. I had three agents on my machine, each reading skills from its own directory, and a growing set of folders I liked. Copying a folder into three places worked until the first time I improved one copy and forgot the other two, which took roughly a day. Then an upstream author fixed a bug in a skill I had copied, and I found out weeks later by watching an agent do the buggy thing. By the third incident I had a spreadsheet of which folder came from where. The spreadsheet was the installer, badly implemented.

Three things break when the format is a folder and the tool is a copy command. Destinations: every runtime reads its own directory, and a skill that is not in the right one does not exist. Drift: a copy has no relationship to the thing it was copied from, so corrections do not reach it. Provenance: six months later nobody can say where a folder came from or why it was kept. An installer is the tool that owns those three problems so the person does not have to, and everything below is one of the three wearing a different hat.

Where the files go

The first design decision is the default destination, and it is a decision about other people’s software. Claude Code reads skills from ~/.claude/skills/ for the user and from a .claude/skills/ folder inside a project. Other agents chose other paths, and for a while there were as many conventions as runtimes. A shared project-level convention has since settled around .agents/skills/, which most of the runtimes now read, and that changed what a sensible default looks like.

My installer’s answer has two halves. The global default is ~/.claude/skills/, because that is the directory the most people using the tool actually have an agent reading. The project default, chosen with -p, is .agents/skills/, because a skill committed alongside code should land where every runtime on the team will find it. The older per-agent targets stay reachable through --agent <name> for 11 runtimes, from Cursor and Codex to Goose and Letta, so nobody who depended on them is stranded.

Two rules came out of getting this wrong first. Never write to a directory the tool was not asked for, even when you are sure the user would want it; an agent that finds an unexpected skill in its folder will use it, and the person will not know why their agent changed its behaviour. And treat the default as a fact about the ecosystem rather than a preference of yours: when the ecosystem moves, the default moves with it, and the escape hatch stays. I changed the defaults once already. I expect to change them again.

What a copy owes its upstream

The second decision is whether the library holds skills or holds pointers to skills, and the honest answer is that it needs both, with a strong default toward pointers. In my library a house copy is a real folder under skills/<name>/ that ships inside the npm package; it installs offline and installs fast. A cataloged entry is a record in skills.jsonwith no folder at all: the repository, the skill name inside it, the shelf it belongs on, and a note saying why. It installs from the source repository when someone asks for it.

The default is the catalog entry, and the reason is drift. A copy goes stale the day its author changes a line, and a library of a hundred copies is a hundred quiet forks with my name on the folder. A pointer stays current by construction. The cost is that a pointer trusts the upstream to still exist and to still be the thing I shelved, which is a real cost, and the reason the copy exists as an option: I vendor a skill when I intend to edit it or when it has to work with no network at all. On the day I wrote this the library held 17 house copies and 123 cataloged entries from 28 source repositories. The ratio is the policy, enforced.

The --why field is the part I would defend hardest. Every command that places a skill on a shelf takes an editorial note, and the note is required. Six months on, the note is the only thing that tells you whether the entry is still earning its place, and a library without notes decays into a list of names that nobody dares delete. Provenance is the repository and the skill name. The note is why you cared. You need both.

A manifest beats a folder

If the library is a directory of folders, the directory is the truth and every other view of it is a copy that can be wrong. The README lists skills that were deleted. The shelf table has the wrong counts. The install command finds a folder the manifest never heard of. I lived with that for a version or two, and the fix was to make skills.json the only source of truth and derive everything else from it.

Derive means generate. The README tables that show shelves, collections, and source repositories sit between generated markers and are rewritten by build-docs from the manifest, so they cannot disagree with it for longer than one command. The terminal browser reads the manifest. The installer resolves names against the manifest. A folder that exists on disk without a manifest entry is an error, which is the inversion that makes the whole thing hold: the file system is downstream of the record, and a record can be validated, diffed, and reviewed in a pull request in a way a pile of folders cannot.

This also decides what a mutation is. add, catalog, vendor, and curate are edits to the manifest with side effects on disk, in that order, and the tool asks for a shelf, a branch, and a note on every one of them because those are manifest fields. Hand editing the JSON works too. The commands exist so that the fields nobody wants to fill in get filled in anyway.

Installed is not the same as current

An installer that only installs has done half the job. A skill on disk has a version, even when nothing in the format says so: it is the state of the upstream folder on the day it was fetched. The upstream keeps moving. So the tool needs a notion of installed versus current, and a command that closes the gap. sync re-fetches an installed skill from wherever the manifest says it lives, and check reports what is installed where without changing anything.

I chose to make refreshing a command you run, with nothing updating itself. The argument for automatic updates is obvious and I still rejected it. A skill is text an agent reads before it acts, and a file that changes underneath an agent between two sessions is a behaviour change nobody reviewed. Auto-updating skills would turn every upstream author into someone with commit access to your agent’s instructions. The cost of my choice is also obvious: a skill on a machine where nobody runs check ages quietly, and I have no fix for that beyond making the command short.

There is a second gap that took me longer to see. An installed skill can be current with its upstream and stale with respect to the runtime that reads it, because runtimes change what they expect from the frontmatter or where they look for files. validate exists for that: it checks a folder against the format rather than against its source, and it is the check people forget, because the skill worked last month.

Failure has to be legible

An installer that writes half a set of files and stops is worse than one that never ran, because the agent reads the half. So the failure behaviour is a feature to design, and I designed three pieces of it. --dry-run shows every path the install would write and writes none of them, and it is the first thing I reach for when a destination looks unfamiliar. Bulk imports, which sweep an existing pile of folders into a managed library, have two documented fallbacks: an invalid private-only name is skipped and reported rather than guessed at, and an import the classifier is not confident about lands on the workflow shelf with a needs-curation label instead of a shelf it may not belong on.

The label matters more than the fallback. A wrong shelf placement that looks confident is a lie the library tells forever. A placement labelled needs-curation is a question, and curate review lists the open questions so a person can answer them in one sitting. The general rule underneath: when the tool is unsure, it should leave a mark that says so, in a place the next command will show. Silence is the failure mode, and it is the default failure mode of every script that catches an exception and moves on.

One more thing an honest installer says out loud. The library knows which repository a skill came from because it wrote that down. It does not verify signatures and it cannot tell you the upstream is benign. Provenance here is recorded, not proven, and a tool that lets people believe otherwise is doing them harm.

Testing an installer without lying to yourself

Unit tests with a mocked file system and a mocked network will pass forever and tell you nothing, because for an installer the file system and the upstream are the product. A mock of either tests the mock. I keep the fast suite, npm test, for the parts that are really logic: CLI argument handling, schema rules for the manifest, name resolution, and install flows into temporary directories on the real disk.

The suite I trust is the live one. npm run test:live uses no mocks at all. It clones the real upstream repositories the manifest points at, captures their raw SKILL.md frontmatter and their file manifests, runs real install, sync, and uninstall flows inside isolated temporary home directories and temporary projects, drives the terminal UI through a real pseudo-terminal so the interactive paths are exercised the way a person exercises them, and writes a report to tmp/live-test-report.json. A quick variant runs a smaller matrix for iteration. The live suite is slow, it depends on the network, and it has caught every class of bug that mattered: an upstream that restructured its folders, a runtime target that moved, a frontmatter field that one publisher formats differently from everyone else.

The lesson generalises past installers. When the thing your software manipulates is someone else’s file layout or someone else’s repository, the only test that means anything is one that touches the real thing, in a sandbox you can throw away. Everything else is a test of your assumptions about them.

What I got wrong

I launched the tool on December 17, 2025 as a universal installer, and for a few weeks that was a distinct thing to be. Then the ecosystem did what ecosystems do. skills.sh appeared with a broader catalog, other installers followed, and being universal stopped being a differentiator and became table stakes. The README changed with the tool: it began to describe a curated library with an installer inside it, because the shelves, the notes, and the provenance were what people kept mentioning. Then I went the other way and settled, in August 2026, on package manager as the category, with the library as one feature of it.

The result is that on the day of writing the npm description and the README lead still say curated library while the repository description and my own pages say package manager, and both are mine. I am not going to pretend that is a design choice. It is a category label that drifted while the software stayed put, and it is on my list to fix. The software itself did not change under either label: small shelves, clear provenance, notes that say why something stays, upstream work staying upstream. Those four lines are in the README under a heading called what I care about, and they have not moved since the first month.

The other mistake was slower. I let the default install targets lag the ecosystem by a couple of months, because changing a default felt like breaking a promise. It was the opposite: the promise is that the skill lands where the agent reads, and a default frozen in time breaks it a little more each week.

If you build one

The decisions, in the order I would make them now:

  1. Pick the default destination from what runtimes read today, keep an explicit per-runtime override, and never write anywhere you were not asked to.
  2. Make a manifest the single source of truth and generate every other view from it, including the documentation.
  3. Default to pointers with provenance; copy only when you intend to edit or need to work offline, and record why on every entry.
  4. Give installed and current different commands, and make updating an action a person takes.
  5. Ship a dry run, and make every uncertain placement leave a visible mark that a later command lists.
  6. Test against real upstreams and a real file system in disposable sandboxes, and treat the mocked suite as a syntax check.

The GUI version of the same problem exists too: SkillCreator.ai previews a diff before every install and can roll it back, which is the desktop answer to the half-written folder. And the question of what belongs in a skill at all, as opposed to behind a tool, is a different essay: Agent skills and MCP servers: what each one is for. The code for everything above is at github.com/MoizIbnYousaf/Ai-Agent-Skills, with the package on npm.

Written by Abdul Moiz Shahzad, AI Engineer, Toronto. Published . More about me and how to reach me on the experience page.

© 2026 Abdul Moiz Shahzad.