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-pluginsmarketplace takingSKILL.mdcontributions by merge request, that attack surface is now ours to gate. This component is the gate, and is reusable by anyone shipping skills, plugins, orCLAUDE.md/.cursorrulesfiles.
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).