Adversarial review of the whole repo at origin/main (900dd90). Everything below was measured, not inferred — commands and observed output are included. Ranked; each finding has a concrete failure scenario.
What's clean (so it isn't relitigated): claude plugin validate . passes; the required status check context catalog matches the job id in .github/workflows/validate.yml:15; branch protection blocks force-push and deletion, dismiss_stale_reviews: true; the validator is stdlib-only (json, sys, pathlib) and resolves the manifest from __file__, so cwd doesn't matter; missing/unparseable manifest exits 1. Happy path works end to end for both CLIs against the live catalog — claude plugin marketplace add open-agent-ai-security/plugins then install praxen@…/socxen@… resolves 1.1.0 / 0.6.5 from each repo's plugin.json, and codex plugin marketplace add + codex plugin add do the same. The deliberate no-version-fields design holds.
H1 — The org-scope check on source.url is bypassable by path traversal; a catalog entry can install an arbitrary repo
scripts/validate_catalog.py:69
if not url.startswith("https://github.com/open-agent-ai-security/"):
A bare startswith on an unnormalized URL. https://github.com/open-agent-ai-security/../octocat/Hello-World.git satisfies it — and git/curl normalize the .. before the request, so the clone lands outside the org:
$ git clone --depth 1 "https://github.com/open-agent-ai-security/../octocat/Hello-World.git" y
Cloning into 'y'...
warning: redirecting to https://github.com/octocat/Hello-World.git/
Proved end to end. Catalog entry {"name":"praxen","source":{"source":"url","url":"https://github.com/open-agent-ai-security/../octocat/Hello-World.git","ref":"master"}}:
python3 scripts/validate_catalog.py → exit 0, "catalog manifest OK — 1 plugin(s), all sources https-pinned, no version metadata."
claude plugin validate . → "✔ Validation passed"
claude plugin install praxen@open-agent-ai-security → "✔ Successfully installed", and the installed tree is Hello-World: …/plugins/cache/open-agent-ai-security/praxen/7fd1a60b01f9/README
Failure scenario: a PR that repoints praxen at …/open-agent-ai-security/../attacker/praxen.git reads as org-scoped at a glance, gets a green catalog check that explicitly advertises "all sources https-pinned", and ships attacker-controlled skill code to every praxen@open-agent-ai-security user on next install/update. This is the single install path for both products, so the blast radius is the whole user base.
Fix: parse rather than prefix-match — urllib.parse.urlsplit, then assert scheme == "https", netloc == "github.com", no userinfo/@ in netloc, and re.fullmatch(r"/open-agent-ai-security/[A-Za-z0-9._-]+\.git", path) (which also rejects .. and the bare-org URL in L1).
H2 — The only supply-chain guard is editable in the same PR it guards
.github/workflows/validate.yml:18-20
The workflow checks out the PR head and runs that checkout's scripts/validate_catalog.py. A single PR can weaken or delete the org-prefix rule and repoint source.url in one change and still show catalog ✅. Nothing in branch protection scopes review to scripts/ — there is no CODEOWNERS (require_code_owner_reviews: false), and enforce_admins: false with 4 admins on the repo means the one required approval is bypassable by any of them.
Failure scenario: the green required check is treated as "the sources were verified" during review, but it verified nothing the PR author didn't consent to. Combined with H1, a green catalog badge is not evidence of org-scoped sources.
Fix: add .github/CODEOWNERS covering /.claude-plugin/** and /scripts/** and turn on require_code_owner_reviews; optionally have the job fetch the validator from main (git show origin/main:scripts/validate_catalog.py) so PRs can't grade their own homework.
M1 — README's mirror claim is false in both halves: praxen carries no mirror of this index, and no CI syncs it
README.md:71-73
The praxen repo carries a mirror of this index in its own .claude-plugin/marketplace.json (legacy install path); a CI check in that repo keeps the mirror in sync with this file.
Neither clause holds. open-agent-ai-security/praxen@main:.claude-plugin/marketplace.json (fetched live):
- lists only praxen — no socxen entry;
- carries
"version": "1.1.0" at both metadata.version and the plugin entry — the exact thing this catalog forbids (validate_catalog.py:54-58);
- uses
"source": "./", a relative string — not the url+ref object this catalog mandates;
- has a different
metadata.description ("Currently publishes Praxen").
And the "CI check that keeps it in sync": praxen's .github/workflows/ci.yml:28-29 runs tests/render/test_plugin_manifests.py, which validates praxen's own file in isolation — it asserts the entry has a version, that all three manifests' versions agree, and that source is a repo-relative .//../ string ("marketplace plugin source is NOT the bare '.'"). It never reads this repo. It enforces the opposite conventions.
Failure scenario: a maintainer trusts this line and "re-syncs the mirror" by copying .claude-plugin/marketplace.json into praxen. praxen CI fails immediately (no version, wrong source type); if forced through, the legacy install path — the one this README promises "remains a maintained mirror" — breaks for every user still on it. Conversely, right now the two files silently diverge with nothing detecting it.
Fix: state what's actually true — praxen keeps a separate, praxen-only legacy marketplace manifest with its own conventions, and there is no cross-repo sync check — or build the check the sentence describes.
M2 — The migration recipes assert a name conflict that doesn't happen, and prescribe an uninstall that isn't needed
README.md:40-49, README.md:56, README.md:60-62
Measured in an isolated CLAUDE_CONFIG_DIR: added the legacy marketplace from open-agent-ai-security/praxen, installed praxen, then ran the new add with no removal first:
$ claude plugin marketplace add open-agent-ai-security/plugins
✔ Successfully added marketplace: open-agent-ai-security (declared in user settings)
settings.json before → after:
"extraKnownMarketplaces": {"open-agent-ai-security": {"source": {"source":"github","repo":"open-agent-ai-security/praxen"}}}
"enabledPlugins": {"praxen@open-agent-ai-security": true}
"extraKnownMarketplaces": {"open-agent-ai-security": {"source": {"source":"github","repo":"open-agent-ai-security/plugins"}}}
"enabledPlugins": {"praxen@open-agent-ai-security": true}
claude plugin list still shows praxen 1.1.0, enabled; claude plugin install socxen@open-agent-ai-security then succeeds. The same-named marketplace is re-pointed in place, losslessly and silently. Consequences:
README.md:60-62 is false — "the add commands conflict on the shared marketplace name until the old one is removed". There is no conflict, no error, no prompt.
- The praxen recipe is gratuitously destructive.
marketplace remove at README.md:41 is what uninstalls the user's plugins; README.md:46-47 then explains the damage the previous line caused. The lossless migration is one line: claude plugin marketplace add open-agent-ai-security/plugins.
- socxen users are sent on a detour —
README.md:60-62 tells anyone holding the legacy praxen marketplace to run the destructive praxen migration "first", which is unnecessary.
README.md:56's "skip only if already added from …/plugins" caveat is moot; the add is idempotent.
Verified via the claude plugin CLI. I did not test in-session /plugin marketplace add; if that path genuinely conflicts, scope the sentence to it rather than stating it generally.
(The rest of the socxen recipe checks out: the legacy manifest at socxen@384ce489:.claude-plugin/marketplace.json was indeed marketplace socxen with plugin key socxen, so socxen@socxen is correct, and it is now deleted from socxen main — the hard cut. marketplace remove uninstalling the marketplace's plugins is confirmed true.)
M3 — "migration is recommended, not required" is untrue for anyone who wants socxen
README.md:48-49
The legacy praxen marketplace publishes praxen only (see M1). A user who declines to migrate cannot install socxen@open-agent-ai-security at all — the plugin key doesn't exist in the manifest their marketplace name resolves to. Migration is optional for staying on praxen, mandatory for reaching socxen. Say so.
L1 — The validator green-lights manifests that break claude plugin marketplace add for everyone
scripts/validate_catalog.py:46-52, :71-75
The docstring at :7-9 claims it "catches the mistakes that would silently break marketplace add". These all exit 0:
| mutation |
validate_catalog.py |
claude plugin validate . |
name: " " |
exit 0 ✅ |
fails: "Plugin name cannot contain spaces" |
description: {"a": 1} |
exit 0 ✅ |
passes (truthiness check only, :52) |
ref: true |
exit 0 ✅ |
— |
url: "https://github.com/open-agent-ai-security/" (no repo) |
exit 0 ✅ |
— |
Failure scenario: a manifest with a space in a plugin name merges on a green catalog check and every marketplace add — the one thing this repo exists to serve — fails schema validation at the client. Add isinstance(..., str) guards and re.fullmatch(r"[a-z0-9][a-z0-9._-]*", name).
L2 — Type confusion crashes the validator instead of reporting a finding
scripts/validate_catalog.py:29-31, :68-69
"owner": "Exabeam" → AttributeError: 'str' object has no attribute 'get' (owner = m.get("owner") or {} passes a truthy string straight through).
"url": null / "url": 123 → AttributeError: 'NoneType' object has no attribute 'startswith'.
Exit status is 1 either way, so the gate holds by accident, but the docstring's "Exit 0 clean, 1 with findings" contract doesn't: no findings list is printed, and a maintainer sees a Python traceback in CI instead of the problem. Type-check owner and url explicitly (the H1 urlsplit rewrite covers the latter).
L3 — ref is unvalidated against the convention it exists to enforce, and entry name is never cross-checked against the target repo
scripts/validate_catalog.py:71-75; convention stated at README.md:69-70
ref is a bare truthiness check, so "ref": "v9.9.9" or "ref": "dev" passes. The README says entries pin each plugin repo's main (the release channel), and the validator's own message warns about "an integration branch" — but only for a missing ref, not a wrong one. Separately, nothing checks that the entry name matches the target repo's plugin.json name: an entry named praxen pointing at socxen.git installs socxen as praxen (the H1 PoC is exactly this shape).
Failure scenario: an entry pinned to dev ships integration-branch skill code to every user, with a green check that claims sources are "https-pinned". Assert ref == "main" (or allowlist), and optionally that <repo>.git basename equals the entry name.
L4 — Floating action tag in the one workflow that gates the install path
.github/workflows/validate.yml:18 — uses: actions/checkout@v4. Repo Actions policy is allowed_actions: "all", sha_pinning_required: false.
Blast radius is genuinely small (permissions: contents: read, no secrets, can_approve_pull_request_reviews: false), so this is hygiene, not an exposure — but this repo defines the install path for two security products and should model its own advice. Pin to a full SHA with a # v4.x.x comment. The pull_request trigger correctly runs on fork PRs with a read-only token, which is the right shape.
L5 — Catalog and README present themselves as Claude Code-only; Codex works and has no documented recipe
README.md:3, .claude-plugin/marketplace.json:8
Both say "Claude Code plugin marketplace". Verified working against the live repo:
$ codex plugin marketplace add open-agent-ai-security/plugins
Added marketplace `open-agent-ai-security` from https://github.com/open-agent-ai-security/plugins.git.
$ codex plugin add praxen@open-agent-ai-security # → 1.1.0
$ codex plugin add socxen@open-agent-ai-security # → 0.6.5
Codex users get no install lines and no migration guidance. Add a Codex block, or drop "Claude Code" from the framing.
Governance note (informational, no action strictly required)
Live protection on main: required_approving_review_count: 1, dismiss_stale_reviews: true, strict: true, contexts ["catalog"], force-push and deletion blocked, enforce_admins: false, no rulesets, no CODEOWNERS.
The premise that the maintainer can't get a PR merged without admin bypass doesn't hold — the repo has four admins (virtualsteve-exa, mike-machnik-exa, scottcexab, SarahFishCO) and four push-level collaborators, so an ordinary reviewed merge path exists and admin bypass shouldn't be the routine. The real residual gap is H2, not the approval count: with enforce_admins: false and no CODEOWNERS, a single admin can merge a PR that edits both the manifest and its own validator, or push straight to main (the push: [main] trigger runs the check after the fact, gating nothing). Enabling enforce_admins and adding CODEOWNERS on /.claude-plugin/** + /scripts/** closes it without changing the approval count.
Review was read-only: no pushes, no branches, no approvals, no repo modifications. All live checks ran against clones and isolated CLAUDE_CONFIG_DIR / CODEX_HOME sandboxes; nothing touched a real plugin install.
— Fable-class review agent, requested by Steve
Adversarial review of the whole repo at
origin/main(900dd90). Everything below was measured, not inferred — commands and observed output are included. Ranked; each finding has a concrete failure scenario.What's clean (so it isn't relitigated):
claude plugin validate .passes; the required status check contextcatalogmatches the job id in.github/workflows/validate.yml:15; branch protection blocks force-push and deletion,dismiss_stale_reviews: true; the validator is stdlib-only (json,sys,pathlib) and resolves the manifest from__file__, so cwd doesn't matter; missing/unparseable manifest exits 1. Happy path works end to end for both CLIs against the live catalog —claude plugin marketplace add open-agent-ai-security/pluginstheninstall praxen@…/socxen@…resolves 1.1.0 / 0.6.5 from each repo'splugin.json, andcodex plugin marketplace add+codex plugin adddo the same. The deliberate no-version-fields design holds.H1 — The org-scope check on
source.urlis bypassable by path traversal; a catalog entry can install an arbitrary reposcripts/validate_catalog.py:69A bare
startswithon an unnormalized URL.https://github.com/open-agent-ai-security/../octocat/Hello-World.gitsatisfies it — and git/curl normalize the..before the request, so the clone lands outside the org:Proved end to end. Catalog entry
{"name":"praxen","source":{"source":"url","url":"https://github.com/open-agent-ai-security/../octocat/Hello-World.git","ref":"master"}}:python3 scripts/validate_catalog.py→ exit 0, "catalog manifest OK — 1 plugin(s), all sources https-pinned, no version metadata."claude plugin validate .→ "✔ Validation passed"claude plugin install praxen@open-agent-ai-security→ "✔ Successfully installed", and the installed tree is Hello-World:…/plugins/cache/open-agent-ai-security/praxen/7fd1a60b01f9/READMEFailure scenario: a PR that repoints praxen at
…/open-agent-ai-security/../attacker/praxen.gitreads as org-scoped at a glance, gets a greencatalogcheck that explicitly advertises "all sources https-pinned", and ships attacker-controlled skill code to everypraxen@open-agent-ai-securityuser on next install/update. This is the single install path for both products, so the blast radius is the whole user base.Fix: parse rather than prefix-match —
urllib.parse.urlsplit, then assertscheme == "https",netloc == "github.com", no userinfo/@in netloc, andre.fullmatch(r"/open-agent-ai-security/[A-Za-z0-9._-]+\.git", path)(which also rejects..and the bare-org URL in L1).H2 — The only supply-chain guard is editable in the same PR it guards
.github/workflows/validate.yml:18-20The workflow checks out the PR head and runs that checkout's
scripts/validate_catalog.py. A single PR can weaken or delete the org-prefix rule and repointsource.urlin one change and still showcatalog ✅. Nothing in branch protection scopes review toscripts/— there is no CODEOWNERS (require_code_owner_reviews: false), andenforce_admins: falsewith 4 admins on the repo means the one required approval is bypassable by any of them.Failure scenario: the green required check is treated as "the sources were verified" during review, but it verified nothing the PR author didn't consent to. Combined with H1, a green
catalogbadge is not evidence of org-scoped sources.Fix: add
.github/CODEOWNERScovering/.claude-plugin/**and/scripts/**and turn onrequire_code_owner_reviews; optionally have the job fetch the validator frommain(git show origin/main:scripts/validate_catalog.py) so PRs can't grade their own homework.M1 — README's mirror claim is false in both halves: praxen carries no mirror of this index, and no CI syncs it
README.md:71-73Neither clause holds.
open-agent-ai-security/praxen@main:.claude-plugin/marketplace.json(fetched live):"version": "1.1.0"at bothmetadata.versionand the plugin entry — the exact thing this catalog forbids (validate_catalog.py:54-58);"source": "./", a relative string — not theurl+refobject this catalog mandates;metadata.description("Currently publishes Praxen").And the "CI check that keeps it in sync": praxen's
.github/workflows/ci.yml:28-29runstests/render/test_plugin_manifests.py, which validates praxen's own file in isolation — it asserts the entry has a version, that all three manifests' versions agree, and thatsourceis a repo-relative.//../string ("marketplace plugin source is NOT the bare '.'"). It never reads this repo. It enforces the opposite conventions.Failure scenario: a maintainer trusts this line and "re-syncs the mirror" by copying
.claude-plugin/marketplace.jsoninto praxen. praxen CI fails immediately (no version, wrong source type); if forced through, the legacy install path — the one this README promises "remains a maintained mirror" — breaks for every user still on it. Conversely, right now the two files silently diverge with nothing detecting it.Fix: state what's actually true — praxen keeps a separate, praxen-only legacy marketplace manifest with its own conventions, and there is no cross-repo sync check — or build the check the sentence describes.
M2 — The migration recipes assert a name conflict that doesn't happen, and prescribe an uninstall that isn't needed
README.md:40-49,README.md:56,README.md:60-62Measured in an isolated
CLAUDE_CONFIG_DIR: added the legacy marketplace fromopen-agent-ai-security/praxen, installed praxen, then ran the new add with no removal first:settings.json before → after:
claude plugin liststill shows praxen 1.1.0, enabled;claude plugin install socxen@open-agent-ai-securitythen succeeds. The same-named marketplace is re-pointed in place, losslessly and silently. Consequences:README.md:60-62is false — "the add commands conflict on the shared marketplace name until the old one is removed". There is no conflict, no error, no prompt.marketplace removeatREADME.md:41is what uninstalls the user's plugins;README.md:46-47then explains the damage the previous line caused. The lossless migration is one line:claude plugin marketplace add open-agent-ai-security/plugins.README.md:60-62tells anyone holding the legacy praxen marketplace to run the destructive praxen migration "first", which is unnecessary.README.md:56's "skip only if already added from …/plugins" caveat is moot; the add is idempotent.Verified via the
claude pluginCLI. I did not test in-session/plugin marketplace add; if that path genuinely conflicts, scope the sentence to it rather than stating it generally.(The rest of the socxen recipe checks out: the legacy manifest at
socxen@384ce489:.claude-plugin/marketplace.jsonwas indeed marketplacesocxenwith plugin keysocxen, sosocxen@socxenis correct, and it is now deleted from socxenmain— the hard cut.marketplace removeuninstalling the marketplace's plugins is confirmed true.)M3 — "migration is recommended, not required" is untrue for anyone who wants socxen
README.md:48-49The legacy praxen marketplace publishes praxen only (see M1). A user who declines to migrate cannot install
socxen@open-agent-ai-securityat all — the plugin key doesn't exist in the manifest their marketplace name resolves to. Migration is optional for staying on praxen, mandatory for reaching socxen. Say so.L1 — The validator green-lights manifests that break
claude plugin marketplace addfor everyonescripts/validate_catalog.py:46-52,:71-75The docstring at
:7-9claims it "catches the mistakes that would silently breakmarketplace add". These all exit 0:validate_catalog.pyclaude plugin validate .name: " "description: {"a": 1}:52)ref: trueurl: "https://github.com/open-agent-ai-security/"(no repo)Failure scenario: a manifest with a space in a plugin name merges on a green
catalogcheck and everymarketplace add— the one thing this repo exists to serve — fails schema validation at the client. Addisinstance(..., str)guards andre.fullmatch(r"[a-z0-9][a-z0-9._-]*", name).L2 — Type confusion crashes the validator instead of reporting a finding
scripts/validate_catalog.py:29-31,:68-69"owner": "Exabeam"→AttributeError: 'str' object has no attribute 'get'(owner = m.get("owner") or {}passes a truthy string straight through)."url": null/"url": 123→AttributeError: 'NoneType' object has no attribute 'startswith'.Exit status is 1 either way, so the gate holds by accident, but the docstring's "Exit 0 clean, 1 with findings" contract doesn't: no findings list is printed, and a maintainer sees a Python traceback in CI instead of the problem. Type-check
ownerandurlexplicitly (the H1urlsplitrewrite covers the latter).L3 —
refis unvalidated against the convention it exists to enforce, and entrynameis never cross-checked against the target reposcripts/validate_catalog.py:71-75; convention stated atREADME.md:69-70refis a bare truthiness check, so"ref": "v9.9.9"or"ref": "dev"passes. The README says entries pin each plugin repo'smain(the release channel), and the validator's own message warns about "an integration branch" — but only for a missing ref, not a wrong one. Separately, nothing checks that the entrynamematches the target repo'splugin.jsonname: an entry namedpraxenpointing atsocxen.gitinstalls socxen as praxen (the H1 PoC is exactly this shape).Failure scenario: an entry pinned to
devships integration-branch skill code to every user, with a green check that claims sources are "https-pinned". Assertref == "main"(or allowlist), and optionally that<repo>.gitbasename equals the entry name.L4 — Floating action tag in the one workflow that gates the install path
.github/workflows/validate.yml:18—uses: actions/checkout@v4. Repo Actions policy isallowed_actions: "all",sha_pinning_required: false.Blast radius is genuinely small (
permissions: contents: read, no secrets,can_approve_pull_request_reviews: false), so this is hygiene, not an exposure — but this repo defines the install path for two security products and should model its own advice. Pin to a full SHA with a# v4.x.xcomment. Thepull_requesttrigger correctly runs on fork PRs with a read-only token, which is the right shape.L5 — Catalog and README present themselves as Claude Code-only; Codex works and has no documented recipe
README.md:3,.claude-plugin/marketplace.json:8Both say "Claude Code plugin marketplace". Verified working against the live repo:
Codex users get no install lines and no migration guidance. Add a Codex block, or drop "Claude Code" from the framing.
Governance note (informational, no action strictly required)
Live protection on
main:required_approving_review_count: 1,dismiss_stale_reviews: true,strict: true, contexts["catalog"], force-push and deletion blocked,enforce_admins: false, no rulesets, no CODEOWNERS.The premise that the maintainer can't get a PR merged without admin bypass doesn't hold — the repo has four admins (
virtualsteve-exa,mike-machnik-exa,scottcexab,SarahFishCO) and four push-level collaborators, so an ordinary reviewed merge path exists and admin bypass shouldn't be the routine. The real residual gap is H2, not the approval count: withenforce_admins: falseand no CODEOWNERS, a single admin can merge a PR that edits both the manifest and its own validator, or push straight tomain(thepush: [main]trigger runs the check after the fact, gating nothing). Enablingenforce_adminsand adding CODEOWNERS on/.claude-plugin/**+/scripts/**closes it without changing the approval count.Review was read-only: no pushes, no branches, no approvals, no repo modifications. All live checks ran against clones and isolated
CLAUDE_CONFIG_DIR/CODEX_HOMEsandboxes; nothing touched a real plugin install.— Fable-class review agent, requested by Steve