Skip to content

fix(plugins): complete issue #10 port hardening - #33

Merged
AojdevStudio merged 3 commits into
mainfrom
worktree-issue-10-plugin-port-hardening
Jul 18, 2026
Merged

AojdevStudio merged 3 commits into
mainfrom
worktree-issue-10-plugin-port-hardening

Conversation

@AojdevStudio

Copy link
Copy Markdown
Owner

Summary

  • normalize four drifted marketplace descriptions to their plugin manifests
  • require every shipped plugin manifest and skill to declare a non-empty description
  • enforce exact manifest/marketplace description equality in the existing 19-plugin validator
  • retain the bulk port sync, manifests, READMEs, referenced files, catalog entries, and junk cleanup already merged in ✨ feat: package shared Pi skills, extensions, and 9 Claude Code plugins #11

Closes #10

Verification

  • bun run lint
  • bun run typecheck
  • bun run test
  • bun run validate:plugins
  • bun run validate:skills
  • bun run list
  • bun pm pack --dry-run --ignore-scripts (228 files, 9.27 MB; no local artifacts)
  • pi -ne -e . --print "Reply exactly: plugin smoke ok"
  • gitleaks detect --no-git --source claude-code/plugins --config .gitleaks.toml --redact --verbose
  • gitleaks git . --config .gitleaks.toml --redact --log-opts="origin/main..HEAD" --verbose
  • manual home-path, RFC1918, localhost-port, credential, and cookie scan

Main refresh

Merged fresh origin/main after PR #31 landed. Only package-lock.json entered this branch; no source files conflicted or auto-merged.

@changeset-bot

changeset-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 95d663d

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AojdevStudio, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 51 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: eeee33de-d6a3-48f9-8e97-dbe574454161

📥 Commits

Reviewing files that changed from the base of the PR and between d17b78e and 95d663d.

📒 Files selected for processing (2)
  • .claude-plugin/marketplace.json
  • scripts/validate-plugin-ports.mjs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-issue-10-plugin-port-hardening

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@AojdevStudio
AojdevStudio merged commit 3b9ed17 into main Jul 18, 2026
6 checks passed
@AojdevStudio

Copy link
Copy Markdown
Owner Author

Heightened post-merge review

Reviewed the immutable squash range d17b78ec540d7b3e903bb3aaa9c3fab116170e5d...3b9ed17d27b7d5776b2c90622d2e504ef14332a4 with the final merged tree as the source of truth.

Findings

  1. P1 — the new description gate accepts YAML values that are not non-empty strings. readFrontmatterField tokenizes text instead of parsing YAML, and checkSkillFrontmatter only tests truthiness. I reproduced description: null, description: ~, description: [], description: {}, and description: # comment all passing this check. The later Agent Skills validator also treats these as non-empty strings, so npm run check can accept them. The same parser misreads valid inline YAML comments and block-scalar modifiers. This contradicts the PR claim that every shipped skill is required to declare a non-empty description.

  2. P1 — the referenced-file guard can approve a non-portable plugin. The validator deliberately skips bare scripts/, cli/, assets/, and hooks/ references even though issue Audit and harden Claude Code plugin ports #10 explicitly requires those shipped resources to exist (scope comments). It also joins paths without rejecting .. or confirming the result remains inside the plugin root (resolution/check); a crafted tools/../../... reference can therefore pass by finding a repo file outside the bundle. Today's known issue-listed resources are present, but the claimed regression gate is incomplete.

  3. P1 — the exact CI package contains a generated local artifact. Merge-SHA CI run 29663221979 is green, but its npm run pack:dry output includes codex/plugins/html-docs/scripts/__pycache__/markdown_to_html.cpython-312.pyc (41.0 kB) and reports 229 files. That file is absent from the committed tree; the preceding Codex-plugin fixture verifier generates it after its junk scan. This directly conflicts with the PR body's 228-file / “no local artifacts” evidence and the issue's packaging-hygiene gate.

  4. P2 — public metadata still has two compatibility/documentation defects. The ask-codex marketplace edit removed “Requires the codex binary on PATH” to match a manifest that omits it, while the README and SKILL still declare that prerequisite. Exact two-file equality now passes, but the issue's four-surface consistency goal does not. Separately, the marketplace $schema URL currently redirects to 404; the current SchemaStore Claude Code marketplace schema is live. $schema is editor-only, so this is not a runtime blocker.

