How DevOps CI/CD Practices Actually Protect Companies — Lessons From the Anthropic CLI Source Leak
How DevOps CI/CD Practices Actually Protect Companies — Lessons From the Anthropic CLI Source Leak
Primary keyword: DevOps CI/CD security Secondary keywords: CI/CD pipeline security, artifact hygiene, supply chain security, npm security, DevSecOps best practices
Introduction
In early 2026, Anthropic accidentally leaked 500,000+ lines of Claude Code source code — not through a cyberattack, not through a breach, but through a misconfigured release pipeline. A source map file was included in a production npm package. That source map pointed to a publicly accessible cloud storage bucket. The full source was downloadable by anyone. By the time it was noticed, the internet had already mirrored it everywhere.
The irony is sharp: a company building one of the most capable AI systems in the world, shipping a product with sophisticated security features, got burned by a missing line in .npmignore.
This isn’t a post mocking Anthropic — it happens to teams at every level of technical sophistication. It’s a post about what CI/CD security actually means in practice, what failed in that pipeline, and the specific controls that would have stopped it.
What Actually Happened: The Technical Failure Chain
Understanding the leak requires understanding how npm packages are built and published, and where the Anthropic pipeline skipped standard safety steps.
Step 1: A source map was generated during build. Source maps (.map files) map minified/compiled JavaScript back to the original source. They’re useful for debugging in development. They should never ship in a production package.
Step 2: No packaging exclusion rule existed. npm respects either a .npmignore file or a files whitelist in package.json to control what gets included in a published package. Neither was configured to exclude the .map file.
Step 3: The source map referenced a public URL. The map file contained a sourceMappingURL pointing to a source bundle in cloud storage — and that bucket had no authentication requirement. Anyone who fetched the map file got a working URL to download the original source.
Step 4: No pipeline gate caught the artifact. The release pipeline published to npm without validating the package contents. No file list inspection, no policy check, no scan for unexpected files.
Step 5: Public distribution amplified the damage instantly. npm packages are public by default and immediately cached by mirrors, CDNs, and third-party registries. Within minutes of publish, the source was downloadable through multiple independent paths. Unpublishing the package didn’t matter — the copies were already out.
Each step is individually fixable. The problem is that none of the controls were in place.
Root Causes: What the Pipeline Was Actually Missing
1. No artifact hygiene enforcement
“Artifact hygiene” means knowing exactly what you’re shipping and having a verified, enforced policy about it. This is distinct from just having a build that works — a build can succeed and still produce output you never intended to ship.
At minimum, this means:
- An explicit file whitelist (ship only these files) rather than an implicit blacklist (exclude these files). Whitelists fail closed; blacklists fail open.
- A pre-publish check that validates the final artifact against that whitelist.
- A build step that explicitly removes debug output, source maps, and development-only files.
For npm specifically: use the files field in package.json to declare exactly what gets published. Then add a CI step that runs npm pack --dry-run and validates the output against the expected list.
2. No pre-publish validation gate
A release pipeline that goes build → publish with nothing in between is not a secure pipeline — it’s a fast one. Fast pipelines ship bugs fast. They also ship leaks fast.
The gate that was missing here would have taken seconds: inspect the tarball that npm pack produces, check it against a policy, fail the pipeline if unexpected files are present.
# Example: check that no .map files are in the package
npm pack --dry-run 2>&1 | grep "\.map" && echo "ERROR: source maps in package" && exit 1
This is not sophisticated tooling. It’s a shell one-liner. It would have caught this leak.
3. Manual release steps introduced human variability
Anthropic’s post-incident notes acknowledged human error during deployment. Manual steps in release pipelines are variability introduced on purpose — you are explicitly betting that every person, every time, under every level of deadline pressure, will remember and correctly execute every step.
That bet loses. Not sometimes — eventually. The only way to eliminate that failure mode is to eliminate the manual step. Automate the packaging, the inspection, the signing, and the publish. Make the pipeline the only path to production. Remove the ability for a human to skip steps even accidentally.
4. Storage permissions weren’t validated as part of the build
The source map referenced a cloud storage URL. That URL was publicly accessible. Neither of those facts triggered any alert or gate.
Supply chain security means treating your build artifacts and the infrastructure they reference as a security surface, not just your production environment. If your build process creates files that reference external resources, those resources need to be governed by the same access policies as everything else.
The DevOps Controls That Would Have Prevented This
These aren’t theoretical — they’re standard practices that teams operating at production quality implement routinely.
Control 1: Deny-by-default packaging
The default behavior for most package managers is to include everything unless you say otherwise. Flip this. Define exactly what belongs in a production artifact and exclude everything else automatically.
For npm:
// package.json
{
"name": "your-package",
"files": [
"dist/",
"bin/",
"README.md"
]
}
With this configuration, source maps, .env files, test fixtures, and anything else not explicitly listed are excluded automatically. The whitelist approach means a new type of file has to be consciously added — it won’t accidentally slip in.
Control 2: Artifact inspection in CI
Add a pipeline stage after build and before publish that inspects the artifact:
# GitHub Actions example
- name: Inspect package contents
run: |
npm pack --dry-run 2>&1 > packed-files.txt
cat packed-files.txt
# Fail if source maps are present
if grep -q "\.map$" packed-files.txt; then
echo "FAIL: Source map files detected in package"
exit 1
fi
# Fail if .env files are present
if grep -q "\.env" packed-files.txt; then
echo "FAIL: .env files detected in package"
exit 1
fi
This runs in seconds and blocks publication before the artifact leaves your control.
Control 3: Secret and sensitive file scanning
Before any artifact is published or any container is pushed, run a scanner against it. Tools like Trivy, Grype, and truffleHog scan for:
- Hardcoded credentials and API keys
- Known vulnerable dependencies
- Files that match patterns for secrets (private keys, tokens,
.env) - Source maps and debug artifacts
- name: Scan for secrets
uses: trufflesecurity/trufflehog@main
with:
path: ./dist
base: HEAD~1
head: HEAD
Integrate this as a required check. Failed scans block the release. No exceptions.
Control 4: Cloud storage access auditing in pipelines
If your build process uploads anything to cloud storage, that upload step should:
- Explicitly set the access policy (private by default)
- Verify the resulting permissions after upload
- Fail the pipeline if the object is not in the expected state
# After uploading to S3
aws s3api get-object-acl --bucket your-bucket --key your-artifact.zip \
| jq '.Grants[] | select(.Grantee.URI == "http://acs.amazonaws.com/groups/global/AllUsers")' \
| grep -q "." && echo "ERROR: Object is publicly accessible" && exit 1
This would have caught the public bucket exposure before the source map was published.
Control 5: Package signing and provenance
After publishing, anyone who installs your package should be able to verify it came from you and wasn’t tampered with in transit. npm supports provenance attestations via GitHub Actions — when you publish from a GitHub Actions workflow, npm records a cryptographic link between the published package and the specific workflow run that produced it.
- name: Publish to npm
run: npm publish --provenance
env:
NODE_AUTH_TOKEN: $
This doesn’t prevent a bad artifact from being published — but it creates an auditable chain of custody and makes tampering detectable.
Control 6: SBOM generation and verification
A Software Bill of Materials (SBOM) is a machine-readable inventory of every component in your artifact — your direct dependencies, their transitive dependencies, the versions pinned, and the licenses attached. Without one, you don’t know what you shipped. With one, you can query it.
Generate an SBOM at build time and attach it to every release:
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
artifact-name: sbom.spdx.json
format: spdx-json
output-file: sbom.spdx.json
- name: Scan SBOM for vulnerabilities
uses: anchore/scan-action@v3
with:
sbom: sbom.spdx.json
fail-build: true
severity-cutoff: high
The SBOM gives you three things: a vulnerability baseline you can query when a new CVE drops (did we ship the affected version?), a license compliance record (are we accidentally distributing GPL code in a proprietary product?), and an audit trail for incident response (what exactly was in the build that leaked?).
For npm packages specifically, cyclonedx-npm generates CycloneDX-format SBOMs directly from your lockfile:
npx @cyclonedx/cyclonedx-npm --output-file sbom.cdx.json
Store the SBOM alongside the release artifact. When a vulnerability is disclosed six months from now, you’ll be able to answer “were we affected?” in seconds instead of hours.
Control 7: Policy as Code for artifact gates
Manual checklists rot. Policies written as code don’t. Open Policy Agent (OPA) lets you express artifact release rules as Rego policies that are evaluated automatically in the pipeline — the same way linting rules are evaluated, but for security and compliance requirements.
A policy that would have directly blocked the Anthropic leak:
# policy/artifact.rego
package artifact
deny[msg] {
file := input.files[_]
endswith(file.path, ".map")
msg := sprintf("Source map file detected in artifact: %s", [file.path])
}
deny[msg] {
file := input.files[_]
endswith(file.path, ".env")
msg := sprintf("Environment file detected in artifact: %s", [file.path])
}
deny[msg] {
file := input.files[_]
file.size > 52428800 # 50 MB
msg := sprintf("File exceeds 50 MB size limit: %s (%d bytes)", [file.path, file.size])
}
deny[msg] {
input.total_size > 104857600 # 100 MB total package
msg := sprintf("Total artifact size exceeds limit: %d bytes", [input.total_size])
}
The size rule matters more than it might seem. A production npm package that’s suddenly 80 MB is almost certainly including something it shouldn’t — a source bundle, a node_modules directory, a build cache. A file-size gate on individual files and total package size catches accidental inclusions that file-pattern rules might miss.
Wire this into CI using the OPA CLI:
- name: Build artifact manifest
run: |
# Generate JSON manifest of packed files with sizes
npm pack --dry-run --json 2>/dev/null | jq '{
files: [.[] | .files[] | {path: .path, size: .size}],
total_size: ([.[] | .files[] | .size] | add)
}' > artifact-manifest.json
- name: Evaluate artifact policy
run: |
docker run --rm \
-v "$PWD:/workspace" \
openpolicyagent/opa:latest \
eval \
--data /workspace/policy/artifact.rego \
--input /workspace/artifact-manifest.json \
--fail \
"data.artifact.deny"
If any deny rule fires, OPA exits non-zero, the step fails, and the release is blocked. The policy lives in your repository, changes go through pull requests, and the audit trail is your git history.
Policy as code also handles edge cases that are hard to enumerate upfront. You can write rules like:
# Block files with no extension (often compiled binaries accidentally included)
deny[msg] {
file := input.files[_]
not contains(file.path, ".")
not startswith(file.path, "bin/")
msg := sprintf("Unexpected extensionless file: %s", [file.path])
}
# Block files modified within the last hour (catching uncommitted local changes)
deny[msg] {
file := input.files[_]
file.age_seconds < 3600
not startswith(file.path, "dist/")
msg := sprintf("Recently modified non-dist file in artifact: %s", [file.path])
}
The point isn’t to enumerate every bad thing — it’s to define what good looks like and reject anything that doesn’t match.
Control 8: Pre-release validation checklist (automated)
A pre-release validation stage is a dedicated pipeline job that runs immediately before the publish step and acts as the final gate. Unlike individual scan steps scattered across the pipeline, this stage aggregates all release-readiness checks into one place with a clear pass/fail signal.
# .github/workflows/release.yml (excerpt)
jobs:
pre-release-validation:
name: Pre-Release Validation
runs-on: ubuntu-latest
needs: [build, test]
steps:
- name: Verify clean git state
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "FAIL: Uncommitted changes present. Release from a clean tree only."
exit 1
fi
- name: Verify version bump is intentional
run: |
CURRENT=$(node -p "require('./package.json').version")
PUBLISHED=$(npm view $(node -p "require('./package.json').name") version 2>/dev/null || echo "0.0.0")
if [ "$CURRENT" = "$PUBLISHED" ]; then
echo "FAIL: Version $CURRENT is already published. Bump the version before releasing."
exit 1
fi
- name: Inspect artifact contents
run: |
npm pack --dry-run --json 2>/dev/null | jq -r '.[].files[].path' > artifact-files.txt
echo "=== Artifact contents ==="
cat artifact-files.txt
echo "=== End artifact contents ==="
# Blocked extensions
BLOCKED_EXTS="\.map$ \.env$ \.key$ \.pem$ \.p12$ \.pfx$"
for ext in $BLOCKED_EXTS; do
if grep -qE "$ext" artifact-files.txt; then
echo "FAIL: Blocked file type detected ($ext)"
grep -E "$ext" artifact-files.txt
exit 1
fi
done
- name: Check artifact size
run: |
TOTAL_SIZE=$(npm pack --dry-run --json 2>/dev/null | jq '[.[].files[].size] | add')
echo "Total artifact size: $TOTAL_SIZE bytes"
if [ "$TOTAL_SIZE" -gt 52428800 ]; then
echo "FAIL: Artifact exceeds 50 MB ($TOTAL_SIZE bytes). Investigate unexpected inclusions."
exit 1
fi
- name: Evaluate OPA artifact policy
run: |
# (OPA evaluation step from Control 7)
- name: Scan for secrets
uses: trufflesecurity/trufflehog@main
with:
path: .
base: HEAD~1
head: HEAD
- name: Verify SBOM exists and is current
run: |
if [ ! -f sbom.spdx.json ]; then
echo "FAIL: SBOM not found. Run SBOM generation before release."
exit 1
fi
SBOM_AGE=$(( $(date +%s) - $(stat -c %Y sbom.spdx.json) ))
if [ "$SBOM_AGE" -gt 3600 ]; then
echo "FAIL: SBOM is more than 1 hour old. Regenerate before release."
exit 1
fi
- name: Confirm all checks passed
run: echo "All pre-release validation checks passed. Safe to publish."
publish:
name: Publish
needs: pre-release-validation # blocked until validation passes
runs-on: ubuntu-latest
steps:
- name: Publish to npm
run: npm publish --provenance
env:
NODE_AUTH_TOKEN: $
The needs: pre-release-validation dependency means the publish job never runs if validation fails. This is structural enforcement — it’s not possible to accidentally skip the gate because the pipeline topology prevents it.
Key validations in this stage:
- Clean git state — no uncommitted changes can end up in the artifact
- Version check — prevents accidentally republishing an existing version
- Artifact content inspection — file extension blocklist, unexpected file detection
- Artifact size gate — catches surprise inclusions (anything over 50 MB is suspicious for a CLI tool)
- OPA policy evaluation — codified rules, not ad-hoc scripts
- Secret scanning — last line of defense before publish
- SBOM freshness check — ensures the SBOM was generated from this exact build, not a stale one
Supply Chain Security: The Broader Lesson
The Anthropic leak is a supply chain security incident, not just a packaging mistake. Supply chain attacks (and supply chain accidents) target the path between source code and users — the build system, the artifact registry, the packaging process, the publish step. Attackers exploit this path deliberately. Accidents exploit it through negligence.
The SolarWinds attack in 2020 compromised the build pipeline itself to inject malicious code into a signed, legitimate update. The event-stream npm incident in 2018 added a dependency to a popular package that stole credentials. These are adversarial. The Anthropic leak was accidental. The controls that prevent adversarial supply chain compromise are the same controls that prevent accidental leaks — because both exploit the same gaps.
A mature supply chain security posture includes:
Dependency pinning. Lock dependency versions. Use lockfiles. Verify checksums. A dependency update that wasn’t reviewed is a potential supply chain vector.
Build environment isolation. Build in clean, ephemeral environments (CI containers, not developer laptops). This prevents local environment contamination — developer tools, local credentials, debugging configurations — from appearing in production artifacts.
Reproducible builds. Given the same source, the build should produce the same artifact. This makes it possible to detect if a build system was tampered with.
Artifact immutability. Once published, an artifact shouldn’t be modifiable. npm’s immutability guarantees (you can’t republish the same version) are a feature, not a limitation. Build your workflows around this assumption.
What This Means for Your Pipeline
Most teams aren’t shipping AI developer tools to millions of users. But the failure modes here exist at every scale. A startup’s internal tool leak, a consulting firm’s client code accidentally included in a published library, proprietary business logic in a docker image pushed to a public registry — these happen regularly and for the same reasons.
The fixes aren’t expensive. They’re mostly configuration and pipeline steps that add seconds to a build:
- Set explicit file whitelists in your package manifests. Takes five minutes.
- Add a packaging inspection step to your CI pipeline. A shell script.
- Run a secret scanner on your artifacts before publish. Most have free tiers or open-source versions.
- Audit your storage permissions as part of your release process, not as a periodic manual review.
- Eliminate manual release steps wherever you can. If a human has to remember to do something, eventually they won’t.
- Generate and scan an SBOM at build time. Know exactly what you shipped, so you can answer vulnerability questions in seconds months later.
- Write your artifact rules as OPA policies — including file-size limits. A production npm package over 50 MB is almost certainly carrying something it shouldn’t.
- Add a dedicated pre-release validation job that gates the publish step. Make it structurally impossible to skip.
The lesson from Anthropic isn’t that sophisticated teams make amateur mistakes. It’s that the controls for preventing these mistakes are straightforward, well-documented, and available to every team — and the cost of skipping them is paid in incidents.
Summary
The Anthropic CLI leak was caused by five compounding failures: debug artifacts included in production builds, no file exclusion configuration, a public cloud storage reference, no CI validation gate, and manual deployment steps with no verification. Any one of these controls in place would have prevented the leak.
Modern DevOps isn’t just about shipping fast — it’s about shipping with confidence that what you’re shipping is exactly what you intended. Artifact hygiene, pipeline security gates, SBOM generation, policy-as-code enforcement (including file-size gates), secret scanning, and a structured pre-release validation job are the engineering practices that create that confidence. They’re not bureaucratic overhead. They’re how you avoid being the next case study.
Want to go deeper? The SLSA framework defines concrete supply chain security levels and the controls required at each level. The OpenSSF Secure Supply Chain Consumption Framework covers the consumption side — how to safely take in dependencies, not just how to publish safely.
16-Week Bootcamp
AI-Augmented Platform Engineering Bootcamp
Go deeper — hands-on Terraform, Kubernetes, GitOps, and AI-assisted operations. Build a complete internal developer platform from scratch.
Leave a Comment