> your AI agent picks dependencies from memory; give it dated facts — try starlog.dev ↗ vet your agent's deps ↗ vibe-coding is fine. vibe-importing isn’t. — try starlog.dev ↗ vibe-importing isn’t fine ↗ your agent has never seen your private packages — try starlog.dev ↗ facts for private packages ↗ a linter for the dependencies your AI agent picks — try starlog.dev ↗ a linter for agent deps ↗

Back to Articles

Building a Security Shim for AI Code Assistants: How safer-dependencies Intercepts Claude's Package Installs

[ View on GitHub ]

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

Write to manifest

Bash command

Agent invocation

Safe version found

Vulnerability/Too recent

Typosquat detected

Claude Code Tool Call

Hook Point Router

PostToolUse:Write

PreToolUse:Bash

PreToolUse:Agent

safer-dependencies-shim.sh

Ecosystem Validator

Package Registry Query

OSV Vulnerability DB

Version Selection

Rewrite Manifest

Block & Emit Signal

Emit UPDATED/REGRESSION

Claude Interprets Signal

Trigger Refactoring

System architecture — auto-generated

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.