Evidence that passed

  • Exact diff: 2 files, +40/-12; no dependency, lockfile, executable, or runtime-source drift.
  • All 19 current plugin directories have a manifest, README, canonical SKILL, marketplace entry, and catalog row. The four changed marketplace descriptions exactly equal their manifests and contain no secret/PII material.
  • The tracked merge tree has no .DS_Store, Thumbs.db, node_modules, __pycache__, or .pyc under claude-code/plugins/.
  • Fresh post-merge CI on the exact SHA passed npm ci, lint, typecheck, repository tests, 19-plugin validation, 34-SKILL validation, pack dry-run, and gitleaks 8.24.3. The CI gitleaks job scanned the one merge commit and found no leaks; the PR-body full-tree gitleaks --no-git and pre-staging status checks remain author-asserted rather than attached CI evidence.
  • GitHub records a direct merge event by AojdevStudio at 2026-07-18T22:19:00Z, no auto-merge event, zero PR reviews, and a rate-limited CodeRabbit comment rather than a substantive review.

Narrow follow-up scope

  • Use one shared, typed YAML frontmatter parser in both validators; require description to be a trimmed non-empty string. Add table-driven fixtures for quoted/plain/folded valid values and null, collections, comment-only, malformed, and empty invalid values.
  • Validate every unambiguous local bundle reference, reject traversal, and assert the resolved path remains inside the plugin root; cover missing script/asset and path-escape fixtures.
  • Prevent Python bytecode creation during fixture validation and add a pack-content assertion excluding __pycache__ and *.py[cod].
  • Restore the ask-codex prerequisite in both manifest and marketplace text, and replace the dead $schema URL with https://json.schemastore.org/claude-code-marketplace.json.
VERDICT
MERGE_SHA: 3b9ed17d27b7d5776b2c90622d2e504ef14332a4
REVIEW_RANGE: d17b78ec540d7b3e903bb3aaa9c3fab116170e5d...3b9ed17d27b7d5776b2c90622d2e504ef14332a4
DIFF_INTEGRITY: PASS
CURRENT_PORT_DATA: PASS
VALIDATOR_CORRECTNESS: FAIL
MARKETPLACE_MANIFEST_SEMANTICS: FAIL
TEST_COVERAGE: FAIL
PUBLIC_PACKAGE_SAFETY: FAIL
EXACT_SHA_CI: PASS
ISSUE_10_SATISFIED: FAIL
DECISION: NEEDS_WORK

