Dependency security policy¶
CLAUDE.md has said "NEVER install, recommend, or use third-party code without a detailed security
review first" since the repo was set up. On 2026-09-11 an agent broke it anyway — it read the
approved-tools.md entry for CocoaPods, saw the Install column said "Pre-installed on the
macos-26 runner", understood that this did not cover the operator's laptop, wrote that caveat
into its own message, and started brew install cocoapods regardless.
That is the design problem this document exists to fix. A rule enforced only by the judgement of
the thing being tempted is not a control. The same audit also found that the @capacitor/*
packages had been "reviewed post-hoc after an agent adopted it autonomously, which the rules
forbid" — so this was the second occurrence, not the first.
The rule¶
Third-party code is any code not written by our team: npm and pip packages, Homebrew formulae, GitHub Actions, MCP servers, Chrome extensions, shell scripts piped from the internet, forks.
It gets a recorded security review before it runs, and the approval is scoped. An approval names a host, a channel, a version and a set of conditions. Anything outside that scope is a new review.
Version upgrades are reviewed too¶
Operator decision, 2026-09-11. A version upgrade is a new review, at every magnitude — major, minor and patch. The approval is of a version, not of a name. An upgrade that has not been re-reviewed is unapproved code, regardless of how long we have trusted the package.
This is the case that matters most. The common supply-chain attack is a new version published to a name everyone already trusts, and it usually ships as a patch, precisely so it is auto-merged. A rule that approves names rather than versions is blind to exactly that.
It is affordable. Measured across the whole estate rather than assumed:
| Repos with Dependabot configured | 1 (rig-cockpit-worker) — every other repo has none |
| Dependabot PRs in the last 30 days | 12, all in that one repo |
| Of those, major bumps | nearly all |
What keeps it cheap is that the re-review is mechanical: scripts/audit-npm-tree.py against the new
version takes about 35 seconds, and its output is the evidence the approval record cites. The
part that actually detects a poisoned release is not the version number but the capability
fingerprint — a patch that gains an install script, or starts reaching the network, spawning
processes, evaluating code, or reading credential paths, is the signature.
Two passes¶
The mechanical pass — scripts/audit-npm-tree.py, ~35 seconds for a 100-package tree. It
downloads every tarball, verifies it byte-for-byte against the hash the lockfile pins, and reads it
in memory without ever extracting to disk, so running it on a hostile tree is safe. It reports:
| Check | Why it matters |
|---|---|
| Integrity vs. lockfile | A mismatch means the bytes served are not the bytes pinned |
| Off-registry sources | A git or tarball URL bypasses the registry's controls |
| Install scripts | The only code that runs during npm ci |
| Archive hygiene | Path traversal, symlinks, hardlinks, setuid bits |
| Capability surface | Which packages import network, child_process, eval, or touch credential paths |
| Typosquat screen | Weekly download volume; squats sit orders of magnitude below real packages |
The human pass — what the script cannot do, and must not be skipped because the script came back clean:
- Author trust: age, maintainer count, publish history, whether the repo is the official one
- Read the call sites the capability table points at. A pattern match is a place to look.
- Trace reachability before repeating a severity label. State which it is.
- Compare to official alternatives — and ask whether the dependency is needed at all
- Record the verdict in
docs/ai-tooling-registry/<category>/and the scoped row inapproved-tools.md
Three findings from the first two reviews, worth carrying forward¶
--ignore-scriptsprotected nothing. Zero of the 104 packages in the Capacitor tree declare an install script. The flag had been cited as the control that made the tree safe. Keep it as a guarantee about future lockfiles; never cite it as the reason a tree is safe today. The exposure is at runtime.- A severity label is not reachability.
npm auditreported critical ontarand high on@capacitor/cli. Neither applied: everytaradvisory needs an attacker-controlled archive, and that path only extracts archives shipped inside the integrity-pinned package. - Ask "is it needed?" before "how do we make it safe?" A whole review was spent designing conditions to make a rejected CocoaPods install survivable. The actual answer was that Swift Package Manager, already present in the pinned version, removed the dependency entirely.
What is mechanically enforced today¶
| Control | Scope | Behaviour |
|---|---|---|
.github/workflows/dependency-review.yml |
Stig-Johnny/claude-3, npm lockfiles |
Runs on any PR touching a package-lock.json. Audits only what the PR adds or upgrades, against the PR base. Comments the report, fails the PR on findings |
scripts/audit-npm-tree.py |
any npm lockfile, any repo, run by hand | The mechanical pass above |
Both fail closed: an audit that cannot complete exits 2 and fails the job rather than reading as a pass.
Enforcement gaps — the honest register¶
These are currently enforced by prose alone, which is the condition that already failed twice. They are listed so nobody mistakes "we have a policy" for "we have coverage".
| Gap | Risk | Tracking |
|---|---|---|
| dashecorp / rig repos have no gate. 23 npm lockfiles across 17 repos; the workflow above covers only claude-3 | A new dependency lands in a rig repo with no audit | Must be a rig-conductor capability, not a per-repo workflow — BRAIN is explicit that rig-conductor owns the PR lifecycle and copying per-repo workflows there is the wrong pattern |
Python is not covered. 5 requirements.txt and no equivalent auditor |
Same class of exposure, no mechanical pass | An auditor for wheels/sdists is the follow-up |
Homebrew is not covered. The CocoaPods incident was a brew install |
Formula autobump can move a reviewed version with no human reading the diff | Noted in the CocoaPods review; no gate exists |
MCP servers were blanket-approved. approved-tools.md said "All MCP servers listed there are approved" — an entire file approved by reference |
An MCP server runs with the agent's full tool access | Needs per-server review rows |
| Nothing audits what is already installed. Every control here is change-triggered | Pre-existing unreviewed code stays unreviewed | A one-off sweep of the 23 lockfiles |
| GitHub Actions are SHA-pinned but not reviewed. Pinning fixes which code runs, not what it does | A pinned action runs with the workflow token and full checkout access — a larger grant than most npm packages get. Three Action major bumps were merged in the last 30 days, and one of them, actions/checkout v4→v7, is the bump that broke the Android build (rig-cockpit-worker#111) |
#886 |
When the gate fires on your PR¶
It is telling you a package is new to this repo, not that it is malicious. Either:
- Attach a review. Run the auditor, do the human pass, write it to
docs/ai-tooling-registry/, add the scoped row toapproved-tools.md, link it in the PR. - Or remove the dependency. Often the cheaper answer, and sometimes the right one.
Do not merge past it on the grounds that the package is popular. Popularity is one trust signal out of several, and it is the one an attacker most wants you to weight.
Related: approved-tools.md · AI tooling registry · CocoaPods review — REJECTED · Capacitor CLI tree review — APPROVED with conditions