Validate manifests, skills and README on every PR - #5
Merged
Conversation
The repo had no CI. A plugin name that stops matching its directory, or a skill absent from the README table, breaks installs for users rather than failing anything at build time, so nothing caught it before merge. Checks the marketplace and plugin manifests describe the same plugins, that shared metadata has not diverged (plugin.json wins at runtime, so divergence ships the wrong version silently), that every SKILL.md has frontmatter whose name matches its directory, and that the README table and its relative links match what is on disk. Also drops the $schema line from marketplace.json. The URL 404s, so it was an editor hint pointing at nothing. Co-authored-by: Cursor <cursoragent@cursor.com>
4 tasks
juangaitanv
reviewed
Aug 11, 2026
Three holes from review. `source` only had to start with `./` and end in the plugin name, so `./vendor/corgea` passed while living outside the tree every other check walks — a plugin could ship without anything here having looked at it. It must now equal `./plugins/<name>`. Quoted frontmatter was compared as raw text, so `name: "corgea-scan"` failed the directory check on a name that is correct. Quoting is optional in YAML; one layer of matching quotes is now stripped, with `''` and backslash escapes undone. A quoted wrong name still fails. README links were checked whole, so any `#fragment` or `?query` read as part of the filename and never resolved. Both are addressing within the target, not part of the path, and are now dropped before the existence check and before comparing a table row to its skill directory. Co-authored-by: Cursor <cursoragent@cursor.com>
juangaitanv
reviewed
Aug 11, 2026
The reader keeps anything it does not understand as text and then validates that text as though it were the value, so `description: [a, b` passed as a description beginning with '['. It now reads a scalar, quoted or not, and rejects the rest by name: flow collections, block scalars, anchors, quotes that are never closed, and unquoted values holding ': ' or ' #', which mean a nested mapping and a comment to a real parser. Duplicate keys are rejected too rather than letting the last one win. A rejected key is not then reported a second time as missing. A missing README.md skipped the skills table and every relative link, so a repo whose entry point had been deleted validated clean. Both it and the marketplace manifest are now required, and their absence says so instead of surfacing as a JSON parse error. Co-authored-by: Cursor <cursoragent@cursor.com>
juangaitanv
reviewed
Aug 13, 2026
Skill rows were matched across the whole README, so the first other table to carry links in its left column would be read as a list of skills and fail on every row of it. Only the table under `## Skills` is read now, and a README without that section says so once rather than once per skill. Rows were matched by name and only the first of them was checked, so a second row could name a real skill while linking at a different one. Every row is checked against the skill it names now, and a name listed twice is rejected. Relative links were resolved without regard for where they landed, so `../` above the checkout passed as long as the path existed on the machine running the validator — while being broken for anyone reading the README on GitHub. Links must now stay inside the repository. Co-authored-by: Cursor <cursoragent@cursor.com>
* Add corgea-mcp and sighthound-rules skills
Two capabilities the repo documented nowhere.
corgea-mcp covers the hosted MCP server: connecting, the CORGEA-TOKEN
header, and the ten read tools. Sourced from the published docs rather
than Corgea/mcp-server, which is private, has not been touched since
September, and still describes a local stdio server with three tools.
A skill written from it would document a product that no longer exists.
sighthound-rules covers rule authoring, which the sighthound skill
mentions in one line. The value is the parts a reader loses an hour to:
rules are RON and not YAML, --use-file-rules is required or edits are
ignored in favour of the rules compiled into the binary, and unknown
fields are dropped silently, so the unless: key in frontend_security.ron
is inert.
MCP is a separate plugin since it needs no CLI and authenticates
differently. Rules sit inside the sighthound plugin, being the same tool
and the same install.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Correct the taint example, field table and test loop
All five verified against the Sighthound source and, where runnable, against
the release binary.
The taint example was a bare rule tuple. Rule files deserialize into `Rules`,
whose list defaults to empty, so that shape parses cleanly and contributes
nothing — a search rule written this way exits 0 with `[]`. It is now wrapped,
and the trap is written up, including that `RULE_WRITING_GUIDE.md` shows a
bare tuple itself.
That example also filed `exec(` and `shell_exec(` under `cwe-89`, copying an
anomaly in `rules/php/taint.ron`. A rule spanning both sink classes has one
`cwe_id` to give them, so half its findings are misclassified. Split into a
SQL rule (`cwe-89`) and a command rule (`cwe-78`), each with matching
sanitizers; running both against a PHP fixture reports the two CWEs correctly.
The field table put severity's values on a row shared with `finding_type` and
`cwe_id`, which take Title Case phrases and lowercase `cwe-<number>`. Each now
has a row, with the `Medium` fallback severity and confidence take when unset.
`mode` is `#[serde(default = "default_search_mode")]`, so it was wrong to list
it as required.
The test loop called whatever `sighthound` is on `PATH`, right after building
the one that has your rules in it.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Take the token out of the config, split matching from metadata
The connect examples pasted the token straight into the file the token section
tells you never to put it in, and the Cursor one dressed it up as a variable:
it set `CORGEA_TOKEN` to a literal and referenced it as `${CORGEA_TOKEN}`,
which is not a syntax Cursor interpolates. Cursor resolves `${env:NAME}` in
`args`, `env`, `url` and `headers`, so the token now stays in the environment
and never reaches the file. Noted that Cursor reads its own environment, so a
GUI launch on macOS may not see a shell export until restarted, and that the
syntax belongs to the client rather than to MCP. Also called out
`.cursor/mcp.json` in the token section, since the project-local copy is the
one committed by accident.
`file_types` and `conditions` were filed under metadata, but both decide what
a rule matches — one picks the files it runs against, the other filters by AST
and context. Split the table in two, so the group to search when a rule fires
in the wrong place is the group that can cause it.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Give Claude Desktop its own config, separate the taint controls
"Same block as Cursor" was wrong: `${env:NAME}` is Cursor's syntax, and Claude
Desktop interpolates nothing. What resolves the placeholder there is mcp-remote,
which substitutes `${NAME}` in a header from its own `process.env`
(`src/lib/utils.ts`), so the value belongs in the `env` block. Cursor's spelling
does not fail loudly in that position — mcp-remote takes `env:CORGEA_TOKEN` as
the variable name, finds none, and sends the header empty — so the symptom is
written down next to the config that causes it. The token stays out of the
Cursor config, and the Claude Desktop file is a user-profile one, not a
repository one.
Both `--header` arguments also lost the space after the colon, which Cursor and
Claude Desktop on Windows mangle.
`sanitizers` and `propagators` were described as one thing that narrows
reported flows. Sanitizers do that. Propagators do nothing at all here:
`src/models.rs` declares the field and no other file under `src/` reads it,
which is why no rule in the repo sets one. They now have separate rows, and the
inert field is filed with `unless:` under the trap it belongs to.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Slim corgea-scan and point it at --help (#7)
* Point corgea-scan at the skill the CLI carries
The CLI now compiles its own skill into the binary and prints it with
`corgea skill show`, so the depth belongs there, pinned to the version
actually installed. Keeping a second copy here guarantees the two drift.
Cuts the body roughly in half: install, auth, and the commands worth
having loaded immediately stay, and the flag matrix, export formats,
upload matrix and troubleshooting go. The skill defers to `corgea skill
show` for the rest, and to `corgea --help` where that command does not
exist yet.
Widens the frontmatter description rather than narrowing it. The body
shrank, but this is now the entry point for the whole CLI, so the
trigger text has to cover the install gate, dependency inventory and
advisories as well as scanning.
Keeps the name: `npx skills add corgea/skills --skill corgea-scan` is
already published in the docs and the README.
Depends on a CLI release carrying `corgea skill show`; merging earlier
points users at a command their binary does not have.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Point corgea-scan at --help rather than a new CLI command
The pointer targeted `corgea skill show`, which does not exist yet and is no
longer being added: `corgea --help` is already compiled into the binary by
clap, already pinned to the installed version, and costs nothing.
Same shape as before — this file stays the lean entry point and defers the
detail — but the target is a command every released CLI already answers, so
this no longer waits on a release to be useful. Adds the version-skew
guidance: report an outdated CLI rather than upgrading unprompted, and treat
a surprising result as a version difference before a bug, since a changed
default will not error the way a missing flag does.
The long-tail reference now points at docs.corgea.app rather than at a second
copy of it.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The repo had no CI at all. A plugin name that stops matching its directory, or a skill absent from the README table, breaks installs for users rather than failing anything at build time, so nothing caught it before merge.
Adds
.github/workflows/validate.ymlmatchingCorgea/clihouse style (ubuntu-latest,permissions: contents: read, SHA-pinned actions, one job) and a dependency-free Node script enforcing:marketplace.jsonentry has aplugins/<name>/.claude-plugin/plugin.json, and vice versa. No duplicate names;sourcestarts with./and contains no..plugin.json.namematches the marketplace entry, anddescription,version,author,keywordsmatch where duplicated.plugin.jsonwins at runtime, so divergence silently ships the wrong metadataSKILL.mdparses, has non-emptynameanddescription, anameequal to its directory, a body, and a description under the 1024-char limitDescriptions are deliberately not diffed against frontmatter: the README uses editorial summaries with inline links.
No JSON Schema validation. The
$schemaURL inmarketplace.jsonredirects to a 404, and the working alternative is a third-party catalog — not worth a network dependency in CI. That dead line is dropped here.Test plan
plugin.jsonversion diverging from the marketplace, a skill on disk missing from the README, a plugin directory with no marketplace entry, and a README link that does not resolveMade with Cursor