AojdevStudio added a commit that referenced this pull request Jul 19, 2026
…ckaging hygiene, ask-codex metadata) (#34)

* fix(validators): parse YAML frontmatter properly, contain bundle references

Post-merge review of #33 (issue #10 hardening) found two P1 gaps:

- readFrontmatterField/checkSkillFrontmatter tokenized frontmatter text
  instead of parsing YAML, so description: null/~/[]/{}  or a bare
  comment all passed the required-description gate as truthy strings.
  Both validate-plugin-ports.mjs and validate-agent-skills.mjs now share
  scripts/lib/frontmatter.mjs, which parses the block with the yaml
  package and requires a trimmed non-empty string.

- The SKILL.md reference guard skipped bare scripts/, cli/, assets/,
  hooks/ references entirely (issue #10 requires those shipped resources
  to exist) and joined paths without rejecting '..' or confirming
  containment, so a crafted tools/../../... reference could resolve to a
  file outside the plugin bundle. scripts/lib/bundle-refs.mjs now
  validates every unambiguous bundle reference, rejects traversal
  segments, and asserts the resolved path stays inside the plugin root.

Widening bare-reference validation surfaced two genuine false positives
(cross-skill delegation text in awesome-readme, a generic example in
harness-audit) — reworded both so they no longer read as ambiguous
same-bundle paths.

Added table-driven regression fixtures for both (scripts/lib/frontmatter-test.mjs,
scripts/lib/bundle-refs-test.mjs), wired into npm test.

* fix(packaging): stop generated Python bytecode from reaching the pack

Merge-SHA CI's npm run pack:dry included
codex/plugins/html-docs/scripts/__pycache__/markdown_to_html.cpython-312.pyc
(41.0 kB) — the html-docs fixture verifier imports markdown_to_html.py via
importlib during its own junk-artifact scan step in
validate-codex-plugins.mjs, and CPython writes the .pyc cache after that
scan runs, so it survives into a later npm run pack:dry.

- Pass PYTHONDONTWRITEBYTECODE=1 to the fixture-verifier subprocess so it
  is never generated in the first place.
- Add a pack-content assertion in validate-package-contents.mjs (already
  wired into npm test) that fails the build if any __pycache__/ or
  *.py[cod] path ever appears in the npm pack --dry-run file list.
- Add __pycache__/ and *.py[cod] to .gitignore for local git-status hygiene.

* fix(ask-codex): restore codex-binary prerequisite, fix dead schema URL

The rebase that squashed into #33 dropped 'Requires the codex binary on
PATH' from plugin.json and marketplace.json while README.md and
skills/ask-codex/SKILL.md kept it, breaking the four-surface consistency
issue #10 requires. Restore the identical sentence to both, keeping the
plugin.json/marketplace.json descriptions byte-for-byte equal (required
by validate-plugin-ports.mjs's marketplace description check).

Also replace marketplace.json's dead $schema
(https://anthropic.com/claude-code/marketplace.schema.json, 404) with
the live SchemaStore Claude Code marketplace schema. Editor-only, not a
runtime blocker, but free to fix alongside the rest of this scope.

* fix(validators): end-to-end reference extraction and filesystem containment

Two P1 gaps from the SHA-pinned re-review:

- REF_EXT excluded ordinary asset types (png, svg, jpg, jpeg, gif, webp),
  so a reference like assets/missing-logo.png never reached matching at
  all — the prior bundle-refs-test.mjs fixture called
  resolveBundleReference directly, never the actual regex extraction
  used in production, so it never proved the real path worked.
  extractBundleReferences moves into scripts/lib/bundle-refs.mjs
  (shared, testable) and widens REF_EXT to cover real asset extensions
  used in the repo's own plugins.

- A token-boundary bug let ${CLAUDE_PLUGIN_ROOT}/../tools/outside.ts get
  rematched as the bare reference tools/outside.ts, silently dropping
  the traversal prefix — a same-named file inside the bundle could then
  make an escaping reference pass. extractBundleReferences now requires
  a proper token boundary before a match (a negative lookbehind rejects
  matches preceded by a word/path character) and captures leading ../
  segments as part of the reference itself, so traversal is detected
  downstream instead of disappearing.

Also closes the 'contained file' gap: resolveBundleReference is pure
again (no filesystem access, matching the lib/ boundary rule), only '..'
segments count as traversal (a safe scripts/./gen.sh no longer false-
positives), and the filesystem-facing checks (existence, regular-file,
symlink real-path containment) move to a new exported
checkReferenceOnDisk in validate-plugin-ports.mjs — the validator, not
the lib. A directory or an escaping symlink now fails even when the
lexical path looks contained.

validate-plugin-ports.mjs gains a guarded main() so its exported
checkReferenceOnDisk can be imported by tests without running a full
validation pass against the real repo.

Extended bundle-refs-test.mjs with extractor+resolver integration cases
(the exact assets/missing-logo.png and CLAUDE_PLUGIN_ROOT-traversal
scenarios from the review, including a same-named-file-inside-bundle
adversarial case) and checkReferenceOnDisk fixtures (directory, symlink
escape, safe symlink, safe dot-segment). Verified via stash: these tests
fail against the pre-fix code (a SyntaxError on the missing export).

* fix(validators): reject near-miss frontmatter closing delimiters

The closing-delimiter search used indexOf("\n---"), which matches any
line merely *starting* with three dashes: ---not-a-delimiter, ----, and
--- trailing junk all silently closed the block early (or passed as a
'valid' close) instead of being rejected. Replace with a regex requiring
an exact standalone --- line (only trailing spaces/tabs before the
newline or end of string).

Added the three near-miss cases as negative fixtures. Verified via
stash: they fail against the pre-fix code with an actual assertion
failure ('Missing expected exception'), not just a load error.

* fix(awesome-readme): remove broken bash gen.sh commands, delegate explicitly

The prior round rewrote SKILL.md's bash scripts/gen.sh to bash gen.sh to
avoid a validator false positive, but that command never worked either:
no gen.sh exists at awesome-readme's own cwd (it belongs to the
delegated gpt-image-2 skill). The workflows still said bash
scripts/gen.sh, so SKILL.md and the workflows now disagreed on top of
both being wrong.

Remove the literal bash command blocks entirely in SKILL.md, Create.md,
and Improve.md and replace them with explicit parameter-list delegation
instructions to the gpt-image-2 skill, matching what the docs already
say to do ("invoke the gpt-image-2 skill directly"). This is a content
correctness fix, not a rewrite to dodge the reference matcher — with the
matcher already fixed to cover scripts/*.sh, a bare gen.sh path would
have been caught as either a false positive again or, if scoped to
scripts/, a real missing-file failure either way.

* fix(packaging): reorder pack-content gate, sync yaml into package-lock.json

npm test (which ran validate-package-contents.mjs) executed before
validate:plugins in the check script. The html-docs fixture verifier
that generates .pyc lives inside validate:plugins, so a
PYTHONDONTWRITEBYTECODE regression would pass the pack-content
assertion first and only produce the artifact afterward — a later
standalone npm run pack:dry would still exit zero with the bytecode
included. Move the assertion to run after validate:plugins instead.
Verified by manually reverting the PYTHONDONTWRITEBYTECODE fix: the
reordered check now fails in the same run with 'Forbidden generated
artifacts in package', where it previously would have passed silently.

Also regenerate package-lock.json via npm install --package-lock-only
so the yaml direct devDependency (added when the shared frontmatter
parser landed) is reflected in the root packages[""].devDependencies
block, matching package.json. CI's npm ci previously succeeded only
because yaml@2.8.3 was already present transitively; npm ci now runs
clean against the synced lockfile with a 2-line diff and no unrelated
drift.

* fix(validators): realpath every containment candidate, URL-safe CLI guard

Two P1 gaps from the SHA-pinned re-review of the prior remediation:

- checkReferenceOnDisk only realpath-checked when the *final* path
  component was itself a symlink (lstatSync().isSymbolicLink()). A
  symlinked *parent* directory resolves away silently during normal path
  resolution, so a plain regular file sitting behind one
  (scripts/linked-dir -> <outside>, scripts/linked-dir/payload.md) was
  seen as an ordinary contained file and returned {reason:"ok"}.
  realpathSync now runs unconditionally on every candidate, and a
  dangling or cyclic symlink (ENOENT/ELOOP) maps to a controlled
  "missing" result instead of an uncaught throw.

- The direct-execution guard compared `import.meta.url ===
  \`file://${process.argv[1]}\`` as raw strings. import.meta.url
  percent-encodes characters like spaces; the interpolated argv string
  does not, so a checkout path containing a space made the comparison
  silently false and main() never ran — `node
  validate-plugin-ports.mjs` would exit 0 having done nothing. Rebuilt
  the comparison as native filesystem paths instead of URL strings
  (fileURLToPath(import.meta.url) vs realpathSync(argv[1])), which also
  fixes a second issue the first attempt at this introduced: macOS
  resolves /tmp to /private/tmp, and import.meta.url reflects that
  resolved path while path.resolve() alone does not follow the symlink.

Added symlinked-parent-directory-escape, dangling-symlink, and
cyclic-symlink fixtures to bundle-refs-test.mjs, and a new
cli-entrypoint-test.mjs that copies the real script into a temp
directory whose path contains a space and runs it as a subprocess,
asserting main() actually executed (non-zero exit, 'plugins directory
not found' on stderr) rather than silently exiting 0. Verified via
stash: the symlinked-parent case returns {reason:"ok"} and the
space-path smoke test exits 0 with no output against the pre-fix code.

* fix(validators): require a terminal boundary, capture safe leading ./

Two more production-shape gaps in extractBundleReferences from the
SHA-pinned re-review (also flagged independently by CodeRabbit):

- The regex had a leading token boundary but no trailing one, so a
  coincidental prefix of a longer filename truncated into a match:
  assets/logo.png.bak extracted assets/logo.png, because the greedy
  path-character class backtracks to find any valid extension match
  rather than requiring the match to reach the actual end of the token.
  A same-prefixed real file could then validate text that was never
  actually a supported asset reference. Added a trailing negative
  lookahead so nothing word/dot/hyphen-like may immediately follow the
  matched extension.

- ./scripts/gen.sh extracted nothing: the leading-boundary lookbehind
  correctly blocks resynchronization after a slash, but that also meant
  a safe, common ./ same-directory prefix was never recognized as part
  of a reference at all, a false negative on legitimate content.
  Widened the existing leading-segment group (already used to preserve
  traversal prefixes) so it captures ./ the same way it already
  captures ../.

Extended bundle-refs-test.mjs (already touched in the prior commit) with
the exact assets/logo.png.bak and ./scripts/gen.sh cases from the
review, plus an end-to-end fixture proving a real assets/logo.png file
in the bundle still can't be coincidentally matched by the .bak text.
Verified via stash: assets/logo.png.bak produces 1 match (not 0) against
the pre-fix regex.

* docs(awesome-readme): add --ref example to Improve.md (CodeRabbit)

Create.md and SKILL.md both document the optional --ref flag for
reference-image conditioning; Improve.md, which handles existing
READMEs where a user might want to restyle an existing logo or diagram,
did not. Add the same one-line guidance for consistency.

* fix(validators): include / in the terminal reference boundary

The terminal negative lookahead added for the .bak truncation bug was
(?![\w.-]), omitting / even though / is itself part of the accepted
path-character class. That left a second truncation path open:
assets/logo.png/extra still extracted assets/logo.png, because a
following slash satisfied the lookahead. If assets/logo.png exists,
the validator approves that truncated prefix and never sees the actual
longer token.

Add / to the lookahead: (?![\w./-]). This is exactly what CodeRabbit's
now-resolved thread proposed; the prior commit implemented the
word/dot/hyphen half but dropped the slash.

Added unit fixtures for assets/logo.pngx (no separator at all) and
assets/logo.png/child.bin (the slash-tail case), plus an end-to-end
production-path fixture proving the pipeline never extracts a match for
the nested-path text even with a real assets/logo.png file present in
the bundle. Verified via stash: the new slash-tail fixture extracts 1
match (not 0) against the pre-fix regex.
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.

Audit and harden Claude Code plugin ports

1 participant