Skip to content

fix(plugins): post-merge remediation for #33 (validator hardening, packaging hygiene, ask-codex metadata) - #34

Merged
AojdevStudio merged 11 commits into
mainfrom
fix/issue-10-33-post-merge-hardening
Jul 19, 2026
Merged

AojdevStudio merged 11 commits into
mainfrom
fix/issue-10-33-post-merge-hardening

Conversation

@AojdevStudio

@AojdevStudio AojdevStudio commented Jul 18, 2026 •

Copy link
Copy Markdown
Owner

Post-merge remediation for #33

Follow-up to #33 (issue #10 plugin-port hardening). A heightened post-merge review of the merged squash range found four bounded blockers in the shipped validators/metadata. This PR closes all of them; no other content changed.

Review: #33 (comment)

1. Validator YAML/description false negatives (P1)

readFrontmatterField/checkSkillFrontmatter in validate-plugin-ports.mjs tokenized frontmatter text instead of parsing YAML, so description: null, ~, [], {}, and a bare comment all passed the required-description gate as truthy strings. validate-agent-skills.mjs had the same class of bug.

Both validators now share scripts/lib/frontmatter.mjs, which parses the frontmatter block with the yaml package and requires a trimmed non-empty string via isNonEmptyString. Table-driven fixtures in scripts/lib/frontmatter-test.mjs cover quoted/plain/folded valid values and null/collection/comment-only/malformed/empty invalid values, wired into npm test.

2. Bundle-reference coverage gap and path traversal (P1)

The SKILL.md reference guard deliberately skipped bare scripts/, cli/, assets/, hooks/ references even though issue #10's acceptance criteria explicitly require those shipped resources to exist, and it 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: awesome-readme's scripts/gen.sh is a cross-skill delegation to the external gpt-image-2 skill, and harness-audit's ./scripts/test.sh is a generic illustrative example in a readiness rubric. Both are real, not bugs, so I reworded the two lines minimally so they no longer read as ambiguous same-bundle paths rather than suppressing the check. Fixtures for missing script/asset and path-escape cases are in scripts/lib/bundle-refs-test.mjs, wired into npm test.

3. Generated Python bytecode reaching the package (P1)

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, and CPython writes the .pyc cache after that scan runs, so it survives into a later npm run pack:dry.

  • validate-codex-plugins.mjs now passes PYTHONDONTWRITEBYTECODE=1 to the fixture-verifier subprocess so the cache is never generated.
  • validate-package-contents.mjs (already wired into npm test) now asserts no __pycache__/ or *.py[cod] path ever appears in the npm pack --dry-run file list.
  • .gitignore gained __pycache__/ and *.py[cod] for local git-status hygiene.

