The public home of the aor-* agent skills: one repository that users of Claude Code and GitHub
Copilot CLI add as an install source, and that gh skill and npx skills install from directly
where a skill is self-contained, with every skill family shipped as a plugin under plugins/ and
versioned on its own.
Maintained by Troels Vognsen (agentsonrails@trvo.dk) as a personal open-source project.
Agents-On-Rails is a GitHub organisation used for that work; there is no company behind it and no
support commitment. To report a security problem, see .github/SECURITY.md;
for anything else, open an issue.
The marketplace is named aor. Two plugins ship today, each as a 0.x preview:
| Plugin | Version | Skills | Needs |
|---|---|---|---|
aor-comm |
0.1.4 | aor-format-teams-message: drafts a rich Microsoft Teams message and puts it on the clipboard ready to paste |
Windows; Python 3.9 or later on PATH as python |
aor-kb |
0.1.8 | aor-kb-query: searches a knowledge base and prints the matching claims, each stamped with a trust label that says how the claim is known; quarantined and retired claims are held back unless you ask for them. aor-kb-capture: records what a session learned as a graded claim and routes it to a personal or a work base. Routing is fail-closed but not a boundary guarantee — it never reads the claim's content and cannot tell work knowledge from personal, it takes its signal from the working directory the command runs in, and its signal lists are empty until you fill them. aor-kb-setup: wires this machine to a knowledge base you have already cloned — dependency check, config root, manifest entry, repo scaffold — and never creates a repository, commits, pushes, or registers a workspace |
Windows; Python 3.9 or later on PATH as python, plus the pinned strictyaml in the plugin's requirements.txt |
What these skills do on your machine. Both plugins are Python that runs locally when you invoke
the skill. Neither makes a network call: no module in either plugin imports anything
network-capable, and the only subprocess either one starts is git, run against a local repository
you named — the subcommands it uses are all local ones. aor-kb additionally imports strictyaml,
a third-party package you install yourself; that is outside this repository and its behaviour is its
own.
aor-commreads the files you name on the command line, plus one file it ships with —capabilities.json, its own manifest, read from its install directory on every render. It writes nothing to disk at all, and it replaces the clipboard.aor-kbreads and writes a knowledge base you point it at. Two things worth knowing before you install it. A query writes nothing into the knowledge base — versions up to 0.1.6 logged each query there, and the plugin's README says what that leaves behind. And it writes a small configuration directory under your local application data — created when you runkb-capture initor when a capture first records a workspace, not merely by querying. One more, only if you ask for it:--as-ofreconstructs the knowledge base as it stood at a past date, and materialises that snapshot into a temporary directory outside both the knowledge base and that configuration directory.
The clipboard is emptied before it is written. aor-comm clears the clipboard and then writes
the new contents. If the write fails part-way, the clipboard is already empty — whatever was on it
before is gone. Do not run it holding something you cannot regenerate.
An unpinned install follows this repository. On the channels that do not pin — Copilot CLI,
which tracks the tip of main, and gh skill or npx skills without a pin — the next update runs
whatever has been committed here since, with the file-system and clipboard access you gave the
skill. Not every channel can pin. Claude Code and gh skill can, and their sections say how;
Copilot CLI accepts a pin syntax whose effect on install is unconfirmed here; npx skills has no
pin at all. If you need a fixed version, use Claude Code or gh skill --pin.
What a pin does and does not give you. A pinned tag cannot be moved: repository rulesets refuse
tag deletion and non-fast-forward tag pushes, and the publish gate refuses to repoint a tag the
remote already carries. That is immutability. It is not authenticity — nothing in this
repository is signed, every commit reports verified=false reason=unsigned, and the tag objects
carry no signature. Pinning means "the same bytes as last time"; it does not mean "the bytes came
from the maintainer". If you need that, review the diff yourself.
Windows only. Both plugins target Windows and neither is supported anywhere else.
aor-comm writes the clipboard through user32 and cannot work off Windows at all. aor-kb
resolves its configuration directory through a Windows shell API that reads the process token, and
raises on any other platform rather than falling back to an environment variable — so
aor-kb-capture, and any query that names an instance, refuse immediately off Windows. (A query
given an explicit path never reaches that guard and may import elsewhere; that is an accident of
where the check sits, not a supported configuration.) The maintainer tooling in scripts/ runs on
Windows and Linux and throws on anything else.
Python, reachable as python. Both plugins run Python 3.9 or later and invoke it as python,
so that name must resolve to a real interpreter. Check with python --version. On Windows a fresh
machine often resolves python to the Microsoft Store stub, which opens the Store instead of
running anything — if python --version opens the Store or prints nothing, install Python and make
sure it precedes the stub on PATH.
aor-kb needs one more package, and will not run without it. aor-kb depends on
strictyaml, pinned:
python -m pip install strictyaml==1.7.3
Install it into the same interpreter python resolves to. Without it, the first invocation of
either aor-kb skill exits 3 and names what is missing — it does not fail at install time, and
no plugin-manager check will tell you, so do this before you try the skill. The pin is the one
in the plugin's own requirements.txt; if it has moved, that file wins over this page.
aor-comm has no third-party dependency and needs nothing here.
Per channel, in addition:
| Channel | Also needs |
|---|---|
| Claude Code | Claude Code itself, and an SSH key registered with GitHub unless you set the HTTPS variable below |
| Copilot CLI | GitHub Copilot CLI |
gh skill |
GitHub CLI new enough to carry gh skill, which is a preview subcommand — gh skill --help tells you whether you have it |
npx skills |
Node and npx |
The same skill has a different name depending on how you installed it, and only the plugin channels
carry aor-kb.
| Channel | Command shape | Installed as | How you invoke it |
|---|---|---|---|
| Claude Code plugin | claude plugin install aor-comm@aor |
plugin, namespaced | /aor-comm:aor-format-teams-message |
| Copilot CLI plugin | copilot plugin install aor-comm@aor |
plugin, bare name when unique | you describe the task; Copilot matches on the skill's description rather than a typed command |
gh skill |
gh skill install Agents-On-Rails/skills aor-format-teams-message --agent <agent> --scope <scope> |
bare name, in the directory --agent and --scope select |
the bare skill name, per your agent's convention |
npx skills |
npx skills add Agents-On-Rails/skills --skill aor-format-teams-message -a <agent> |
bare name, in the directory -a selects — with -a claude-code, .claude/skills/ in the current project |
the bare skill name, per your agent's convention |
The double prefix in /aor-kb:aor-kb-query is deliberate: the plugin is aor-kb and the skill is
aor-kb-query, so that the bare name stays globally unique on the channels that install it flat.
If two channels install the same skill on one machine you get two copies on separate update lines, and which one answers depends on the host:
- Claude Code refuses a second plugin that declares a name an installed plugin already owns, and names the remedy. A personal skill of the same bare name shadows the plugin's bare alias.
- Copilot CLI resolves first-found-wins across its discovery tiers, and plugin skills rank below
every project and personal skills directory. So a copy installed by
gh skillornpx skillsbeats the plugin's, and the plugin's is silently ignored — no message, no error.copilot skill listis what shows you which one won. gh skillrefuses a same-named skill from a second repository unless you pass--force, which overwrites. The two never coexist.
Pick one channel per machine unless you have a reason not to.
claude plugin marketplace add Agents-On-Rails/skills
claude plugin install aor-comm@aor
claude plugin install aor-kb@aor
Inside a session the same steps are /plugin marketplace add Agents-On-Rails/skills and
/plugin install aor-comm@aor or /plugin install aor-kb@aor.
- Name after install — plugin skills are namespaced:
/aor-comm:aor-format-teams-message,/aor-kb:aor-kb-query,/aor-kb:aor-kb-captureand/aor-kb:aor-kb-setup. - Transport — the first command clones over SSH, so it needs an SSH key registered with GitHub.
Without one, set
CLAUDE_CODE_PLUGIN_PREFER_HTTPS=1first and it clones over HTTPS. - Updates — Claude Code installs the version named in the plugin's manifest and updates only
when that version rises, so a commit here that does not raise the version never reaches you. Run
claude plugin marketplace update aor, thenclaude plugin update aor-comm@aor, then restart. Marketplaces you add yourself do not auto-update unless you enable it for them. - To pin — add the marketplace at a tag before installing:
claude plugin marketplace add Agents-On-Rails/skills@aor-comm--v0.1.0, thenclaude plugin install aor-comm@aor. A marketplace pins to a branch or a tag, never to a commit. - Confirm it worked — restart, then
claude plugin list. You should seeaor-comm@aorwith status✔ enabledand the version you expect. A running session keeps resolving the copy it started with, so the restart is not optional andplugin listin a new terminal says nothing about a session already open. ⚠✔ enabledmeans loaded, not working. Foraor-kbit will say✔ enabledeven whenstrictyamlis missing and every invocation exits 3. The only check that covers that is to run the skill — invoke/aor-kb:aor-kb-queryonce and see it answer. Do the dependency step above first and this will not arise.
copilot plugin marketplace add Agents-On-Rails/skills
copilot plugin install aor-comm@aor
copilot plugin install aor-kb@aor
- Name after install — the bare skill name,
aor-format-teams-message. If another plugin ever ships the same skill name, both appear plugin-qualified asaor-comm:aor-format-teams-message. Copilot has no user-typed slash commands: the skill is selected by matching your request against its description, so you describe the task rather than naming the skill. - Transport — a public marketplace needs no login step.
- Updates — Copilot installs the tree at the tip of
main, andcopilot plugin update aor-comm@aorrefreshes to it whether or not the version has changed. For Copilot the tracked branch is the release line and the version string is informational. - To pin —
copilot plugin marketplace add Agents-On-Rails/skills#aor-comm--v0.1.0is accepted and the ref is recorded in the profile settings. Its effect on a subsequent install has not been confirmed here, so treat Copilot as an unpinned channel unless you have verified otherwise yourself. - Confirm it worked — run
copilot skill list. It runs in its own process and reads the installed set directly, so it tells you the install landed. ⚠ Then start a new session before using the skill. Copilot enumerates skills once at session start, so a session that was already open when you installed will answer "not found" no matter whatskill listsays. The two checks answer different questions:skill listtells you the files are there, a new session is what makes them reachable. ⚠ Being listed means loaded, not working. As on Claude Code, anaor-kbwhosestrictyamlis missing lists normally and exits 3 when invoked. Do the dependency step above, then ask Copilot to do something the skill covers and see it answer.
gh skill install Agents-On-Rails/skills aor-format-teams-message --agent claude-code --scope user
Name the agent and the scope. Run non-interactively without them, gh skill defaults to
--agent github-copilot and --scope project, so the command lands the skill somewhere you may not
have meant. Substitute --agent github-copilot for Copilot, or drop --scope user to install into
the current repository instead of your home directory.
Do not install aor-kb-query, aor-kb-capture or aor-kb-setup this way — install aor-kb as
a plugin instead (claude plugin install aor-kb@aor or copilot plugin install aor-kb@aor, above).
⚠ The installer will offer them to you anyway: they are declared in the marketplace manifest, so
--list shows all three skills and an install of any of them will appear to succeed. It does not. A
skill-level install delivers a skill's own folder and nothing above it. All three aor-kb skills
share their tools/, requirements.txt and instances.yml.example at the plugin root, so a
skill-level install places a SKILL.md whose tool is not there. It looks like it worked: the
command exits 0, prints no warning, and writes exactly one file. aor-format-teams-message keeps
its tool beside its own SKILL.md, which is why it installs cleanly this way.
- Name after install — the bare skill name, in a flat per-scope directory. Names must therefore be unique across every repository you install from, not just within this one.
- Placement —
--scopetakesprojectoruserand defaults toproject.--agentselects the host directory; the values you are likely to want areclaude-codeandgithub-copilot, andgh skill install --helplists all of them. Run non-interactively with no--agent, it defaults togithub-copilot.--diroverrides both. - Updates —
gh skill update --allrefreshes installed skills;--dry-runreports what would change without touching anything. - To pin —
--pin aor-comm--v0.1.0, or name it asaor-format-teams-message@aor-comm--v0.1.0. A pin takes a tag or a commit SHA, and a pinned install is left alone bygh skill updateuntil--unpin. - Unpinned resolution is repository-wide, and that will surprise you.
gh skill install --helpstates the order as "latest tagged release in the repository, else default branch HEAD". Measured here, "tagged release" means a GitHub Release, not a git tag: plain tags are ignored and prereleases do not count. This repository tags every plugin release but has exactly one Release,v0.1.0-preview, which is marked prerelease — so today an unpinned install resolves to the tip ofmain, not to any of those tags. The first non-prerelease Release, for either plugin, would become the resolution target for every unpinned install of every skill here, because the search is repository-wide rather than per-skill. Pin if that matters to you. - Confirm it worked —
gh skillhas nolistcommand. Look in the directory the install reported and check the skill's folder is there with itsSKILL.mdand its tool files; a one-file result is the silent failure described above. On Windows the success output may end with(could not read directory)even when every file is present — that message is a false alarm, not a failed install.
npx skills add Agents-On-Rails/skills --skill aor-format-teams-message -a claude-code
Name the agent. Without -a the installer prompts, and the identifier for Claude Code is
claude-code — -a claude is rejected. Add -y to skip the prompts entirely, and -g to install
globally rather than into the current project.
Do not install aor-kb-query, aor-kb-capture or aor-kb-setup this way either — same reason
as the gh skill section above, and the same successful-looking one-file result. --list will show all
three skills here too. Install aor-kb as a plugin.
- Name after install — the bare skill name. With
-a claude-codeit lands in.claude/skills/<name>/inside the current project, not in your home directory;-ginstalls globally instead. - Placement —
-atakes an agent identifier, and the identifier for Claude Code isclaude-code;-a claudeis rejected.npx skills add Agents-On-Rails/skills --listshows what this repository offers, andnpx skills listshows what you have installed. By default the installer symlinks into the agent directory;--copycopies instead. - Updates —
npx skills update. There is no pin flag: this channel follows the repository, so treat it as unpinned. It writes askills-lock.jsonbeside the install, andnpx skills experimental_installrestores from it. - Telemetry — the installer has an install telemetry event, which its
--metadataflag attaches JSON to. That is the installer's behaviour, not this repository's, and it is worth knowing before you run it in a place where that matters. - Confirm it worked —
npx skills list, and check the skill's folder holds its tool files and not a loneSKILL.md.
Everything above about how the four installers behave was observed on Windows on 2026-09-09
against Claude Code 2.1.266, GitHub Copilot CLI 1.0.83, GitHub CLI 2.93.0, and the skills npm
package 1.5.25 run through npm/npx 11.12.1 on Node 26. skills@latest is a moving target — the
command this page gives you resolves whatever is newest at the time you run it, which may not be
1.5.25. gh skill is a GitHub CLI preview and is documented as subject to change
without notice; Copilot CLI released point versions roughly weekly over the period these notes were
built from. All four installers move faster than this page does. Where an installer's behaviour
has changed since, its own --help wins over this page — and if you find a difference, please
report it, because that is the failure mode this section exists to make visible.
Each plugin carries one version, in its plugin.json, and every push that changes a plugin raises
it. A release is that version plus the tag <plugin>--v<version> (aor-comm--v0.1.0), pushed
together. While a plugin is 0.x, its releases are tags only; a GitHub Release, if any, is marked
as a prerelease.
What 0.x means here. Anything may change in any release: skill names, invocation strings,
command-line flags and file locations. There is no deprecation period, no migration path and no
support commitment. If you need a fixed version, pin a tag — each install section above says how for
its channel. None of these plugins is close to 1.0.
plugins/aor-<family>/holds one plugin per skill family, each with its own.claude-plugin/plugin.jsonandskills/<name>/SKILL.md; the marketplace manifest at.claude-plugin/marketplace.jsonlists them.legacy/<skill>/holds the first-generation requirements skills (aor-req,aor-test-trace,aor-review,aor-review-adhoc) and their SME agents underlegacy/agents/, parked untouched until they are ported into a family. They are not installable through a plugin. The tagv0.1.0-previewkeeps the original layout and the documents that described it.scripts/holds the publish-safety gate, its installer, the scanner fetcher and the fixture suite..github/workflows/gate.ymlruns the same gate as detection in CI, the fixture suite on Windows and Linux, and the two root validators..github/SECURITY.mdand.github/CONTRIBUTING.mdcarry the disclosure path and the contribution terms.
Everything in this repository is public, and it is maintained from a context that also holds private material. The gate is what keeps the two apart, and it is structural rather than editorial: a commit or push that carries the wrong thing is refused, not flagged for later.
One script, scripts/publish-gate.ps1, runs in three places: as the local pre-commit hook over
the staged tree, as the local pre-push hook over every commit in the push range, and in CI over
the pushed range. It uses two instruments. The literal layer runs git grep -F over a private
list of identifiers the maintainer must never publish; the list lives outside every repository
(its path in the clone's local git config) and reaches CI as an organisation secret read through
stdin, the gate refuses to run without it, and it never prints a matched value. The pattern layer
runs a hash-pinned release of gitleaks with the tracked
.gitleaks.toml, which extends gitleaks' default secret rules with this repository's own rules
for path shapes that would reveal a machine or an account; the config file is the only place
those patterns are spelled. Tree rules make process noise impossible to commit: only a fixed set
of entries may exist at the root, file names that look like session artifacts are refused, every
SKILL.md must sit at plugins/aor-<family>/skills/<name>/SKILL.md or legacy/<name>/SKILL.md
with a frontmatter name equal to its directory and unique across the tree, every plugin
directory must carry a complete manifest and exactly one marketplace entry, no file may exceed
1 MB, and no binary file is accepted. The noise, size and binary rules also run over every commit
tree inside a push range, not only its tip, and file names, commit messages, authors and tag
messages pass through both the identifier list and the pattern rules. Every run first proves its
own detection with a positive control and re-hashes the scanner; an error is a failure, never an
empty result.
Pushes to main must raise the version of every plugin they touch; a tag must be named
<plugin>--v<version>, match that plugin's manifest at the tagged commit and be reachable from
main; non-fast-forward pushes to main and tag deletions are refused outside INCIDENT MODE.
Two repository rulesets are the second lock: one on the default branch and one on all tags, each
refusing deletion and non-fast-forward pushes, each with no bypass actors — so they bind the
maintainer too. Moving or deleting a published tag takes disabling a ruleset first, which is a
deliberate and visible act rather than a quiet force-push. Note that these are rulesets, not
classic branch protection, and the two APIs disagree in a way that will waste your time:
repos/.../branches/main/protection returns 404 "Branch not protected" while
repos/.../branches/main returns protected: true. Both are accurate about their own question.
Check repos/.../rulesets.
The gate prints the path, the line numbers and the number of matching lines, never the matched
text. The maintainer re-derives the culprit locally with git grep -n. A file whose name
contains a listed identifier is reported only as a count, and the name is withheld.
- Have PowerShell 7 (
pwsh), git 2.28 or later, the GitHub CLI (gh, signed in) and Claude Code (a native install or the npm package) on PATH. - Write the identifier list by hand, outside every repository: one identifier per line,
#comment lines allowed, UTF-8. The default location is~/.aor/publish-denylist.txt. - Fetch the pinned scanner:
pwsh scripts/get-gitleaks.ps1. It verifies the download against the hashes pinned in the gate script and refuses anything else. - Install:
pwsh scripts/install-gate.ps1 -GitHubUser <your login>. It aborts at the first failed guard and writes nothing on abort. The guards, in order: the clone is not nested inside another work tree; the identifier list and the pinned scanner are both in place;pwshand both root validators (claude,gh) are on PATH;core.hooksPathis unset in every scope; and a remoteoriginexists. Then it writes the two hooks, records the list and scanner paths in local git config, rewrites theoriginURL to HTTPS if it is a github.com URL — a non-GitHub origin is left alone — and sets a credential helper that reads your token from theghkeyring. Safe to re-run. ⚠ Thecore.hooksPathguard catches people out. If you use husky, a global hooks directory, or anything else that sets it, this aborts and tells you nothing about which scope set it. Checkgit config --show-origin --get core.hooksPath.
Maintaining requires Windows or Linux. The scanner fetcher and the gate both resolve a pinned binary for Windows x64 or Linux x64 and throw on anything else, so the gate cannot run on macOS, and CI runs neither macOS nor any arm64 host. The platform test looks at the operating system only and never at the architecture, so on an arm64 machine the fetch takes the x64 branch, downloads the x64 binary and passes both hash checks — the pins are on the x64 artefact. Whether it then runs is up to your emulation layer. Untested here; treat arm64 as unsupported rather than as known-broken.
From then on every commit and push from that clone runs the gate. Bypassing it (--no-verify,
editing the local config) is a deliberate, visible act; the threat model is accidental exposure
by the maintainer's own tools, not a hostile maintainer.
To check a plugin directory that lives elsewhere, before it moves here:
pwsh scripts/publish-gate.ps1 -Path <directory> treats it as plugins/<basename>/ and applies
the same rules (without the root set and the marketplace check), reading the list and scanner
paths from that repository's local config.
The literal layer has none, in any file, name or message. The pattern layer accepts only scoped
entries in .gitleaks.toml, each with a description naming its reason, never a content literal
and never a .gitleaksignore. The gate pins the number of such entries, so adding one is a
two-file commit that names the entry, confirmed by the maintainer. There are none today. The
pattern config is accepted at the root only, a gitleaks ignore file nowhere, and a .gitattributes
line may not change how git tells text from binary or add a filter, because either would change
what the gate sees.
- Freeze. No further pushes until the rest is done.
- Remove. Rewrite the ref so that no reachable commit carries the content; delete or move
every tag that points at a removed commit in the same push; delete any GitHub Release on such
a tag. The hooks accept a non-fast-forward push to
mainand a tag deletion only withAOR_GATE_INCIDENT=1set for that one push, and they still scan the whole new range. Disable the branch and tag rulesets for that push and re-enable them afterwards. Never use--no-verify. - Purge. Ask GitHub Support, through their documented request, to remove cached views and pull-request references. Forks and clones made inside the window are out of reach; record them as such.
- Rotate. Anything that is a secret is rotated. Identifiers cannot be rotated; the exposure window is recorded.
- Reinstate. Confirm both rulesets are active again and re-run the CI gate green.
CI withholds the organisation secret from a fork, so the literal layer reports "denylist not
loaded" and the gate job is red by design, while its pattern and tree layers still report. A
maintainer reviews the diff, fetches the branch, merges it on main through the local hooks and
pushes. The merge button is never on the path. See .github/CONTRIBUTING.md.
The scanner is an unsigned binary in the user profile. Windows Defender may re-scan it on every launch, which can add a second or two to each commit. That is accepted; a system-wide install on PATH would trade the hash pin for speed.
MIT, see LICENSE.