Building a Security Shim for AI Code Assistants: How safer-dependencies Intercepts Claude's Package Installs
Hook
When Claude Code adds a dependency to your project, it touches disk before any security check runs. For a brief moment, a package with a critical CVE exists in your manifest, waiting for the next npm install to execute its postinstall scripts. This is the race condition safer-dependencies was built to close.
Context
AI coding assistants like Claude Code operate with write access to your codebase, including package manifests. When you ask Claude to "add authentication to this Express app," it might write passport@0.4.1 to your package.json—a version from 2018 with known vulnerabilities. Traditional security tools catch this hours later in CI, after the vulnerable code is already committed and potentially deployed.
The challenge isn't just CVE detection. AI assistants can introduce supply-chain risks that static scanners miss: typosquatted package names (reqests instead of requests), abandoned packages (pycrypto hasn't been updated since 2013), or brand-new packages that haven't been vetted by the community. These assistants don't have security judgment—they pattern-match from training data that includes plenty of insecure examples. The problem requires runtime interception: a security layer that validates dependencies at the exact moment Claude proposes them, not after they're merged.
Technical Insight
Safer-dependencies implements a multi-hook architecture that intercepts Claude Code at five distinct execution points. The core design insight is treating AI-generated dependency changes as untrusted input that requires validation before touching the package manager.
The primary hook is PostToolUse:Write, which fires immediately after Claude writes to a package manifest. Here's how the corrective flow works:
# Simplified from the actual validator
def validate_and_correct_manifest(file_path, ecosystem):
if ecosystem == 'npm':
with open(file_path) as f:
manifest = json.load(f)
for pkg_name, version_spec in manifest.get('dependencies', {}).items():
# Query OSV database for known CVEs
vulns = query_osv(ecosystem, pkg_name, version_spec)
# Check package age (7-day cooldown)
publish_date = get_publish_date(pkg_name, version_spec)
if is_too_recent(publish_date, days=7):
# Select newest safe version published 7+ days ago
safe_version = find_safe_version(pkg_name, cooldown_days=7)
emit_signal(f"UPDATED: {pkg_name} {version_spec} → {safe_version}")
manifest['dependencies'][pkg_name] = safe_version
# Typosquat detection via edit distance + popularity
if looks_like_typosquat(pkg_name):
emit_signal(f"BLOCKED: {pkg_name} (possible typosquat)")
del manifest['dependencies'][pkg_name]
# Rewrite manifest in-place
with open(file_path, 'w') as f:
json.dump(manifest, f, indent=2)
This is a post-write corrective model (what the analysis calls "Shape C"). The vulnerable version briefly touches disk before being rewritten. Why not block the write entirely? Because blocking would cause Claude's tool call to fail, forcing it into retry loops. By allowing the write and immediately correcting it within the same tool cycle, the conversational flow stays intact while still achieving safety.
The PreToolUse:Bash hook handles a different attack vector: explicit package manager commands. When Claude runs pip install requests==2.25.0, the bash hook intercepts before execution:
#!/bin/bash
# safer-dependencies-shim.sh (simplified)
command="$@"
# Fast path: skip validation for non-package-manager commands
if [[ ! "$command" =~ ^(npm|pip|gem|mvn|go get|cargo) ]]; then
exec "$command" # ~115ms overhead
fi
# Parse command to extract package + version
if [[ "$command" =~ pip\ install\ ([^=]+)==([0-9.]+) ]]; then
package="${BASH_REMATCH[1]}"
version="${BASH_REMATCH[2]}"
# Dispatch to Python validator
result=$(python3 -m safer_dependencies.validate \
--ecosystem pypi \
--package "$package" \
--version "$version")
if [[ "$result" =~ ^BLOCKED: ]]; then
echo "$result" >&2
exit 1 # Prevent installation
elif [[ "$result" =~ ^UPDATED: ]]; then
# Extract safe version and rewrite command
safe_version=$(echo "$result" | awk '{print $NF}')
exec pip install "${package}==${safe_version}"
fi
fi
exec "$command"
The bash hook closes the race condition where Claude writes a vulnerable manifest and immediately runs the install command. By intercepting both the write and the install, the tool ensures no unsafe package ever touches the node_modules or site-packages directory.
A subtle but critical feature is REGRESSION detection. Safer-dependencies maintains an audit log of (file, package, version) tuples. If Claude's plan is stale—or a subagent re-introduces a previously patched CVE—the tool detects the regression:
def check_regression(file_path, package, proposed_version):
audit_log = load_audit_log()
key = (file_path, package)
if key in audit_log:
previous_fix = audit_log[key]
if version_compare(proposed_version, previous_fix['safe_version']) < 0:
# Claude is trying to downgrade to an old vulnerable version
emit_signal(f"REGRESSION: {package} was fixed at {previous_fix['safe_version']}")
return previous_fix['safe_version'] # Auto-restore
return None
This handles the case where Claude's context window doesn't include the earlier fix, so it regenerates the same vulnerable dependency from its training data.
For PyPI, the tool validates hash pins in requirements.txt against the registry's published SHA256:
def validate_hash_pin(package, version, declared_hash):
registry_hash = fetch_pypi_hash(package, version)
if declared_hash != registry_hash:
emit_signal(f"BLOCKED: {package}=={version} hash mismatch (possible tampering)")
return False
return True
This catches manifest tampering or registry-side attacks that wouldn't appear as CVEs—a rare feature among dependency scanners.
Gotcha
The post-write corrective model has a timing gap: if Claude writes a vulnerable package.json and immediately runs npm install before the shim executes, the postinstall scripts from the vulnerable package will run. The PreToolUse:Bash hook mitigates this, but there's no atomic guarantee both hooks fire in sequence.
Private registries are unsupported. All provenance checks assume public npm, PyPI, RubyGems, and Maven Central. If your organization uses Artifactory or a private PyPI mirror, typosquat detection breaks (it compares against public package popularity metrics), and you'll get false positives. Enterprise users are blocked here. The 7-day cooldown is hardcoded with no override—if a critical 0-day patch lands today, the tool will actively prevent you from installing it until next week. There's no escape hatch for "I know this version is safe, let me use it anyway."
Verdict
Use if: You're a small-to-medium team giving Claude Code write access to dependency manifests, you don't already run Dependabot or Renovate, and your threat model includes junior developers or non-specialists making risky package choices. The real-time blocking prevents obvious supply-chain footguns (ancient CVE-ridden packages, typosquats, abandoned libraries) without requiring security expertise from every team member. Skip if: You already have mature dependency automation (Dependabot, Snyk, Renovate), you use private package registries, or you're building a commercial product around Claude Code (the non-OSI license requires separate licensing for commercial redistribution). For enterprises, the lack of private registry support and transitive dependency auto-correction are dealbreakers. This is a pragmatic defensive layer for teams where AI coding assistance is newer than your security tooling—not a replacement for comprehensive SCA.