Skip to content

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 passscripts/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 in approved-tools.md

Three findings from the first two reviews, worth carrying forward

  1. --ignore-scripts protected 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.
  2. A severity label is not reachability. npm audit reported critical on tar and high on @capacitor/cli. Neither applied: every tar advisory needs an attacker-controlled archive, and that path only extracts archives shipped inside the integrity-pinned package.
  3. 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 to approved-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