fix(guards): refuse a .npmrc beside a portal manifest, like bunfig.toml

Bun reads .npmrc too, so a registry line beside a borrowed portal's
manifest fetches every dependency from a server the portal's author
picked - the same door bunfig.toml opens, which the installer and guard
check 3 already refuse. Both now reject either file. The #718 integration
test, which pointed Bun at its offline registry through a .npmrc, passes
the registry through NPM_CONFIG_REGISTRY instead; it still proves the
hook stays off, because the hooked package only exists on that server.

Follows GHSA-wq7j-x5jq-pjrw; this case is ours, not the report's. Guard
tests for the workspaces and bunfig.toml refusals, which the previous
commit added without them, come along.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mads LorentzenandClaude Fable 5.1 authored and Mads Lorentzen committed 2026-10-27 16:14:35 +01:00
1 parent 0bcfd75e60
commit 83c788d779
9 files changed
+67 -28

No files matched your search

+16 -12
View File
@@ -27,8 +27,8 @@ Checks:
a subdirectory, which outranks the root file, is checked for negations.
3. .agents/**/package.json — no npm/bun lifecycle scripts (preinstall,
install, postinstall, prepare, prepack), no trustedDependencies, no
workspaces, and no bunfig.toml beside the manifest. Catches code
execution smuggled into `bun install`.
workspaces, and no bunfig.toml or .npmrc beside the manifest. Catches
code execution smuggled into `bun install`, and a registry swap.
4. Skill and command frontmatter — every `allowed-tools` entry in
.claude/skills/*/SKILL.md, .agents/skills/*/SKILL.md and
.claude/commands/*.md must be in ALLOWED_SKILL_TOOLS (a portal skill may
@@ -254,10 +254,13 @@ FORBIDDEN_SCRIPTS = {"preinstall", "install", "postinstall", "prepare", "prepack
# Manifest keys that pull code into `bun install` by another door: `workspaces`
# installs sub-packages with their own lifecycle scripts (a workspace
# postinstall ran under Bun 1.3.10, scripts the top-level check never saw), and
# `trustedDependencies` re-enables dependency scripts. A bunfig.toml beside the
# manifest reconfigures the install itself (registry, scopes, cache) and is
# refused for the same reason. tools/install_portal_clis.py skips all of these.
# `trustedDependencies` re-enables dependency scripts. A bunfig.toml or .npmrc
# beside the manifest reconfigures the install itself (registry, scopes,
# cache), so every dependency can come from a server the portal's author
# picked; both are refused for the same reason. tools/install_portal_clis.py
# skips all of these.
FORBIDDEN_MANIFEST_KEYS = {"trustedDependencies", "workspaces"}
INSTALL_CONFIG_FILES = ("bunfig.toml", ".npmrc")
# The exact `allowed-tools` entries a skill or command frontmatter may carry.
# Claude Code pre-approves these for the turn that invokes the skill, with no
@@ -666,13 +669,14 @@ def check_package_manifests() -> None:
f"{relpath}: workspaces is forbidden - each workspace package brings its own "
"lifecycle scripts, which this check never reads, into `bun install`."
)
bunfig = manifest.parent / "bunfig.toml"
if bunfig.exists():
errors.append(
f"{bunfig.relative_to(ROOT)}: a bunfig.toml beside a portal manifest is forbidden - "
"it reconfigures `bun install` itself (registry, scopes, trusted packages) for "
"every fork user."
)
for name in INSTALL_CONFIG_FILES:
config = manifest.parent / name
if config.exists():
errors.append(
f"{config.relative_to(ROOT)}: a {name} beside a portal manifest is forbidden - "
"it reconfigures `bun install` itself (registry, scopes, trusted packages) for "
"every fork user."
)
def check_tracked_node_modules() -> None: