Repository Types¶
skillsaw automatically detects your repository structure. A repository can match multiple types simultaneously (e.g. an agentskills repo that also has .coderabbit.yaml).
A type describes either how the repository packages its content — a
marketplace, a plugin, an APM project — or which tool it is configured
for. Both are the same kind of fact: if the checkout holds a tool's
configuration, that tool's rules run and Repo type: says so. Every value
below is also accepted by --type, which replaces packaging-type detection;
tool types are always detected from the checkout, and plugin-contributed
types too.
The tool types sort below the packaging types, so the single "primary" type
in the JSON report's repo_type field is unchanged: a marketplace that also
ships a .cursor/ is still a marketplace.
agentskills.io Skills¶
Standalone skill repositories following the agentskills.io specification:
my-skill/
├── SKILL.md # Required: metadata + instructions
├── scripts/ # Optional: executable code
├── references/ # Optional: documentation
├── assets/ # Optional: templates, resources
├── evals/ # Optional convention from the evaluation guide
│ └── evals.json
├── agents/
│ └── openai.yaml # Optional: OpenAI skill metadata (interface, policy, dependencies)
└── <any-dir>/ # Arbitrary directories allowed per spec
Skill collections (multiple skills in subdirectories) are also supported:
Standard discovery paths are checked automatically: .agents/skills/,
.agent/skills/, _agents/skills/, _agent/skills/, .apm/skills/,
.claude/skills/, .github/skills/, .cursor/skills/,
.clinerules/skills/, .cline/skills/, .qwen/skills/,
.opencode/skills/ and .opencode/skill/. A portable SKILL.md under any
of them makes the repository an Agent Skills repository, which turns on the
agentskill-* rules.
Devin also reads native skills from .devin/skills/, including that directory
under nested workspace/package roots. Those files deliberately use a separate
dialect: their YAML frontmatter is optional, name defaults from the
directory, and Devin adds model, subagent, permission, tool, and trigger
fields. They get the shared content and security rules plus
devin-skill-valid, not the portable
agentskill-valid/agentskill-name requirements.
Windsurf skills under .windsurf/skills/ follow the portable Agent Skills
dialect: name and description are required, and the specification expresses
allowed-tools as a space-separated string. Skillsaw also accepts the
historical list form for compatibility. Like Devin skills, nested Windsurf
skill collections are discovered. A skill under .agents/skills/ also remains
a portable Agent Skill even when the repository contains Devin configuration.
Agent Plugins¶
Portable plugin packages following the Agent Plugins v1 specification:
my-plugin/
├── plugin.json # Required: exactly at the plugin root
├── skills/ # Optional
│ └── my-skill/ # Immediate child directory
│ └── SKILL.md # Agent Skills specification
└── mcp.json # Optional: portable MCP server configuration
Each skill must be an immediate child of skills/; deeper descendants are not
discovered as additional skills. plugin.json and, when present, mcp.json
must use the canonical Agent Plugins v1 schema identifiers. The
agent-plugin-json-valid and agent-plugin-mcp-valid rules validate those
files, while the agentskill-* rules validate discovered SKILL.md files.
Automatic detection is deliberately strict. A plugin.json at the lint root,
or at an immediate plugins/* child, must declare a canonical Agent Plugins
manifest schema identifier. This both supports multi-package collections and
avoids claiming unrelated repositories that happen to contain plugin.json.
An mcp.json alone is not detection evidence. Supported v1 manifests are
validated locally; a canonical identifier for an unsupported version is still
detected so the version error can be reported.
Use skillsaw lint --type agent-plugin to force Agent Plugin validation when
the manifest is missing, malformed, incomplete, or declares the wrong schema;
the defect is then reported instead of preventing detection.
Agent Plugins can coexist with Claude and Codex plugin formats. A repository
may contain root plugin.json, .claude-plugin/plugin.json, and
.codex-plugin/plugin.json markers at the same time; skillsaw detects each
matching repository type and applies its rule family independently. One
format's manifest does not substitute for another's.
MCP Registry publisher metadata¶
Publisher repositories for the
official MCP Registry
can keep one or more server.json documents at the repository root or inside
monorepo packages:
weather-server/
├── server.json # Registry publisher metadata
└── package.json # npm ownership metadata, when locally available
Automatic detection requires either the canonical MCP Registry $schema URL
or the Registry's distinctive server identity plus package/remote shape. This
keeps unrelated application files named server.json out of scope. Use
--type mcp-registry when an intended Registry document is too malformed to
provide detection evidence.
The Registry rules validate strict JSON against the released schema each
document declares, enforce the reverse-DNS server namespace and current
transport/registry type vocabulary, reject version ranges, recommend strict
Semantic Versioning, and compare a local npm package's mcpName with the
server.json name. The npm check never queries a package registry; an
external package with no matching local package.json is left alone.
Single Plugin¶
my-plugin/
├── .claude-plugin/
│ └── plugin.json
├── commands/
│ └── my-command.md
├── skills/
│ └── my-skill/
│ └── SKILL.md
└── README.md
Marketplace (Multiple Plugins)¶
skillsaw supports multiple marketplace structures per the Claude Code specification:
Traditional Structure (plugins/ directory)¶
marketplace/
├── .claude-plugin/
│ └── marketplace.json
└── plugins/
├── plugin-one/
│ ├── .claude-plugin/
│ └── commands/
└── plugin-two/
├── .claude-plugin/
└── commands/
Flat Structure (root-level plugin)¶
marketplace/
├── .claude-plugin/
│ └── marketplace.json # source: "./"
├── commands/
│ └── my-command.md
└── skills/
└── my-skill/
Custom Paths and Mixed Structures¶
Plugins from plugins/, custom paths, and remote sources can coexist in one marketplace. Only local sources are validated.
OpenAI Codex Plugin¶
Directories with a .codex-plugin/plugin.json manifest, per the Codex plugin specification:
my-plugin/
├── .codex-plugin/
│ └── plugin.json # Required — only this file belongs here
├── skills/
│ └── my-skill/
│ ├── SKILL.md
│ └── agents/
│ └── openai.yaml # Optional: OpenAI skill metadata
├── agents/
│ └── openai.yaml # Observed plugin-root metadata form (catalog compatibility)
├── hooks/
│ └── hooks.json # Optional
├── .mcp.json # Optional: bundled MCP servers
├── .app.json # Optional: registered MCP mappings
└── assets/ # Optional: icons, screenshots
skillsaw probes the repository root, plugins/*, .codex/plugins/*, and every local source the Codex marketplace declares.
.codex/plugins/* is where Codex installs plugins into a checkout, so the split there is between what the repository runs and what the repository wrote:
| Rule | On an installed plugin | Why |
|---|---|---|
hooks-dangerous, hooks-prohibited, codex-hooks-valid |
Runs (no autofix) | These commands execute in this checkout. Whoever wrote them, they are this checkout's exposure. |
mcp-valid-json, mcp-prohibited |
Runs (no autofix) | Same — the host spawns these commands here. |
agentskill-* |
Runs (no autofix) | These skills enter the agent's context window here. |
codex-plugin-json-valid, codex-plugin-structure |
Stands down | A kebab-case name, a missing description or a dangling asset path is a defect in a file the developer cannot edit. |
codex-openai-metadata |
Stands down | Same — a vendor plugin's agents/openai.yaml is presentation metadata the developer cannot edit, and it configures nothing that executes here. |
codex-marketplace-registration |
Stands down | The repository did not author the plugin; its published catalog has no business listing it. |
The line is authorship, not discovery: skillsaw does not walk .claude/plugins/* at all, so a broken vendor manifest there is likewise not the repository's problem.
Findings under .codex/plugins/* are diagnostic only; autofix never rewrites
vendor-managed installed content.
hooks and mcpServers accept a path, an array of paths, or the config inline — all forms are followed, because a hook written inline runs exactly like one in a file. skills names directories:
| Field | Default location | Also followed |
|---|---|---|
hooks |
hooks/hooks.json |
declared paths, inline objects |
mcpServers |
.mcp.json |
declared paths, inline server maps |
skills |
skills/ |
declared directory paths |
Paths that leave the plugin root are not followed; codex-plugin-json-valid reports them.
OpenAI Codex Marketplace¶
Repositories with a Codex catalog at .agents/plugins/marketplace.json:
marketplace/
├── .agents/
│ └── plugins/
│ └── marketplace.json
└── plugins/
├── plugin-one/
│ └── .codex-plugin/plugin.json
└── plugin-two/
└── .codex-plugin/plugin.json
Sibling files in .agents/plugins/ are read as catalogs too. A name ending in marketplace.json at a separator — api_marketplace.json, which is how openai/plugins splits its catalog — is taken on existence alone, so a broken one still reaches the rule that reports it. Any other *.json has to carry a plugins array with at least one entry declaring a source to be treated as a catalog — a version-pin or metadata sibling with no sources is left alone, which also means a source-less broken catalog under an arbitrary name goes unlinted; give a real catalog a marketplace.json-suffixed name so existence alone claims it.
Codex also reads .claude-plugin/marketplace.json for backward compatibility, but skillsaw leaves that path to the Claude marketplace-* rules: the two schemas disagree (Claude requires owner; Codex adds policy, category, and interface), so linting one file against both would report contradictory violations. The consequence is that a Codex-schema catalog written to the legacy path will be checked against the Claude schema — a missing owner, and an unknown local source type on every entry. Put a Codex catalog at .agents/plugins/marketplace.json.
Both Codex types are independent of the Claude types — a repository commonly ships both manifests, and skillsaw detects both.
.claude/ Directory¶
Repositories with a .claude/ directory containing commands, skills, hooks, agents, or rules. When APM is present, .claude/ is treated as compiled output and this type is not detected.
CodeRabbit¶
Repositories with a .coderabbit.yaml file. skillsaw validates the instruction fragments within the config.
Muse Code¶
Repositories with a .muse/hooks.json, the committed project hooks
Muse Code reads. skillsaw finds one at
the repository root and in any subpackage, because Muse reads the .muse/
layer of the project it is started in; .muse/worktrees/ holds whole
checkouts Muse made for child agents and is skipped.
Muse uses the nested hooks format Claude Code pioneered, with its own
lifecycle events, matcher-group keys and handler fields.
muse-hooks-valid checks the file against them.
This matters more than it sounds: Muse prints no diagnostic for anything it
refuses, so a rejected file, a dropped matcher group and a skipped handler
all look like a hook that had nothing to do. The commands themselves are
scanned by hooks-dangerous and
hooks-prohibited, including the
commandWindows variant.
Muse reads AGENTS.md for portable instructions and .agents/memory/ for
committed team memory. Both are shared conventions rather than Muse
surfaces, so neither is Muse evidence on its own — but both are linted:
AGENTS.md wherever it appears, and committed memory at the repository
root, <repo>/.agents/memory/, which is where Muse documents it.
Grok Build¶
Repositories with a .grok/ project layer — a .grok/ directory carrying
any of rules/, skills/, agents/, commands/, hooks/, config.toml,
lsp.json, workflows/, roles/, personas/ or sandbox.toml — the
layer Grok Build reads. An empty
.grok/ is not detected. skillsaw finds a project layer at the repository
root and in any subpackage, because Grok reads the .grok/ layer of the
project it is started in.
Most of what is attached is linted by rules that already existed:
.grok/skills/*/SKILL.md are portable Agent Skills and get the full skill
rule set, and .grok/rules/*.md, .grok/commands/*.md and
.grok/agents/*.md get the shared content and security rules. Grok reads
each of those three directories at the top level only, so a file nested a
directory deeper is not attached either — it is not context Grok loads.
.grok/skills/ is the exception and is walked in full. config.toml is
parsed as TOML and attached, and the rules that read it come with it.
lsp.json, sandbox.toml, workflows/, roles/ and personas/ are
detection evidence only today — nothing under them is read or linted yet;
covering them is later work (see the
Grok Build design record).
Three things in that layer are Grok's own structure, on top of the shared
rules above. grok-hooks-valid validates every
.grok/hooks/*.json — Grok merges the whole directory, so a repository may
have several — against Grok's events, alias table and handler fields. This
matters more than it sounds: Grok refuses a whole file over one wrong-typed
field and reports nothing when it does, so a rejected file, a dropped matcher
group and a skipped handler all look like a hook that had nothing to do. The
commands themselves are scanned by
hooks-dangerous and
hooks-prohibited.
grok-agent-valid covers the second: a
.grok/agents/*.md whose frontmatter is missing, malformed, or without
name or description is dropped by Grok, and the subagent never appears
in the agent list. The third is config.toml, which gets its own paragraphs
below.
Two things in that layer decide whether a file loads at all, and neither
changes what skillsaw lints. Grok gates hooks, MCP and LSP on folder trust —
until a project is trusted they are silently skipped — while skills, rules,
commands and agents load whether or not the folder is trusted. Trust is a
per-machine decision recorded outside the repository, so skillsaw lints the
files as committed. Project MCP servers are declared in .grok/config.toml
under [mcp_servers] and in the repository-root .mcp.json; there is no
.grok/mcp.json. skillsaw reads both, so
mcp-prohibited sees a server wherever a Grok
project declared it.
A project config.toml contributes only [mcp_servers], [plugins],
[permission] and [mcp] max_output_bytes. Every other table in it is
dropped, and dropped silently: Grok's unknown-key warnings cover the user's
own ~/.grok/config.toml and not a project file, so a typo'd table there
produces no diagnostic anywhere. [plugins] paths is dropped the same way,
honored only from the user's file.
grok-config-project-scope reports
that: an ignored top-level table or scalar, [plugins] paths, and the
spellings that load nothing at all —
[[mcp.servers]], [mcp-servers], [mcpServers], [permissions],
transport inside a server, defaultMode inside [permission].
grok-config-valid covers the file itself: a
parse error costs every table in it including the ones above the error, and
Grok exits 0 with an empty stderr when that happens, while a malformed
server costs that server and a malformed [permission] key costs that key —
or, for a non-table entry inside rules, every rule in the array.
Grok reports the server defects through mcpConfigProblems and the
permission ones not at all.
Grok reads AGENTS.md and CLAUDE.md for portable instructions, both of
which carry their own repository types, so a .grok/ directory is the only
marker that is Grok Build's alone. .grok/plugins/ holds project-scoped
plugins rather than project configuration, so it is not evidence for this
type; a plugin there is found by the plugin discovery below, like any other.
Grok Build Plugin¶
Directories with a .grok-plugin/plugin.json manifest, plus every local
source a Grok catalog declares:
my-plugin/
├── .grok-plugin/
│ └── plugin.json # Optional to Grok, and the marker skillsaw claims
├── skills/
│ └── my-skill/
│ └── SKILL.md
├── commands/
├── agents/
├── hooks/
│ └── hooks.json # Optional
├── .mcp.json # Optional: bundled MCP servers
└── .lsp.json # Optional: not linted yet
Grok resolves a manifest from plugin.json, then .grok-plugin/plugin.json,
then .claude-plugin/plugin.json, and reads the first it finds. Two
different questions follow from that chain, and skillsaw answers them
separately.
Which directory is Grok's is decided by .grok-plugin/plugin.json alone,
or by a Grok catalog listing the directory. The other two spellings are
another ecosystem's declaration — a root plugin.json is the Agent Plugins
entrypoint, and .claude-plugin/ is Claude's — and claiming them would put
every Claude plugin and every portable package under Grok's rules as well.
Which file Grok reads once the directory is claimed is the whole chain. So
grok-plugin-json-valid reports against a root plugin.json or a
.claude-plugin/plugin.json when that is the one Grok resolves to — the
finding names the file, and it is the file to open. A directory carrying
both .grok-plugin/plugin.json and .claude-plugin/plugin.json is both a
Grok plugin and a Claude plugin, and each ecosystem's rules apply
independently to the manifest its own host reads.
A manifest is optional to Grok: a directory holding skills/, agents/,
hooks/hooks.json or .mcp.json loads without one. skillsaw still needs a
declaration to attribute the directory to Grok, so a manifest-less plugin is
claimed only when a Grok catalog lists it as a local source.
hooks and mcpServers accept a path or the object inline; skills,
commands and agents accept a path or an array of paths. All forms are
followed, because a hook written inline runs exactly like one in a file:
| Field | Default location | Also followed |
|---|---|---|
hooks |
hooks/hooks.json |
a declared path, an inline object |
mcpServers |
.mcp.json |
a declared path, an inline server map |
skills |
skills/ |
declared directory paths |
commands, agents |
commands/, agents/ |
declared directory paths |
Paths that leave the plugin root are not followed. Grok drops them too, and
silently: a declared skills path pointing outside the plugin loads zero
skills while grok plugin validate still calls the manifest valid.
Two rules cover the packaging itself.
grok-plugin-json-valid validates the
manifest, and its severities carry the blast radius: a manifest that fails
to load makes Grok skip the whole directory — skills/ does not rescue it,
and grok plugin install still prints success — while a declared path that
escapes or does not exist costs that component list alone.
grok-plugin-structure covers the
directory: with no manifest and none of skills/, agents/,
hooks/hooks.json or .mcp.json, the installer refuses it. commands/
alone and .lsp.json alone do not count, measured against the binary.
A plugin's hooks/hooks.json is scanned by
hooks-dangerous and
hooks-prohibited, and its .mcp.json by
mcp-valid-json and
mcp-prohibited — inline declarations included.
grok-hooks-valid deliberately does not see them: Grok loads plugin hooks
through a different adapter from the project layer's, and that adapter
publishes nothing observable about which entries survived, so the failure
scopes that rule reports were measured on .grok/hooks/*.json and apply
there only.
Grok Build Marketplace¶
Repositories with a Grok catalog at .grok-plugin/marketplace.json:
marketplace/
├── .grok-plugin/
│ ├── marketplace.json # The index Grok reads
│ └── plugin-index.json # Optional display catalog, read from beside it
└── plugins/
├── plugin-one/
│ └── .grok-plugin/plugin.json
└── plugin-two/
└── .grok-plugin/plugin.json
Grok looks for a catalog at .grok-plugin/marketplace.json, then
.claude-plugin/marketplace.json, then a root-level marketplace.json, and
reads exactly one. The root spelling is last here and first in the
plugin-manifest order above; the two lookups share no ordering. skillsaw
claims the first for Grok and leaves .claude-plugin/marketplace.json to the
Claude marketplace-* rules, because the two schemas disagree: Claude
requires owner, while a Grok entry carries category and a source in one
of three shapes. Put a Grok catalog at .grok-plugin/marketplace.json.
An entry's source names either a directory in this repository or a remote
repository to clone. The local forms are {"type": "local", "path": "./x"}
and the bare string "./x" — and, measured against the binary, an object
with no discriminator or a misspelled one, because the loader keys on path
alone. A url is what makes an entry remote, and its own path then names a
subdirectory of the clone rather than a directory here. Local sources are
resolved and contained against the marketplace root, so a package that is a
marketplace of its own resolves against the package. Sources that escape
that root are dropped, by Grok and here.
plugin-index.json beside the catalog is what the marketplace browser reads
before anything is installed, and a require_sha deployment installs from
the sha values it publishes. skillsaw attaches it under its catalog.
grok-marketplace-json-valid
validates the catalog. A catalog Grok cannot parse is discarded whole and
discovery falls back to scanning plugins/, so the repository looks healthy
while everything catalogued from anywhere else disappears; an entry with no
name, no source, or a path that does not resolve is dropped one at a
time, silently. grok-marketplace-index-parity
compares plugin-index.json against the catalog beside it — a sha that
has drifted blanks that plugin's component listing — and reports nothing
when there is no index.
A Grok catalog explains its own plugins/ directory, so a Grok-only
marketplace is not reported as a Claude marketplace with a missing manifest.
Both Grok packaging types are independent of the Claude and Codex types — a
repository commonly ships more than one catalog or manifest, and skillsaw
detects each.
Google Antigravity¶
Repositories that configure Google Antigravity's CLI, agy. Configuration
lives in a customization root — .agents/, .agent/, _agents/ or
_agent/. agy walks up from the directory it was started in to the
repository root and reads every root it finds on the way, so a monorepo
package carries its own layer. skillsaw attaches every dot root's content
the same way; _agents/ and _agent/ are also ordinary source-package
names, so those two attach only where the root declares one of Antigravity's
own files.
A root holds hooks.json, mcp_config.json, always-on prose in
rules/**/*.md, subagents in agents/*.md, portable Agent Skills in
skills/, plugins in plugins/<name>/, and the registries agents.json,
plugins.json, skills.json and workflows.json, each naming where else
to load that kind of customization from.
The root's presence is not what detects the type, and neither is all of
its content. .agents/skills/ is the portable Agent Skills convention every
ecosystem reads and .agents/memory/ is committed project memory that
predates this host; .agents/ itself is a tool-neutral layout, and of 30
sampled repositories carrying .agents/rules, 27 hold no Antigravity file
at all. So detection asks for one of the six named JSON files or a
plugins/<name>/plugin.json — with one exception: under .agent/, the
documented Windsurf-lineage path no other tool reads, a populated rules/
or agents/ is evidence too.
Attachment is wider than detection here, deliberately: prose under any dot
root is linted whether or not the repository is typed antigravity, since a
rules file is agent context whichever tool ends up reading it. The two
non-dot roots are the exception in the other
direction — they read the same "declares one of its files" test detection
uses, so nothing attaches from a source package that merely shares the
name.
The same directory is where OpenAI Codex publishes a catalog, at
.agents/plugins/marketplace.json, with its plugins declaring themselves in
<name>/.codex-plugin/plugin.json. The two never collide: Antigravity's
marker is a plugin.json at the top of a plugin directory, Codex's is the
.codex-plugin/ directory inside it, and a catalog file is neither. A
directory both claim keeps both sets of checks — provenance() records
every claim, and each ecosystem's format rules read only their own.
Configuration is validated by:
antigravity-hooks-valid: a defect inhooks.jsonthat drops the whole file, or a keyagyignores so the hook never runs.antigravity-mcp-valid:mcp_config.json, which is startup-fatal when it does not parse and silently drops one server when its shape is wrong.mcp-valid-jsonstands its own shape walk down for this file and keeps only its dialect-neutral checks — a committed credential and a URL carrying user information.antigravity-config-json-valid: the registry files. Opt-in.
Rules in <root>/rules/**/*.md are always-on prose and get the full suite of
content-quality and context-budgeting checks; <root>/agents/*.md are
subagents; <root>/skills/*/SKILL.md get the Agent Skills rules.
Google Antigravity Plugin¶
Automatic discovery checks direct children of plugins/ under a customization
root, such as .agents/plugins/<plugin-name>/. A plugins.json entry or an
inherited registry can also name a plugin directory elsewhere in the repository.
Both require plugin.json. Nested plugins/outer/inner/ directories are not
automatically discovered unless a registry names them.
berth-tools/
├── plugin.json # name, description, disabled, logo
├── skills/ # Agent Skills
│ └── berth-check/
│ └── SKILL.md
├── agents/ # subagents
├── commands/ # converted to skills on install
├── rules/ # prose
├── hooks.json # lifecycle hooks
└── mcp_config.json # MCP servers
antigravity-plugin-json-valid
validates the manifest. It carries four fields that mean anything — name,
description, disabled, logo — and every other key, $schema and
version and author included, is discarded by agy and reported by
nothing. A package written to the portable
Agent Plugins schema and dropped in here
is claimed and loaded unchanged.
skillsaw does not follow a plugin.json or a plugin directory symlinked out
of the checkout, where agy does. Reading a file outside the repository it
was pointed at is a line it does not cross; see
THREAT_MODEL.md, T6.
OpenAI Codex project configuration¶
Repositories with a .codex/hooks.json or a .codex/config.toml, the
project layer Codex reads. This is distinct from a Codex plugin
(.codex-plugin/plugin.json) and from a Codex marketplace: it configures the
checkout rather than packaging anything, so it is never treated as a plugin
claim and never exempts the repository from another ecosystem's rules.
Codex reads the layer of every directory between the repository root and the
one a session starts in, so a package's own .codex/ is live configuration
and every one in the checkout is linted.
Lifecycle hooks come from both files, merged: the [hooks] tables of a
config.toml get the same checks hooks.json gets.
codex-hooks-valid validates both files and
reports a layer that declares hooks in both, while
hooks-dangerous and
hooks-prohibited scan the commands in them. A
shape defect in config.toml stops Codex starting at all, where the same
defect in hooks.json costs only that file's hooks; the rule's page records
that asymmetry, and the one check config.toml gets and hooks.json does
not.
config.toml also carries the project's MCP servers, in
[mcp_servers.<name>] tables — there is no .codex/mcp.json — so
mcp-prohibited inventories them and
mcp-valid-json applies its dialect-neutral
checks, such as a committed credential in an env or http_headers table.
No rule validates a server table's shape: Codex names the server and the
field and exits 1 over a malformed one itself. Everything else in the file is
Codex settings skillsaw reads nothing from.
.codex/plugins/ is an install location rather than project configuration —
see OpenAI Codex Plugin for what runs there.
Promptfoo¶
Repositories with promptfoo eval configs (promptfooconfig*.yaml or YAML files in evals/ directories). Prompt strings in the config are treated as content blocks, so all content-* rules apply to them automatically. Dedicated promptfoo-* rules validate config structure, assertion coverage, and metadata.
APM (Agent Package Manager)¶
Repositories with an .apm/ directory or apm.yml file. APM manages dependencies and compiles instruction files for all supported agents (.claude/, .cursor/rules/, .github/instructions/, etc.). When APM is present it is the authoritative source — .claude/ is treated as compiled output. Package content under apm_modules/ is externally sourced: it is linted but never autofixed by default, and lint-external-content: false omits it from the lint tree.
Editor and CLI tools¶
Each tool below is a repository type of its own, detected from the
configuration it reads. Their content is picked up in any repository,
whatever else it is, because it ships in the checkout. Every prose file
listed below gets the content-* rules that apply to it (weak language,
contradictions, attention dead zones, secrets, and the rest) plus the
security rules, because its text lands in an agent's context window. A few
content rules are scoped to a role rather than to all prose —
content-instruction-drift compares always-on instruction files, so it
does not look at on-demand commands, prompts, agents or workflows. The JSON
configuration files — mcp.json, hooks.json — are machine config, never
linted as prose; they get the MCP and hook rules instead.
The same separation applies to the Vercel skills CLI's project
skills-lock.json: it is generated machine state, so only
skills-lock-valid checks it. The rule validates
the structure and portability metadata the CLI reads; it does not pass the
generated JSON through content-quality rules. Installed skill directories
named by remote lock entries are tagged as externally sourced. They remain
visible to rules by default but are never autofixed; see
lint-external-content for the opt-out.
Where a tool reads AGENTS.md, that is the file skillsaw expects you to write
— Cursor, Copilot, Cline, OpenCode, Muse Code, Grok Build, Google Antigravity and Codex all read it, and one well-linted
AGENTS.md beats five per-vendor copies that drift apart. skillsaw does not
reimplement a per-vendor instruction format on top of it; what it adds is
coverage of the prose each tool keeps in its own directory, plus structural
validation wherever a tool's own metadata can fail silently — see
cursor-rules-valid,
cursor-hooks-valid,
copilot-agent-valid, and
opencode-config-valid.
Each tool is its own repository type, named in the Type column. That is
the value Repo type: prints, the JSON report lists under repo_types, and
--type accepts.
| Tool | Type | Files linted |
|---|---|---|
| Portable | agents-md, claude-md, gemini, qwen |
AGENTS.md, CLAUDE.md, GEMINI.md, QWEN.md |
| Portable skills | agentskills |
.agents/skills/*/SKILL.md and the other conventional skill directories |
| Vercel skills CLI | skills-lock |
Every skills-lock.json, plus matching installed skill payloads unless lint-external-content: false |
| Cursor | cursor |
.cursor/rules/**/*.mdc, .cursor/commands/**/*.md, .cursor/skills/*/SKILL.md, .cursor/mcp.json, .cursor/hooks.json, legacy .cursorrules |
| Copilot / VS Code | copilot |
.github/copilot-instructions.md, **/*.instructions.md, .github/prompts/**/*.prompt.md, .github/agents/**/*.md, legacy .github/chatmodes/**/*.chatmode.md, .github/skills/*/SKILL.md, .vscode/mcp.json |
| Cline | cline |
.clinerules (file), .clinerules/**/*.md, .clinerules/**/*.txt (excluding workflows/, hooks/, skills/), .clinerules/workflows/**/*.md, .clinerules/skills/*/SKILL.md, .cline/skills/*/SKILL.md |
| OpenCode | opencode |
opencode.json or opencode.jsonc at the root and in .opencode/, .opencode/commands/**/*.md, .opencode/agents/**/*.md, .opencode/modes/*.md, .opencode/skills/*/SKILL.md, and the 1.x singular spelling of each (command/, agent/, mode/, skill/). Repository-local files matched by instructions paths or globs are also linted; remote URLs are not fetched. |
| Devin CLI / Desktop | devin |
.devin/rules/**/*.md, .devin/global_rules.md, .devin/skills/*/SKILL.md, nested AGENTS.md/agents.md, AGENTS.local.md, AGENT.md, CLAUDE.md; legacy .windsurf/rules/, .windsurf/global_rules.md, and .windsurfrules |
| Windsurf | devin |
.windsurf/skills/*/SKILL.md (portable Agent Skills dialect, including nested workspace roots) |
| Qwen Code | qwen |
QWEN.md, .qwen/skills/*/SKILL.md |
| Kiro | kiro |
.kiro/steering/*.md |
| Google Antigravity | antigravity |
Inside .agents/, .agent/, _agents/ or _agent/: hooks.json, mcp_config.json, the registries {agents,plugins,skills,workflows}.json, prose in rules/**/*.md and agents/*.md, and skills under skills/. A plugins.json or agents.json registry's entries are followed, so a plugin or agent directory it names elsewhere in the repository is linted too. Detection is narrower — see Google Antigravity |
| Muse Code | muse |
.muse/hooks.json — see Muse Code |
| Grok Build | grok-project |
.grok/rules/*.md, .grok/commands/*.md, .grok/agents/*.md, .grok/skills/*/SKILL.md, .grok/hooks/*.json, .grok/config.toml — see Grok Build |
| OpenAI Codex | codex-project |
.codex/hooks.json, .codex/config.toml — see OpenAI Codex project configuration |
| Committed project memory | — | <repo>/.agents/memory/MEMORY.md (index) and every **/*.md beneath that directory |
.agents/memory/ is the one row with no type of its own: the convention
predates every tool that reads it and none owns it, so committed memory is
linted without making the repository anything in particular. It is read from
the repository root only — <repo>/.agents/memory/, which is where Muse
documents it — and everything below that directory is linted. A copy nested
somewhere else in the tree is not attached, because it is not memory to the
tools that read it either.
Discovery and validation are separate layers for Copilot. Every Markdown file
under .github/agents/ and every *.chatmode.md file under the legacy
.github/chatmodes/ directory is attached as agent prose, so it receives the
shared content and security rules.
copilot-agent-valid additionally validates
the YAML fields that determine how GitHub cloud and VS Code interpret the
agent, including their target-specific model, tool, subagent, handoff, MCP,
metadata, and hook behavior. Unknown tool names remain valid, matching both
consumers' forward-compatible behavior.
skillsaw finds .cursor/, .github/, .clinerules/, .opencode/, .devin/
and .windsurf/
anywhere in the tree, so a monorepo package that carries its own set is
linted alongside the root's. How much each tool actually reads from a nested
directory varies, and not every case is settled: Cursor documents nested
AGENTS.md and .cursor/skills/, but steers rules toward a single root
.cursor/rules/ scoped with globs, and reports on whether nested rule
directories load disagree across versions. VS Code walks from the workspace
folder up to the repository root. Cline and
.github/copilot-instructions.md resolve one path relative to the workspace
directory, so a nested copy is read only when that directory is the
workspace. OpenCode walks from the working directory up to the git worktree
root and merges every .opencode/ it passes, so a nested one is read as
well as the root's. Devin reads rule directories and its supported plain
instruction files at multiple project levels; Devin Desktop also discovers
AGENTS.md case-insensitively. skillsaw lints every nested tool directory either way —
committed instructions are worth checking wherever a teammate might open
them, and a rule that turns out not to load is worth knowing about too.
skills-lock.json is recursive for a different reason: each project that
runs the skills CLI owns its own lockfile, so a monorepo can legitimately
commit several. Exact-name lockfiles are discovered throughout the checkout;
vendored directories and configured exclude paths stay out of scope.
Lockfiles still contribute external-source provenance when the lockfile path
itself is excluded: an exclude must not make autofix reinterpret a managed
dependency as authored content.
The plain GEMINI.md and QWEN.md formats remain root-only. AGENTS.md
(including Desktop's case-insensitive spelling), AGENTS.local.md,
AGENT.md, CLAUDE.md, and .windsurfrules are discovered at every project
level for Devin's location-scoped behavior. A file shared with another tool
is attached once, so a nested CLAUDE.md or AGENTS.md does not produce
duplicate content findings.
Most conventional skill directories remain root-only: a skill in
apps/web/.cursor/skills/review/SKILL.md is not discovered. Devin and
Windsurf are the exceptions because the workspace scan explicitly supports
nested .devin/ and .windsurf/ roots. Their distinct skill dialects are
preserved after discovery.
devin-rules-valid validates rule YAML,
activation fields, repository-relative glob patterns, and Devin Desktop's
12,000-character workspace-rule limit. Unknown frontmatter keys are accepted
so a newly added Devin field does not break existing repositories.
MCP configuration is read for its servers wherever it lives, so
mcp-valid-json and mcp-prohibited cover .cursor/mcp.json,
.vscode/mcp.json and the mcp section of an opencode.json or
opencode.jsonc, plus mcp-servers embedded in Copilot custom-agent
frontmatter, as well as
.mcp.json. VS Code spells the server map servers and adds a sibling
inputs array for prompted variables; skillsaw reads the former and ignores
the latter.
Among the editor tools, OpenCode is the one whose shape is validated
elsewhere — Agent Plugins also defers, though more broadly, to its own
agent-plugin-mcp-valid. OpenCode's transports are named for where the
server runs (local/remote) rather than for the wire protocol, a local
command is an argv array rather than a string, and its environment map is
spelled environment, so every field check would misfire. mcp-valid-json
stands aside and
opencode-config-valid checks the shape.
Some checks do not defer. Those that hold whatever dialect a file is written
in — a document that is not JSON, a url carrying user information, a
credential in a server's environment, headers or oauth map — stay in
mcp-valid-json even for a deferred block, which also means they still fire
for a project pinned to a version: older than opencode-config-valid.
That carve-out is specific to OpenCode; the Agent Plugins deferral is total,
and applies only while agent-plugin is among the detected repository
types.
The policy rules are unaffected: mcp-prohibited reads OpenCode servers in
the 1.x flat layout under mcp and the 2.0 nested one under
mcp.servers, including a file that carries both at once. Reading only one
layout would let a config hide a server behind the other.
Files that are on-demand rather than always-on — Cursor commands, Copilot
prompt files, Cline workflows, OpenCode commands — are budgeted by
context-budget as commands, not as instruction
files, because they enter the context window only when invoked.
Cursor hooks¶
.cursor/hooks.json is a command-execution surface that ships in the
repository, so its commands are scanned by
hooks-dangerous and
hooks-prohibited alongside Claude Code hooks
and settings. Cursor's schema is flatter than Claude's — hooks hang directly
off the event name rather than off a matcher group — so claude-hooks-valid
leaves the file alone and cursor-hooks-valid validates the shape instead.
A type: "prompt" hook injects text rather than spawning a process, so the
command scanners skip it — but Cursor puts that text into the agent's
context every time the event fires, which makes it shipped instruction
prose. Its prompt string is linted as content, so
security-hidden-instructions and
the other injection scanners read it, and hooks-prohibited counts it as a
hook. JSON carries no line numbers, so those findings name the file without
a line.
Committed project memory¶
.agents/memory/ holds notes a team checks into the repository for whatever
agent reads it — the shared counterpart of Claude Code's per-developer auto
memory. The convention belongs to no tool: projects were committing it
before Muse Code shipped, and Muse reads it the way it reads AGENTS.md,
injecting MEMORY.md in full at session start (even in an untrusted
workspace) alongside the paths of the other Markdown files in the directory,
which it reads on demand. The index is one line per topic by convention;
Muse lists every Markdown file there whether or not the index mentions it.
skillsaw therefore attaches the directory at the repository root
unconditionally, and it is evidence of no tool in particular. The index and
the topic files beside it are agent context, so they get every content and
security rule, and both are budgeted under the memory category — the index
because a reader loads it whole, a topic file because a reader loads it
whole once the topic comes up.
OpenCode and APM¶
.opencode/ is an editor directory that is also an APM compile target
(.claude, .cursor, .gemini, .opencode, .agents), so "authored
content" and "build output" have to be told apart. The four readings below
resolve the same way for each of those directories; the evidence is APM's,
never OpenCode's:
- No
.apm/and noapm.yml— the repository is native OpenCode..opencode/is authored and everything in it is linted in full. - APM present with a readable
apm.ymlwhosetargets:omitopencode— APM never writes there, so.opencode/is hand-written and still linted in full. A source tree alone does not make a directory generated. The manifest has to be readable for this: a repository with an.apm/directory and noapm.ymlat all falls into the last case below, not this one. - APM present and targeting
opencode—.opencode/is compiled output and APM wins, exactly as it does for.claude/. The content findings belong on the.apm/primitives an author can edit, not on copies the nextapm compileoverwrites. The suppression is content-only: the security and structural rules still read what actually ships, because a generated file can be hand-edited. A skill under the compiled directory is not discovered, for the same reason. - The
targets:list cannot be read — becauseapm.ymlis missing, unparseable, or declares notargets:key. APM keeps the directory: answering "not generated" when the manifest cannot say would report every finding twice, once on the.apm/source and once on its copy. Note that an.apm/directory alone is enough to make a repository an APM project, so a repository with.apm/and noapm.ymllands here.
A root opencode.json or opencode.jsonc is never treated as build
output: APM compiles
into .opencode/, never over a root config.
This determination is made at the repository root only — apm_compiled_roots()
looks for <root>/.opencode, nothing deeper. A nested packages/x/.opencode/
is always authored content and is always linted in full, whatever apm.yml
lists.