4. ask-codex metadata drift (P2)

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. Restored the identical sentence to both manifest and marketplace entry (kept byte-for-byte equal, per validate-plugin-ports.mjs's exact-match check). Also replaced marketplace.json's dead $schema (https://anthropic.com/claude-code/marketplace.schema.json, confirmed 404) with the live SchemaStore Claude Code marketplace schema (https://json.schemastore.org/claude-code-marketplace.json, confirmed valid draft-07 schema). Editor-only, not a runtime blocker.

Evidence

All gates run with bun/bunx at head bbd7dce90276af0309843324f4d4fcb4abcd722a:

  • bun install: clean
  • bun run check (lint, typecheck, tests including the two new fixture suites, validate:plugins 19/19, validate:skills 34 SKILL.md files, list-resources): exit 0
  • bun run pack:dry: exit 0, 232 files, no __pycache__/.pyc in the listing
  • gitleaks detect --no-git --config .gitleaks.toml against the exact diff range (origin/main..HEAD, excluding the untracked local bun.lock): no leaks
  • gitleaks protect --staged: no leaks (ran on each of the 3 commits via the repo's own pre-commit hook)
  • pi -ne -e .: exit 0, all 10 repository extensions load cleanly (-ne avoids this operator's machine-local global-extension-collision quirk documented separately in build(deps): bump ws from 8.20.0 to 8.21.1 in the npm_and_yarn group across 1 directory #31; unrelated to this PR's content)

Verified the two widened false-positive fixtures fail without the content rewording and pass with it; verified the pack-content assertion actually catches a manually-planted __pycache__ file before removing it.

Not merged, per assignment scope.

Refs #33, #10.

Summary by CodeRabbit

  • New Features

    • README workflows now generate visuals through the installed gpt-image-2 skill, including support for prompts and reference images.
    • Plugin documentation now clearly states that the codex command must be available on PATH.
  • Bug Fixes

    • Improved validation of frontmatter, bundle references, symlinks, and path traversal.
    • Package checks now detect unwanted Python cache and compiled artifacts.
  • Tests

    • Added coverage for frontmatter parsing and bundle-reference validation.

…rences

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.
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.
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.
@changeset-bot

changeset-bot Bot commented Jul 18, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: a921591

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

Review Change Stack

Warning

Review limit reached

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

Next review available in: 25 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: b33f028d-7851-452e-a70f-4de887b8f390

📥 Commits

Reviewing files that changed from the base of the PR and between 256d498 and a921591.

📒 Files selected for processing (6)
  • claude-code/plugins/awesome-readme/skills/awesome-readme/workflows/Improve.md
  • package.json
  • scripts/cli-entrypoint-test.mjs
  • scripts/lib/bundle-refs-test.mjs
  • scripts/lib/bundle-refs.mjs
  • scripts/validate-plugin-ports.mjs

Walkthrough

The pull request adds shared YAML frontmatter and bundle-reference utilities, strengthens skill and plugin validation, detects generated Python artifacts in packages, updates validation scripts, and revises plugin metadata and README image-generation instructions.

Changes

Validation and plugin workflows

Layer / File(s) Summary
Shared parsing and reference utilities
scripts/lib/frontmatter.mjs, scripts/lib/bundle-refs.mjs, scripts/lib/*-test.mjs, package.json
Adds YAML frontmatter parsing, bundle-reference extraction and lexical resolution, filesystem-boundary tests, and the yaml development dependency.
Validator integration and safety checks
scripts/validate-agent-skills.mjs, scripts/validate-plugin-ports.mjs
Reuses shared parsers, validates non-empty metadata, rejects traversal and escaping symlinks, and makes plugin-port validation safe to import.
Package integrity and generated-artifact checks
scripts/validate-package-contents.mjs, scripts/validate-codex-plugins.mjs, .gitignore, package.json
Adds forbidden Python artifact checks, prevents fixture bytecode generation, ignores Python cache files, and wires checks and tests into package scripts.
Plugin metadata and workflow instructions
.claude-plugin/marketplace.json, claude-code/plugins/ask-codex/.claude-plugin/plugin.json, claude-code/plugins/awesome-readme/..., claude-code/plugins/harness-audit/...
Updates schema and plugin metadata, switches README image-generation guidance to direct gpt-image-2 invocations, and revises a harness-audit test command example.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SkillValidator
  participant FrontmatterParser
  participant ReferenceUtilities
  participant PluginFilesystem
  SkillValidator->>FrontmatterParser: Parse SKILL.md frontmatter
  FrontmatterParser-->>SkillValidator: Return name and description
  SkillValidator->>ReferenceUtilities: Extract and resolve bundle references
  ReferenceUtilities->>PluginFilesystem: Check file and symlink containment
  PluginFilesystem-->>SkillValidator: Return validation result
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: validator hardening, packaging hygiene, and ask-codex metadata updates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-10-33-post-merge-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

Copy link
Copy Markdown
Owner Author

SHA-pinned remediation review

Reviewed exact range 3b9ed17d27b7d5776b2c90622d2e504ef14332a4...bbd7dce90276af0309843324f4d4fcb4abcd722a. The original typed-YAML values, current Python-bytecode output, and ask-codex metadata are fixed, but several blocker paths remain.

Findings

  1. P1 — the reference fix is not end-to-end, so the advertised negative fixture never reaches production matching. REF_EXT excludes ordinary asset types (png, svg, etc.). I reproduced assets/missing-logo.png producing zero matches, even though the fixture lists that exact path as covered in bundle-refs-test.mjs; the test calls only the resolver, not the production extractor. I also reproduced ${CLAUDE_PLUGIN_ROOT}/../tools/outside.ts being rematched as bare tools/outside.ts, dropping the traversal prefix. A same-named local file can therefore make an escaping reference pass. Fix: move extraction into the shared library, enforce token boundaries, cover all shipped resource path forms, and test extractor + resolver together for both cases.

  2. P1 — “contained file” is not established. resolveBundleReference performs lexical containment and existsSync only. A directory named tool.ts passes; a symlink inside the bundle can resolve outside it. Conversely, safe normalization such as scripts/./gen.sh is deliberately rejected as traversal. The module also violates the documented hard lib/ boundary (“no filesystem”). Fix: keep normalization/containment pure, require a regular file at the validator boundary, and realpath-check or reject symlinks; add directory, symlink-escape, and safe-. fixtures.

  3. P1 — malformed frontmatter closing delimiters still pass. indexOf("\n---") accepts ---not-a-delimiter, ----, and --- trailing junk as closers. I reproduced all three returning a valid mapping. The new malformed fixtures omit this near-match. Fix: locate an exact --- delimiter line and add near-match negatives. The new yaml parse plus isNonEmptyString otherwise correctly rejects null, ~, collections, comment-only, empty, whitespace-only, and malformed YAML.

  4. P1 — avoiding the heuristic created a public command regression. awesome-readme now tells users to run bash gen.sh, but no gen.sh exists at the caller cwd; the delegated gpt-image-2 skill and awesome-readme workflow still use bash scripts/gen.sh. Fix: remove the copied shell command and explicitly delegate to the installed gpt-image-2 skill, or document one actually resolvable invocation. Do not rewrite user-facing instructions merely to evade the matcher.

  5. P2 — two regression/lock gates are incomplete. The bytecode assertion runs inside npm test, which precedes validate:plugins; the fixture verifier that formerly generated .pyc runs afterward. If PYTHONDONTWRITEBYTECODE regresses, the assertion passes first, the verifier creates bytecode, and the final npm pack --dry-run still exits zero with the artifact. Move the package-content assertion after plugin validation (or validate the final JSON pack list). Also, package.json adds direct dev dependency yaml, but the unchanged package-lock.json root specifiers omit it; CI succeeds only because yaml@2.8.3 is already hoisted transitively. Regenerate the npm lock with no unrelated drift.

Passing evidence

  • Exact-head CI run 29664335976 is green: npm ci, lint, typecheck, both new fixture scripts, 19-plugin validation, 34-SKILL validation, and gitleaks 8.24.3 over all three commits.
  • Current CI pack output is 232 files with no __pycache__ or .pyc; PYTHONDONTWRITEBYTECODE=1, .gitignore, and the pack-list rejection are directionally correct.
  • ask-codex marketplace/manifest descriptions are byte-identical and both restore “Requires the codex binary on PATH”; README and SKILL retain the same prerequisite.
  • The SchemaStore marketplace schema resolves successfully and matches current Claude Code documentation.
  • No secrets/PII, lock resolution bytes, or unrelated runtime code entered the diff. Bun results are present only as author-asserted PR-body evidence; CI runs the canonical npm gates, not Bun.
VERDICT
HEAD_SHA: bbd7dce90276af0309843324f4d4fcb4abcd722a
REVIEW_RANGE: 3b9ed17d27b7d5776b2c90622d2e504ef14332a4...bbd7dce90276af0309843324f4d4fcb4abcd722a
YAML_TYPED_VALUES: PASS
FRONTMATTER_DELIMITERS: FAIL
REFERENCE_EXTRACTION: FAIL
REFERENCE_CONTAINMENT: FAIL
NEGATIVE_FIXTURES: FAIL
CURRENT_PACK_CONTENTS: PASS
PYC_REGRESSION_GATE: FAIL
ASK_CODEX_METADATA: PASS
DEPENDENCY_LOCK: FAIL
PUBLIC_SAFETY: FAIL
BUN_GATES: UNVERIFIED
EXACT_SHA_CI: PASS
DECISION: NEEDS_WORK

…inment

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).
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.
…licitly

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.
…k.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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
claude-code/plugins/awesome-readme/skills/awesome-readme/workflows/Improve.md (1)

133-139: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider adding the --ref example for consistency.

Create.md and SKILL.md document the optional --ref flag for reference-image conditioning. Since the Improve workflow handles existing READMEs where a user might want to restyle an existing logo or diagram, it could be helpful to include the --ref guidance here as well.

💡 Proposed optional addition
 - Comparison visual (if applicable): `--prompt "Side-by-side comparison: '<A>' vs '<B>'. Two columns, contrasting treatment, clear winner indicated." --out docs/comparison.png`
 
+For reference-conditioned remixes, also pass `--ref /absolute/path/to/reference.png`.
+
 Pass the user's prompt through raw. Only polish wording when they explicitly asked for it. Reference each output in the README with descriptive alt text. If image generation fails, name the failing layer in one sentence and ask the user how to proceed; do not silently fall back to a weaker model.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@claude-code/plugins/awesome-readme/skills/awesome-readme/workflows/Improve.md`
around lines 133 - 139, Update the image-generation guidance in the Improve
workflow to document the optional --ref flag for reference-image conditioning,
matching the existing guidance in Create.md and SKILL.md. Include it alongside
the architecture, workflow, and comparison examples without changing the
raw-prompt or failure-handling instructions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/lib/bundle-refs.mjs`:
- Around line 13-17: Update the RegExp in extractBundleReferences to add a
negative lookahead immediately after the extension, preventing matches when
additional word or filename characters follow it (such as .bak or x). Add a
regression fixture covering these trailing-character cases and verify valid
references remain matched.

In `@scripts/validate-plugin-ports.mjs`:
- Around line 118-146: Update checkReferenceOnDisk to perform realpath
containment for every resolved target within the guarded filesystem operation,
not only when the final entry is a symlink. Handle realpathSync/statSync
failures from dangling or cyclic symlinks by returning the appropriate
validation reason instead of throwing, while preserving regular-file and
symlink-escape rejection. Add a regression test covering a target reached
through an escaping intermediate-directory symlink.

---

Nitpick comments:
In
`@claude-code/plugins/awesome-readme/skills/awesome-readme/workflows/Improve.md`:
- Around line 133-139: Update the image-generation guidance in the Improve
workflow to document the optional --ref flag for reference-image conditioning,
matching the existing guidance in Create.md and SKILL.md. Include it alongside
the architecture, workflow, and comparison examples without changing the
raw-prompt or failure-handling instructions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2ab00732-2829-43c8-8ead-aa185d65f693

📥 Commits

Reviewing files that changed from the base of the PR and between 3b9ed17 and 256d498.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json, !package-lock.json
📒 Files selected for processing (16)
  • .claude-plugin/marketplace.json
  • .gitignore
  • claude-code/plugins/ask-codex/.claude-plugin/plugin.json
  • claude-code/plugins/awesome-readme/skills/awesome-readme/SKILL.md
  • claude-code/plugins/awesome-readme/skills/awesome-readme/workflows/Create.md
  • claude-code/plugins/awesome-readme/skills/awesome-readme/workflows/Improve.md
  • claude-code/plugins/harness-audit/skills/harness-audit/SKILL.md
  • package.json
  • scripts/lib/bundle-refs-test.mjs
  • scripts/lib/bundle-refs.mjs
  • scripts/lib/frontmatter-test.mjs
  • scripts/lib/frontmatter.mjs
  • scripts/validate-agent-skills.mjs
  • scripts/validate-codex-plugins.mjs
  • scripts/validate-package-contents.mjs
  • scripts/validate-plugin-ports.mjs

Comment thread scripts/lib/bundle-refs.mjs
Comment thread scripts/validate-plugin-ports.mjs Outdated
@AojdevStudio

Copy link
Copy Markdown
Owner Author

SHA-pinned fix re-review

Reviewed exact remediation delta bbd7dce90276af0309843324f4d4fcb4abcd722a...256d4988571b3a26d8195541af1fb26b6e471b6c. Several prior gates are now genuinely closed, but the containment claim and production entrypoint are still unsafe.

Blocking findings

  1. P1 — realpath containment is still bypassable through a symlinked parent directory. checkReferenceOnDisk calls realpathContained only when lstatSync(resolved).isSymbolicLink(). I exercised that exported production function under Bun 1.3.7 with scripts/linked-dir -> <outside-dir> and scripts/linked-dir/payload.md: the final entry was seen as a regular file and returned {reason:"ok"} even though realpathSync(resolved) was outside the bundle. A dangling final symlink also threw ENOENT instead of producing a controlled validation failure. The submitted fixtures cover only final-component symlinks. Narrow fix: realpath-check every existing candidate inside the guarded filesystem block, then require the real target to be a regular file; map dangling/cyclic-link failures to a validation reason. Add escaping-parent-directory, dangling-link, and cyclic-link production-path fixtures.

  2. P1 — the new CLI guard can silently skip the validator. if (import.meta.url === file://${process.argv[1]}) is not URL-safe: a checkout path containing a space is percent-encoded in import.meta.url but not in the interpolated string, and Windows drive/backslash paths also cannot compare equal. Direct node scripts/validate-plugin-ports.mjs then exits successfully without running main(). Narrow fix: compare with pathToFileURL(path.resolve(process.argv[1])).href (or compare normalized filesystem paths via fileURLToPath) and add a direct-entrypoint smoke test from a path containing a space.

  3. P1 — reference extraction still has both a terminal partial-match false positive and a safe-prefix false negative. The production regex has a leading boundary but no trailing boundary. I reproduced assets/logo.png.bak extracting assets/logo.png; a coincidental prefix file can therefore validate text that is not actually a supported asset reference. I also reproduced ./scripts/gen.sh extracting nothing, because the leading / blocks resynchronization but optional safe ./ is not captured. The new tests cover xtools/..., PNG extraction, and leading ../, but not these production shapes. Narrow fix: require a token boundary after the full extension, explicitly capture optional leading ./, and add end-to-end fixtures with coincidental prefix files.

Verified closed / evidence

  • Exact delimiter matching rejects ---not-a-delimiter, ----, and --- trailing junk; typed YAML cases pass.
  • ${CLAUDE_PLUGIN_ROOT}/../tools/outside.ts retains its traversal prefix and rejects even with a same-named local decoy; ordinary PNG asset extraction works.
  • Awesome-readme no longer exposes broken shell commands; it delegates to the installed gpt-image-2 skill across SKILL/Create/Improve.
  • The package-content assertion now executes after validate:plugins; exact-head pack output is 232 files with no __pycache__/.pyc.
  • The lock delta is only the root "yaml": "^2.8.2" devDependency metadata. Existing yaml@2.8.3 resolution and SHA-512 integrity are byte-identical; no other dependency drift.
  • Fresh exact-head GitHub Actions are green: PR run 29665172594 and push run 29665171410, including gitleaks 8.24.3 with no leaks.
  • Independent exact-head Bun 1.3.7 dispatch passed lint, typecheck, all tests, both plugin validators, final package-content validation, 34-SKILL validation, resource listing, and dry pack. bun run pack:dry necessarily enters the repository's committed npm pack script.
  • Process violation recorded: the implementer used npm install --package-lock-only/npm ci, and the PR body’s claimed Bun evidence remains pinned to stale head bbd7dce.... I did not rely on that evidence; the independent exact-head Bun-dispatched run above is the replacement evidence.
VERDICT
HEAD_SHA: 256d4988571b3a26d8195541af1fb26b6e471b6c
REVIEW_RANGE: bbd7dce90276af0309843324f4d4fcb4abcd722a...256d4988571b3a26d8195541af1fb26b6e471b6c
FRONTMATTER_DELIMITERS: PASS
REFERENCE_LEADING_BOUNDARY: PASS
LEADING_TRAVERSAL_DETECTION: PASS
REFERENCE_TERMINAL_BOUNDARY: FAIL
SAFE_DOT_PREFIX_COVERAGE: FAIL
REALPATH_CONTAINMENT: FAIL
BROKEN_SYMLINK_HANDLING: FAIL
CLI_ENTRYPOINT_COMPATIBILITY: FAIL
PUBLIC_COMMANDS: PASS
PACK_GATE_ORDER: PASS
PACK_CONTENTS: PASS
DEPENDENCY_LOCK: PASS
PUBLIC_SAFETY: FAIL
IMPLEMENTER_BUN_ONLY_PROCESS: FAIL
INDEPENDENT_BUN_DISPATCHED_VALIDATION: PASS
EXACT_SHA_CI: PASS
DECISION: NEEDS_WORK

…uard

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.
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.
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.
@AojdevStudio

Copy link
Copy Markdown
Owner Author

SHA-pinned final re-review

Reviewed the exact three-commit delta 256d4988571b3a26d8195541af1fb26b6e471b6c...da24a27254df583067e5abb73f6a927f9e5a21b2. Five requested remediations are closed, but the terminal-boundary fix is still one character class short of the production invariant.

Remaining blocker

P1 — the terminal regex boundary still permits slash-tail truncation. extractBundleReferences ends with (?![\w.-]), omitting / even though / is part of the accepted path alphabet. Under Bun 1.3.7 I reproduced:

assets/logo.png.bak   -> []
assets/logo.pngx      -> []
assets/logo.png/extra -> [{ pathPart: "assets/logo.png", ... }]

If assets/logo.png exists, the validator approves that truncated prefix and never detects the actual longer token. This is the same partial-match class as .png.bak. CodeRabbit’s now-resolved thread explicitly proposed (?![\w./-]), but the committed implementation dropped /, and the new fixtures cover .bak rather than a slash tail. Narrow fix: include / in the terminal negative lookahead and add one end-to-end coincidental-file fixture for assets/logo.png/child.bin (or equivalent).

Verified closed / evidence

  • Production checkReferenceOnDisk now realpaths every candidate. Independent probes returned symlink-escape for a regular file behind an escaping parent-directory symlink and controlled missing results for dangling and cyclic links; no throws.
  • The production CLI guard compares decoded/native realpaths. The new subprocess fixture copies the real validator into a path containing a space and proves main() executes. The comparison is separator/URL-safe and statically Windows-compatible; CI itself remains Ubuntu-only.
  • ./scripts/gen.sh extracts and resolves successfully; ${CLAUDE_PLUGIN_ROOT}/../tools/outside.ts retains ../ and rejects as traversal.
  • Improve.md now includes the consistent --ref /absolute/path/to/reference.png guidance; no broken public shell command was reintroduced.
  • The adversarial symlink, traversal, safe-./, .bak, and CLI-space fixtures import or execute production code paths. The only fixture gap is the slash-tail case above.
  • Both CodeRabbit threads are isResolved: true, bot-acknowledged, and CodeRabbit status is green. The symlink evidence is complete; the terminal-boundary evidence is incomplete because the resolved thread’s / condition was not implemented.
  • Independent exact-head Bun 1.3.7 dispatch passed lint, typecheck, all tests (including bundle and CLI fixtures), 19-plugin validation, 34-SKILL validation, resource listing, and the production package-content assertion. A pure bun pm pack --dry-run --ignore-scripts produced 233 files with no __pycache__ or .py[cod] entries.
  • Fresh exact-head CI is green: PR run 29665859499 and push run 29665858284, plus gitleaks and GitGuardian. CI’s configured package-manager path is still npm; the Bun evidence above is the independent exact-head verification.
  • The three-commit delta changes only the validator/tests, test script registration, and the --ref documentation line. No lockfile/dependency drift, generated bytecode, secrets, or unrelated public content.
VERDICT
HEAD_SHA: da24a27254df583067e5abb73f6a927f9e5a21b2
REVIEW_RANGE: 256d4988571b3a26d8195541af1fb26b6e471b6c...da24a27254df583067e5abb73f6a927f9e5a21b2
SYMLINKED_PARENT_CONTAINMENT: PASS
DANGLING_SYMLINK_HANDLING: PASS
CYCLIC_SYMLINK_HANDLING: PASS
CLI_SPACE_PATH_EXECUTION: PASS
CLI_CROSS_PLATFORM_NORMALIZATION: PASS
LEADING_DOT_SLASH_EXTRACTION: PASS
LEADING_TRAVERSAL_DETECTION: PASS
TERMINAL_DOT_WORD_BOUNDARY: PASS
TERMINAL_SLASH_BOUNDARY: FAIL
PRODUCTION_PATH_FIXTURES: FAIL
REF_DOCUMENTATION: PASS
CODERABBIT_THREADS_RESOLVED: PASS
CODERABBIT_EVIDENCE_COMPLETE: FAIL
INDEPENDENT_BUN_GATES: PASS
PACK_HYGIENE: PASS
EXACT_SHA_CI: PASS
PUBLIC_SAFETY: FAIL
DECISION: NEEDS_WORK

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.
@AojdevStudio

Copy link
Copy Markdown
Owner Author

SHA-pinned final boundary re-review

Reviewed exact delta da24a27254df583067e5abb73f6a927f9e5a21b2...a9215910dbecf37ac75db3c9fddd8c09e1bf67c9. No blocking findings remain.

Boundary verification

  • extractBundleReferences now uses the complete terminal lookahead (?![\w./-]); slash is included.
  • Independent production-extractor probes under Bun 1.3.7 returned zero matches for assets/logo.png.bak, assets/logo.pngx, and assets/logo.png/extra.
  • Valid assets/logo.png still extracts and resolves inside the bundle. ./scripts/gen.sh remains a valid leading-dot reference. ${CLAUDE_PLUGIN_ROOT}/../tools/outside.ts retains ../ and rejects as traversal.
  • bundle-refs-test.mjs exercises the production extractor for all three suffix shapes and includes the adversarial coincidental-file case: a real assets/logo.png cannot make assets/logo.png/child.bin pass. Existing end-to-end valid, leading-./, traversal, regular-file, parent-symlink, dangling-link, and cyclic-link cases remain green.

Full gate evidence

  • All prior remediation remains closed: typed YAML and exact frontmatter delimiters; complete resource extraction; lexical plus realpath containment; controlled dangling/cyclic handling; URL/space-safe CLI execution; executable gpt-image-2 delegation and --ref guidance; ask-codex compatibility metadata; post-generation package assertion; synchronized YAML lock metadata.
  • Exact-head Bun 1.3.7 passed lint, typecheck, all fixture suites, 19-plugin validation, 34-SKILL validation, resource listing, and the production package-content assertion.
  • Pure bun pm pack --dry-run --ignore-scripts produced 233 files with no __pycache__ or .py[cod] entries.
  • Both CodeRabbit threads remain resolved with bot acknowledgements; CodeRabbit and GitGuardian are green with no new review threads.
  • Fresh exact-head CI is green: PR run 29666167460 and push run 29666166097, including gitleaks, full checks, and the 233-file dry pack.
  • Delta scope is exactly one commit and two validator/test files. No dependency, lockfile, metadata, secret, generated-artifact, or unrelated public-content drift.
VERDICT
HEAD_SHA: a9215910dbecf37ac75db3c9fddd8c09e1bf67c9
REVIEW_RANGE: da24a27254df583067e5abb73f6a927f9e5a21b2...a9215910dbecf37ac75db3c9fddd8c09e1bf67c9
TERMINAL_LOOKAHEAD_INCLUDES_SLASH: PASS
PNG_BAK_NO_TRUNCATION: PASS
PNGX_NO_TRUNCATION: PASS
PNG_SLASH_TAIL_NO_TRUNCATION: PASS
VALID_REFERENCE_EXTRACTION: PASS
LEADING_DOT_SLASH_EXTRACTION: PASS
LEADING_TRAVERSAL_DETECTION: PASS
PRODUCTION_PATH_FIXTURES: PASS
PRIOR_REMEDIATION_GATES: PASS
CODERABBIT_THREADS: PASS
INDEPENDENT_BUN_GATES: PASS
PACK_HYGIENE: PASS
EXACT_SHA_CI: PASS
PUBLIC_SAFETY: PASS
DECISION: MERGE_READY

@AojdevStudio
AojdevStudio merged commit 232fd85 into main Jul 19, 2026
6 checks passed
@AojdevStudio
AojdevStudio deleted the fix/issue-10-33-post-merge-hardening branch July 19, 2026 00:03
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.

1 participant