Skip to content

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:

  1. 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

  2. Multi-source check (fail-closed ordering):

  3. osv.dev /v1/querybatch — CVE lookup per package/version
  4. OSSF Malicious Packages via GitHub Contents API — GET /repos/ossf/malicious-packages/contents/osv/malicious/<eco>/<pkg>
  5. No git clone required — a 404 means clean, a 200 means malicious, anything else is treated as offline

  6. Decision logic:

  7. CVE or OSSF hit → BLOCK (exit 1, structured message to model)
  8. Any source unreachable → FAIL CLOSED (exit 1)
  9. VTS_ALLOW_OFFLINE=1 set → ALLOW with result=partial-allow logged (not result=allow)
  10. All clean → ALLOW with result=allow logged

  11. 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.

  12. Skill cross-reference: vt-c-npm-security and vt-c-python-security reference 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