Hooks Over Skills for Security Controls¶
The Rule¶
Before reaching for a skill to enforce a security rule, ask: "Can this instruction be overridden by prompt context?" If yes, it must be a hook.
Skills are documentation and guidance. Hooks are enforcement.
Pattern¶
Trigger: Any security invariant that must hold even when the model is mid-conversation, distracted, or operating from a cached context.
Structure:
-
PreToolUse hook (
~/.claude/hooks/supply-chain-guard.sh) intercepts Bash tool calls matching package install patterns:npm install,pnpm add,yarn add,pip install,uv add,poetry add -
Multi-source check (fail-closed ordering):
- osv.dev
/v1/querybatch— CVE lookup per package/version - OSSF Malicious Packages via GitHub Contents API —
GET /repos/ossf/malicious-packages/contents/osv/malicious/<eco>/<pkg> -
No git clone required — a 404 means clean, a 200 means malicious, anything else is treated as offline
-
Decision logic:
- CVE or OSSF hit → BLOCK (exit 1, structured message to model)
- Any source unreachable → FAIL CLOSED (exit 1)
VTS_ALLOW_OFFLINE=1set → ALLOW withresult=partial-allowlogged (notresult=allow)-
All clean → ALLOW with
result=allowlogged -
Audit log (
~/.claude/security-hook.log, JSON-lines): Every intercept — blocked, allowed, or overridden — is recorded with timestamp, package, version, ecosystem, result, CVE IDs, override flag. -
Skill cross-reference:
vt-c-npm-securityandvt-c-python-securityreference the hook so developers know enforcement exists at a lower layer.
Critical Subtlety: partial-allow ≠ allow¶
When only one of two check sources responds (e.g., OSV answers but OSSF is
offline), the result is result=partial-allow, not result=allow.
Partial success is not full clearance. The audit log must reflect which sources were consulted. This distinction matters for ISO 27001 audit trail review.
OSSF Lookup Without Clone¶
The OSSF Malicious Packages repository is large. Cloning it per session or per install is impractical. Use the GitHub Contents API instead:
GET https://api.github.com/repos/ossf/malicious-packages/contents/osv/malicious/<ecosystem>/<package-name>
- HTTP 200 → package is in the malicious list → BLOCK
- HTTP 404 → package is not in the list → clean
- HTTP 4xx/5xx or network failure → treat as offline → FAIL CLOSED
EC-4 Pattern: Private Packages¶
Packages with private/scoped namespaces (e.g., @mycompany/internal-pkg)
cannot be verified against public CVE databases. Do not allow them silently.
Log them as result=unchecked-private with a warning. This makes the gap
explicit in the audit trail rather than masking it as an allowed install.
Hook Priority¶
Deploy the supply-chain hook at priority 78 (before venv-guard.sh at priority 85). Lower number = runs first. Supply-chain check should precede environment enforcement because a malicious package is more dangerous than a venv violation.
What the Skill Layer Still Does¶
After the hook enforces the security gate, the skill layer (vt-c-npm-security,
vt-c-python-security) provides:
- Guidance on interpreting CVE findings
- Lock-file best practices
- Remediation workflows
The skill is documentation. The hook is the gate. They work together; neither replaces the other.
Reference¶
- Hook source:
plugins/core-standards/hooks/supply-chain-guard.sh - Tests:
plugins/core-standards/tests/test-supply-chain-hook.sh(39 cases) - Contract:
specs/139-supply-chain-security-hook/contracts/supply-chain-guard.contract.md - Trust registry:
configs/security/trusted-hooks.yaml - Related patterns:
docs/solutions/security/venv-enforcement-pattern.md,docs/solutions/security/deny-rule-hooks-hardening.md