Skip to content

Validate manifests, skills and README on every PR - #5

Merged
leenk7991 merged 5 commits into
mainfrom
feat/validation-ci
Aug 17, 2026
Merged

Validate manifests, skills and README on every PR#5
leenk7991 merged 5 commits into
mainfrom
feat/validation-ci

Conversation

@leenk7991

Copy link
Copy Markdown
Member

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.yml matching Corgea/cli house style (ubuntu-latest, permissions: contents: read, SHA-pinned actions, one job) and a dependency-free Node script enforcing:

  • Manifest bijection. Every marketplace.json entry has a plugins/<name>/.claude-plugin/plugin.json, and vice versa. No duplicate names; source starts with ./ and contains no ..
  • Cross-file metadata equality. plugin.json.name matches the marketplace entry, and description, version, author, keywords match where duplicated. plugin.json wins at runtime, so divergence silently ships the wrong metadata
  • Frontmatter. Every SKILL.md parses, has non-empty name and description, a name equal to its directory, a body, and a description under the 1024-char limit
  • README table. One row per on-disk skill, no orphan rows, link paths correct, and every relative link resolves

Descriptions are deliberately not diffed against frontmatter: the README uses editorial summaries with inline links.

No JSON Schema validation. The $schema URL in marketplace.json redirects 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

  • Passes on the current tree
  • Fails, with a precise message, on each of five deliberately introduced breakages: frontmatter name drifting from its directory, plugin.json version 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 resolve
  • Both pinned action SHAs verified against their upstream tags
  • Workflow YAML parses

Made with Cursor

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>
Comment thread scripts/validate.mjs Outdated
Comment thread scripts/validate.mjs Outdated
Comment thread scripts/validate.mjs
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>
@leenk7991
leenk7991 requested a review from juangaitanv August 11, 2026 14:20
Comment thread scripts/validate.mjs Outdated
Comment thread scripts/validate.mjs Outdated
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>
@leenk7991
leenk7991 requested a review from juangaitanv August 12, 2026 20:30
Comment thread scripts/validate.mjs
Comment thread scripts/validate.mjs Outdated
Comment thread scripts/validate.mjs Outdated
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>
@leenk7991
leenk7991 requested a review from juangaitanv August 13, 2026 15:49

@juangaitanv juangaitanv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

* 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>
@leenk7991
leenk7991 merged commit a25f7a1 into main Aug 17, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants