Supply-Chain Hardening
Installing or importing a dependency runs someone else's code with your
privileges. Every control in this skill exists to change what that sentence
permits: when the code runs, whether it runs at all, what it can do when it
does, and how you find out it was hostile.
This is a content skill, not a tool: it owns the reasoning - which
control, which layer, whether to allow an exception - while config files own
the rules. It serves three situations: hardening a project, hardening a
machine or global config, and an agent mid-task when a gate fires.
Principles
- A dependency is code execution, not data. Adding a package grants its
publisher - and whoever compromises them - execution on your machines and
CI. Vet accordingly, and extend the same reasoning to non-package
dependencies: GitHub Actions, base images, MCP servers, agent skills.
- Removing triggers is not removing capability. Blocking install scripts
stops one execution path; the code still runs on import with full
privileges. Every structural control closes a trigger; attackers move to
the next one. That's why layers, not any one switch.
- Layer for defence-in-depth, and know each layer's limit. Age gates
detect nothing; scanners prevent nothing; provenance authenticates the
pipeline, not the payload; runtime "permission" flags are seat belts, not
boundaries. A layer whose limit you can't state is a layer you're
over-trusting.
- A gate set is not a gate enforced. Some tools' gates are advisory by
default (aube falls back to an older version unless
minimumReleaseAgeStrict = true; pnpm's built-in default gate is
non-strict, though explicitly configuring the gate flips strict on).
After configuring any gate, verify it fails closed - try to install
something that should be refused. When a broader control masks a
narrower one, that verification is unavailable: the narrow gate has
nothing to refuse, so it stays silent locally and fires on the first
machine without the mask. Reproducing it means turning the broad control
off, which is a de-protection, not a check - assert the missing decision
statically instead (read the config, run no install).
- The user sets policy; the agent applies it; something boring verifies.
Denial enforces the policy and needs no approval: record a package as
denied, verify the required behaviour, and continue. Granting execution or
weakening a gate requires either standing authority that names the
conditions or a user decision. Enforcement belongs in config, hooks, and
CI - never in anyone's memory.
- Every permission exception is scoped, reasoned, and has an exit
criterion. An exception without an expiry condition is a permanent hole
with a historical excuse. An explicit denial is the baseline, not an
exception. See references/exceptions.md.
When to use this skill
- New project or machine → walk the five-layer stack below; per-tool keys
and drop-in blocks in references/ecosystems.md.
- Hardening incrementally → audit the existing setup against the stack;
the usual gaps are a non-strict gate, an uncovered frozen-install path, and
no detective layer.
- A gate fired mid-task → jump to "When a gate fires" below. Never bypass
globally to keep a task moving.
- Unfamiliar or agent-suggested package → verify before installing (see
"Slopsquatting").
- Scanner hit or suspected compromise →
references/incident-response.md.
- "Should we buy a scanner/firewall product?" →
references/evaluating-dependencies.md.
Scope fence: if the task adds no dependency and touches no package-manager or
CI config, this skill has nothing to add - do the task without narrating
supply-chain considerations.
Trigger vs capability
ignore-scripts removes a trigger, not a capability. Reason about which
execution triggers an ecosystem exposes, because closing one does nothing for
the others:
| Ecosystem |
Install/build time |
Load time |
| npm family |
lifecycle scripts (blockable: npm 12+ default-off, pnpm/bun/aube block by default); git deps execute code even with scripts off |
module top-level bodies and IIFEs run on import/require - not blockable, and where attackers moved when scripts got blocked |
| Python |
build backends run at install |
.pth files execute on any interpreter startup, no import needed; import hooks; top-level module code |
| Rust |
build.rs and proc-macros run at compile time - no off-switch exists |
- |
The load-time column is why "scripts are disabled, we're safe" is false, and
why the detective layer and containment exist. Runtime containment: real
boundaries are containers/VMs with no egress; Node's --permission is, per
its own docs, a seat belt and not a security boundary (known gaps:
node:sqlite filesystem access, no worker-thread inheritance, pre-init flag
escapes).
The five-layer stack
| # |
Layer |
Control |
Role |
Limit |
| 1 |
Quarantine |
Release-age gate (per-tool keys: references/ecosystems.md) |
Preventive: buys the community-catch window before you install a fresh compromise |
Detects nothing; blind to old-but-bad versions; must be verified strict |
| 2 |
Structural |
Scripts off, git/exotic deps blocked, lockfile + checksums, trust policy |
Removes install-time triggers; pins exactly what CI installs |
Load-time execution untouched; lockfile pins whatever was resolved, good or bad |
| 3 |
Containment |
No-egress containers/VMs for untrusted installs and builds |
Bounds what hostile code reaches |
Only as strong as the isolation primitive; --permission flags are not boundaries |
| 4 |
CI/CD |
Default-deny egress, pinned actions, same script/gate policy as dev machines |
CI is the highest-value target: it holds publish tokens |
Config drift between CI and dev silently forks the policy |
| 5 |
Detective |
osv-scanner MAL-* matching, wired pre-mutation (references/incident-response.md) |
Catches what slipped every preventive layer |
Trails advisory publication; can't parse tool-manager lockfiles |
Layer 5 is not optional garnish: malware campaigns have shipped with valid
SLSA provenance (a compromised pipeline signs its own malware), and the
large majority of malicious packages are pulled by registries without ever
receiving a CVE - so neither provenance nor CVE feeds substitute for MAL-*
matching. Provenance ≠ safety.
Slopsquatting
Agents hallucinate plausible package names; attackers register them. Before
installing anything unfamiliar - especially a name suggested by an agent or
LLM output - verify existence, purpose match, age, downloads, and repository;
a brand-new low-download package answering exactly a niche need is the attack
shape. A strict age gate refuses brand-new names as a side effect;
low-download-threshold checks (aube) encode the same defence.
Age-gate units: never copy a number between tools
The same 4-day policy is spelled in days (npm), minutes (pnpm, aube, Yarn),
seconds (bun), duration strings (mise, uv), and ISO-8601 or minutes
(P4D/5760 - pip, Deno; Deno rejects 4d).
Copying a literal between configs silently changes the policy by orders of
magnitude. Full table and per-tool keys:
references/ecosystems.md.
When one policy value is hand-spelled across several configs, enforce
agreement mechanically - a drift-guard pre-commit checker (one constant, one
file+regex+unit row per config, normalise, fail on drift). The
mechanical-enforcement skill owns that checker pattern.
When a gate fires mid-task
The most common agent-facing situation: an install refused, a build script
blocked, a provenance or trust-policy failure. The gate firing is the system
working - the burden of proof is on the bypass.
- Identify which control fired and what it's protecting against. Read
the error, not just past it (
ERR_PNPM_IGNORED_BUILDS = script blocking;
"no mature version" = age gate; trust/provenance = downgrade check).
- Try enforcement before exception. For a blocked dependency script,
record the package as explicitly denied and run a clean install plus the
checks that exercise its required behaviour. Continue without asking when
denial works. Test every target platform whose package graph or native
artifact differs; a warm
node_modules or side-effects cache is not a
clean contrast.
- Investigate a functional failure. A package failing without its script
proves only that it demands the script, not that executing it is safe or
necessary. Inspect the exact resolved artifact and lifecycle script, then
prefer removing the dependency, using a published artifact, or moving the
operation to an explicit setup step.
- Apply standing authority or ask. If repository policy delegates narrow
permission under observable conditions, apply it and record the evidence.
Otherwise ask before granting execution. Never ask merely to record a
denial.
- Allowlist narrowly: per-package
allowBuilds/approve-scripts/
scoped exclude - never a global disable, never ignore-scripts=false,
never removing the gate. Project-level is not the narrow option: it
is not per-package, and it re-arms scripts the per-package allowlist
does not govern - the root package's own preinstall/install/
postinstall run again (silently arming repo git hooks from prepare,
among other things), and every allowBuilds: { x: true } in the repo
flips from inert to executing. Turning the mask off changes what runs;
it is a de-protection, never a diagnostic.
- Document a permission's scope, reason, and exit criterion in the config
where the exception lives - see
references/exceptions.md for the template and
worked examples (publishing-bug-vs-attack, aged backports). A denial may
record why required behaviour works without the script; it needs no expiry.
Hard lines: never --no-verify past a supply-chain hook; never disable a
gate globally or project-wide to save a round-trip; a one-off env/flag
bypass is for human-attended one-shots only (the per-tool asymmetry - which
tools even offer one - is in
references/ecosystems.md).
Composition with neighbouring skills
- mechanical-enforcement owns linter-shaped controls - gitleaks, zizmor
for GitHub Actions, cargo-deny licence/advisory bans, and the generic
config-drift-guard checker pattern. This skill owns dependency posture:
what to gate, what to scan, when to except.
- hk owns wiring: this skill says what to run (osv-scanner as update
task + CI gate + hook); hk says how to hook it.
- security-review reviews your own code for vulnerabilities; this skill
governs third-party code you adopt.
Adding a new control or ecosystem
- Identify which layer it belongs to (quarantine / structural / containment
/ CI / detective). If it claims two, it's probably two controls.
- Prefer structural removal over runtime mitigation, and automated detective
wiring over a manual task.
- Record the tool's key, unit, and strictness default in
references/ecosystems.md - verify each against
the live tool, not memory; units and defaults are where the traps live.
- Note the new tool's escape hatches and exception vehicle, and whether its
gate applies on the frozen/lockfile install path.
References
| When the task involves… |
Read |
| Per-tool gate keys, units, strictness defaults, drop-in config blocks, escape-hatch table |
references/ecosystems.md |
| A scanner hit, suspected compromise, wiring osv-scanner, provenance verification |
references/incident-response.md |
| Granting/reviewing an exception, allowlist templates, bypass acceptability |
references/exceptions.md |
| Choosing between local-OSS controls and commercial products; skills/MCP as dependencies |
references/evaluating-dependencies.md |
evals/ holds this skill's test prompts, fixtures, and assertions - run per
the writing-skills evals harness when revising. The compromised-lockfile
fixture is inert grading text; never install from it.
1---
2name: supply-chain-hardening
3description: >-
4 Harden software supply chains across ecosystems (npm/pnpm/bun/yarn/aube,
5 uv/pip, Cargo, GitHub Actions/aqua) by reasoning about what installing or
6 importing a dependency actually executes, then choosing controls: release-age
7 quarantine gates, install-script and load-time trigger removal, lockfile
8 pinning and checksums, provenance/SLSA verification, and osv-scanner malware
9 detection. Use when hardening a project or machine/global config (new or
10 incremental); when converting age-gate settings/units between package
11 managers; when a gate fires mid-task - a blocked install or build script, an
12 age-gate or provenance refusal, a slopsquatted or unfamiliar package - and
13 you must decide whether to allowlist; when adding a dependency you have not
14 vetted; or when auditing a suspected compromised package. For linter rules,
15 gitleaks, and zizmor use the mechanical-enforcement skill; for wiring checks
16 into git hooks use the hk skill; for code-level vulnerability review of your
17 own code use security-review.
18---
19
20# Supply-Chain Hardening
21
22**Installing or importing a dependency runs someone else's code with your
23privileges.** Every control in this skill exists to change what that sentence
24permits: when the code runs, whether it runs at all, what it can do when it
25does, and how you find out it was hostile.
26
27This is a **content skill**, not a tool: it owns the *reasoning* - which
28control, which layer, whether to allow an exception - while config files own
29the *rules*. It serves three situations: hardening a project, hardening a
30machine or global config, and an agent mid-task when a gate fires.
31
32## Principles
33
341. **A dependency is code execution, not data.** Adding a package grants its
35 publisher - and whoever compromises them - execution on your machines and
36 CI. Vet accordingly, and extend the same reasoning to non-package
37 dependencies: GitHub Actions, base images, MCP servers, agent skills.
382. **Removing triggers is not removing capability.** Blocking install scripts
39 stops one execution path; the code still runs on import with full
40 privileges. Every structural control closes a *trigger*; attackers move to
41 the next one. That's why layers, not any one switch.
423. **Layer for defence-in-depth, and know each layer's limit.** Age gates
43 detect nothing; scanners prevent nothing; provenance authenticates the
44 pipeline, not the payload; runtime "permission" flags are seat belts, not
45 boundaries. A layer whose limit you can't state is a layer you're
46 over-trusting.
474. **A gate set is not a gate enforced.** Some tools' gates are advisory by
48 default (aube falls back to an older version unless
49 `minimumReleaseAgeStrict = true`; pnpm's *built-in* default gate is
50 non-strict, though explicitly configuring the gate flips strict on).
51 After configuring any gate, verify it fails closed - try to install
52 something that should be refused. **When a broader control masks a
53 narrower one, that verification is unavailable**: the narrow gate has
54 nothing to refuse, so it stays silent locally and fires on the first
55 machine without the mask. Reproducing it means turning the broad control
56 off, which is a de-protection, not a check - assert the missing decision
57 statically instead (read the config, run no install).
585. **The user sets policy; the agent applies it; something boring verifies.**
59 Denial enforces the policy and needs no approval: record a package as
60 denied, verify the required behaviour, and continue. Granting execution or
61 weakening a gate requires either standing authority that names the
62 conditions or a user decision. Enforcement belongs in config, hooks, and
63 CI - never in anyone's memory.
646. **Every permission exception is scoped, reasoned, and has an exit
65 criterion.** An exception without an expiry condition is a permanent hole
66 with a historical excuse. An explicit denial is the baseline, not an
67 exception. See [references/exceptions.md](references/exceptions.md).
68
69## When to use this skill
70
71- **New project or machine** → walk the five-layer stack below; per-tool keys
72 and drop-in blocks in [references/ecosystems.md](references/ecosystems.md).
73- **Hardening incrementally** → audit the existing setup against the stack;
74 the usual gaps are a non-strict gate, an uncovered frozen-install path, and
75 no detective layer.
76- **A gate fired mid-task** → jump to "When a gate fires" below. Never bypass
77 globally to keep a task moving.
78- **Unfamiliar or agent-suggested package** → verify before installing (see
79 "Slopsquatting").
80- **Scanner hit or suspected compromise** →
81 [references/incident-response.md](references/incident-response.md).
82- **"Should we buy a scanner/firewall product?"** →
83 [references/evaluating-dependencies.md](references/evaluating-dependencies.md).
84
85Scope fence: if the task adds no dependency and touches no package-manager or
86CI config, this skill has nothing to add - do the task without narrating
87supply-chain considerations.
88
89## Trigger vs capability
90
91`ignore-scripts` removes a **trigger, not a capability**. Reason about which
92execution triggers an ecosystem exposes, because closing one does nothing for
93the others:
94
95| Ecosystem | Install/build time | Load time |
96|---|---|---|
97| npm family | lifecycle scripts (blockable: npm 12+ default-off, pnpm/bun/aube block by default); git deps execute code even with scripts off | module top-level bodies and IIFEs run on `import`/`require` - not blockable, and where attackers moved when scripts got blocked |
98| Python | build backends run at install | `.pth` files execute on **any interpreter startup, no import needed**; import hooks; top-level module code |
99| Rust | `build.rs` and proc-macros run at **compile** time - no off-switch exists | - |
100
101The load-time column is why "scripts are disabled, we're safe" is false, and
102why the detective layer and containment exist. Runtime containment: real
103boundaries are containers/VMs with no egress; Node's `--permission` is, per
104its own docs, a seat belt and **not a security boundary** (known gaps:
105`node:sqlite` filesystem access, no worker-thread inheritance, pre-init flag
106escapes).
107
108## The five-layer stack
109
110| # | Layer | Control | Role | Limit |
111|---|---|---|---|---|
112| 1 | Quarantine | Release-age gate (per-tool keys: [references/ecosystems.md](references/ecosystems.md)) | Preventive: buys the community-catch window before you install a fresh compromise | Detects nothing; blind to old-but-bad versions; must be verified strict |
113| 2 | Structural | Scripts off, git/exotic deps blocked, lockfile + checksums, trust policy | Removes install-time triggers; pins exactly what CI installs | Load-time execution untouched; lockfile pins whatever was resolved, good or bad |
114| 3 | Containment | No-egress containers/VMs for untrusted installs and builds | Bounds what hostile code reaches | Only as strong as the isolation primitive; `--permission` flags are not boundaries |
115| 4 | CI/CD | Default-deny egress, pinned actions, same script/gate policy as dev machines | CI is the highest-value target: it holds publish tokens | Config drift between CI and dev silently forks the policy |
116| 5 | Detective | osv-scanner `MAL-*` matching, wired pre-mutation ([references/incident-response.md](references/incident-response.md)) | Catches what slipped every preventive layer | Trails advisory publication; can't parse tool-manager lockfiles |
117
118Layer 5 is not optional garnish: malware campaigns have shipped with **valid
119SLSA provenance** (a compromised pipeline signs its own malware), and the
120large majority of malicious packages are pulled by registries without ever
121receiving a CVE - so neither provenance nor CVE feeds substitute for `MAL-*`
122matching. Provenance ≠ safety.
123
124## Slopsquatting
125
126Agents hallucinate plausible package names; attackers register them. Before
127installing anything unfamiliar - especially a name suggested by an agent or
128LLM output - verify existence, purpose match, age, downloads, and repository;
129a brand-new low-download package answering exactly a niche need is the attack
130shape. A strict age gate refuses brand-new names as a side effect;
131low-download-threshold checks (aube) encode the same defence.
132
133## Age-gate units: never copy a number between tools
134
135The same 4-day policy is spelled in days (npm), minutes (pnpm, aube, Yarn),
136seconds (bun), duration strings (mise, uv), and ISO-8601 or minutes
137(`P4D`/`5760` - pip, Deno; Deno rejects `4d`).
138Copying a literal between configs silently changes the policy by orders of
139magnitude. Full table and per-tool keys:
140[references/ecosystems.md](references/ecosystems.md).
141
142When one policy value is hand-spelled across several configs, enforce
143agreement mechanically - a drift-guard pre-commit checker (one constant, one
144file+regex+unit row per config, normalise, fail on drift). The
145mechanical-enforcement skill owns that checker pattern.
146
147## When a gate fires mid-task
148
149The most common agent-facing situation: an install refused, a build script
150blocked, a provenance or trust-policy failure. The gate firing is the system
151working - the burden of proof is on the bypass.
152
1531. **Identify which control fired** and what it's protecting against. Read
154 the error, not just past it (`ERR_PNPM_IGNORED_BUILDS` = script blocking;
155 "no mature version" = age gate; trust/provenance = downgrade check).
1562. **Try enforcement before exception.** For a blocked dependency script,
157 record the package as explicitly denied and run a clean install plus the
158 checks that exercise its required behaviour. Continue without asking when
159 denial works. Test every target platform whose package graph or native
160 artifact differs; a warm `node_modules` or side-effects cache is not a
161 clean contrast.
1623. **Investigate a functional failure.** A package failing without its script
163 proves only that it demands the script, not that executing it is safe or
164 necessary. Inspect the exact resolved artifact and lifecycle script, then
165 prefer removing the dependency, using a published artifact, or moving the
166 operation to an explicit setup step.
1674. **Apply standing authority or ask.** If repository policy delegates narrow
168 permission under observable conditions, apply it and record the evidence.
169 Otherwise ask before granting execution. Never ask merely to record a
170 denial.
1715. **Allowlist narrowly**: per-package `allowBuilds`/`approve-scripts`/
172 scoped exclude - never a global disable, never `ignore-scripts=false`,
173 never removing the gate. **Project-level is not the narrow option**: it
174 is not per-package, and it re-arms scripts the per-package allowlist
175 does not govern - the *root package's own* `preinstall`/`install`/
176 `postinstall` run again (silently arming repo git hooks from `prepare`,
177 among other things), and every `allowBuilds: { x: true }` in the repo
178 flips from inert to executing. Turning the mask off changes what runs;
179 it is a de-protection, never a diagnostic.
1806. **Document a permission's scope, reason, and exit criterion in the config**
181 where the exception lives - see
182 [references/exceptions.md](references/exceptions.md) for the template and
183 worked examples (publishing-bug-vs-attack, aged backports). A denial may
184 record why required behaviour works without the script; it needs no expiry.
185
186Hard lines: never `--no-verify` past a supply-chain hook; never disable a
187gate globally *or project-wide* to save a round-trip; a one-off env/flag
188bypass is for human-attended one-shots only (the per-tool asymmetry - which
189tools even offer one - is in
190[references/ecosystems.md](references/ecosystems.md)).
191
192## Composition with neighbouring skills
193
194- **mechanical-enforcement** owns linter-shaped controls - gitleaks, zizmor
195 for GitHub Actions, cargo-deny licence/advisory bans, and the generic
196 config-drift-guard checker pattern. This skill owns dependency posture:
197 what to gate, what to scan, when to except.
198- **hk** owns wiring: this skill says *what* to run (osv-scanner as update
199 task + CI gate + hook); hk says *how* to hook it.
200- **security-review** reviews your own code for vulnerabilities; this skill
201 governs third-party code you adopt.
202
203## Adding a new control or ecosystem
204
2051. Identify which layer it belongs to (quarantine / structural / containment
206 / CI / detective). If it claims two, it's probably two controls.
2072. Prefer structural removal over runtime mitigation, and automated detective
208 wiring over a manual task.
2093. Record the tool's key, unit, and strictness default in
210 [references/ecosystems.md](references/ecosystems.md) - verify each against
211 the live tool, not memory; units and defaults are where the traps live.
2124. Note the new tool's escape hatches and exception vehicle, and whether its
213 gate applies on the frozen/lockfile install path.
214
215## References
216
217| When the task involves… | Read |
218|---|---|
219| Per-tool gate keys, units, strictness defaults, drop-in config blocks, escape-hatch table | [references/ecosystems.md](references/ecosystems.md) |
220| A scanner hit, suspected compromise, wiring osv-scanner, provenance verification | [references/incident-response.md](references/incident-response.md) |
221| Granting/reviewing an exception, allowlist templates, bypass acceptability | [references/exceptions.md](references/exceptions.md) |
222| Choosing between local-OSS controls and commercial products; skills/MCP as dependencies | [references/evaluating-dependencies.md](references/evaluating-dependencies.md) |
223
224`evals/` holds this skill's test prompts, fixtures, and assertions - run per
225the writing-skills evals harness when revising. The compromised-lockfile
226fixture is inert grading text; never install from it.