Skip to content

Spec: phpboyscout/cicd v0.19.0 — skill-security

  • Repository: gitlab.com/phpboyscout/cicd
  • Released as: v0.19.0 (minor — new component / feat).
  • Driver: AI instruction files are source code an agent reads and executes, but they live in repos as "documentation" with none of the scopes or sandboxing a language interpreter has. A markdown file can carry instructions that are invisible in a diff (zero-width characters) or reordered to deceive a reviewer (Trojan-Source bidirectional overrides). With the new gitlab.com/phpboyscout/claude-code-plugins marketplace taking SKILL.md contributions by merge request, that attack surface is now ours to gate. This component is the gate, and is reusable by anyone shipping skills, plugins, or CLAUDE.md/.cursorrules files.

Decisions

D1 — Four scanners, one job each, individually opt-out-able

Following the *-security family (go-security, svelte-security), each check is its own job so a consumer disables one with <job>: { rules: [{ when: never }] } rather than dropping the component. All jobs are MR-only by default via inputs.if, carry the leading schedule → never guard, and are interruptible: true.

D2 — hidden-chars: HARD FAIL

Scans the files matched by inputs.paths (default **/*.md) for characters that hide intent from a human reviewer:

  • zero-width / invisible (U+200B–200D, U+2060, U+FEFF, soft hyphen, the Hangul fillers, invisible math operators, …),
  • bidirectional overrides (U+202A–202E, U+2066–2069, U+200E/F, U+061C) — the Trojan-Source class,
  • line / paragraph separators (U+2028/2029) and other C0/C1 control characters (excluding tab, LF, CR).

Any hit fails the pipeline. These have no legitimate place in a skill or instruction file, so a hard fail is unambiguous and carries no false-positive cost on normal prose or code fences (verified: the scanner runs clean over all of claude-code-plugins, while catching a planted U+200B and U+202E).

D3 — injection-scan: WARN (allow_failure: true)

Heuristic patterns in instruction files (curl|sh, base64 -d | sh, eval $(…), "ignore previous instructions", exfil-of-secret phrasing). These are genuinely suspicious but legitimate skills do contain shell commands, so this is a warning for a human to review, not a gate. Kept deliberately tight to avoid flagging ordinary grep/git usage.

D4 — plugin-schema: HARD FAIL, no-op when empty

Validates a Claude plugin marketplace: marketplace.json (name, owner.name, plugins[] each with name + source), every plugin.json (name), and every SKILL.md frontmatter (description required; and only the allowed keys — name, description, disable-model-invocation, allowed-tools, requires-trust — an unknown key is both a load-time problem and a smuggling smell). A repo with no marketplace/skill files simply reports nothing checked and passes, so the component is safe to include in a plain CLAUDE.md repo for the hidden-chars value alone.

D5 — gitleaks: HARD FAIL, scoped

Byte-identical to the *-security family: scans the MR's own commit range ($CI_MERGE_REQUEST_DIFF_BASE_SHA..$CI_COMMIT_SHA) to stay safe on a shared runner, full-history fallback outside an MR.

D6 — Custom scanners on a pinned python image, standard library only

hidden-chars, injection-scan, and plugin-schema run on inputs.python_image (default python:3.13-slim) and use only the Python standard library — no pip install, so the jobs are fast and have no supply-chain surface of their own. paths is a string of space-separated recursive globs (per the path-list-as-string convention). gitleaks keeps its upstream image. All images are per-input for Renovate.

Self-test

tests/skill-security/ ships a minimal clean fixture, a one-plugin marketplace with a valid SKILL.md, and a child pipeline that runs all custom scanners against it (overriding inputs.if to fire under parent_pipeline, scoping paths to the fixture, and opting gitleaks out, as it is identical to and covered by the other security components). The positive path (clean input passes) matches the repo's self-test convention; the detection path (dirty input fails) was verified at author time against planted zero-width and bidi characters, and a CI negative fixture is a possible future addition.

Versioning

v0.19.0 — minor: a new component grows the surface (the *-security family precedent, e.g. svelte-security at v0.17.0).