Rules / CFG074
CFG074 skills-lock.json pulls skill content from an unpinned source
Severity: warn
OWASP: LLM04:2026 – Supply Chain
OWASP Agentic: ASI04:2026 – Agentic Supply Chain Vulnerabilities: LLM04:2026 now covers the model and data supply chain and refers the agentic one, "including MCP servers and tool registries", to ASI04
What cfgaudit checks
The vercel-labs/skills CLI (skills.sh) installs agent skills — instruction text the agent loads into its trusted context — and records where each came from in a skills-lock.json lock file at the repo root. cfgaudit already scans installed skill content (.claude/skills/*/SKILL.md → CFG024/026/029–036, CFG051/056); CFG074 covers the lock file, where the trust decision is committed.
cfgaudit flags an entry whose source is remote but carries no integrity pin at all. The skills CLI normally records integrity when it installs a skill, in one of a few fields depending on its version — any of these pins the content (the upstream cannot silently change what was installed), so they are not flagged:
computedHash— a SHA-256 of the fetched skill content (the dominant v1-schema pin);commit— the resolved commit SHA (40- or 64-hex);integrity— an SRI-style content hash (sha256-…);refthat is itself a full commit SHA.
Only an entry with a remote source and none of the above (a bare branch/tag ref, or just source + sourceType) is reported — the fetched skill (instruction) text is unverified, so whoever controls the upstream repo can change what every contributor's agent reads on the next sync. This is the skills-CLI analogue of an unpinned MCP package (CFG017) or an auto-loaded plugin source.
| Entry | Result |
|---|---|
{"source": "owner/repo", "sourceType": "github", "computedHash": "515ba7…"} |
no finding — content-hash pinned (the common case) |
{"source": "owner/repo", "commit": "868e73…", "integrity": "sha256-…"} |
no finding — commit + integrity pinned |
{"source": "owner/repo", "ref": "5f2e8c1a…3d5e7f"} (full SHA) |
no finding — pinned |
{"source": "owner/repo", "sourceType": "github", "ref": "main"} |
warn — branch ref, no content hash |
{"source": "owner/repo", "sourceType": "github"} |
warn — no integrity at all |
{"source": "./skills/helper", "sourceType": "local"} |
no finding — on-disk, no remote trust edge |
The parser keys off the skills map and tolerates unknown fields and schema versions, so a future schema bump does not break the scan. Findings name the local alias and the source slug.
Calibrated against a 358-file / 2,556-entry corpus of real
skills-lock.jsonfiles: 355 usecomputedHash, so recognizing the content-hash fields (not justref) is what keeps this rule's false-positive rate at zero while still catching genuinely unpinned entries.
Scope
Only the project-root skills-lock.json (committable) is scanned. The user-global ~/.agents/.skill-lock.json / $XDG_STATE_HOME/skills/.skill-lock.json is not scanned — it is not committable, so it is out of scope (consistent with cfgaudit's portable-committable scope rule).
Trigger example
{
"version": 3,
"skills": {
"code-review": {
"source": "vercel-labs/agent-skills",
"sourceType": "github",
"ref": "main"
}
}
}
→ warn (a bare main branch ref with no content hash — the source content can change under you).
Safe alternative
Install the skill from a released/pinned version so the CLI records a content hash, or pin the source to a full commit SHA — either makes the installed content reproducible and tamper-evident:
{ "skills": { "code-review": {
"source": "vercel-labs/agent-skills",
"sourceType": "github",
"computedHash": "515ba75178bd44875812d9a560bdf14651f86709f89cf1d4f209638e879807f3"
} } }
References
- CFG017 (unpinned MCP package version) — the MCP analogue of this supply-chain concern
- CFG024/026/029–036, CFG051/056 — the rules that scan installed skill (
SKILL.md) content once it is on disk - OWASP LLM04:2026 – Supply Chain
Documentation for cfgaudit 1.18.1. Source of this page · Rule implementation