security
-
Your AI Coding Agent Can Read Every Secret on Your Machine
Every developer running an AI coding agent has handed that agent the keys to their machine. Not metaphorically. Literally. The agent runs as your user. It can read every file you can read, execute every command you can execute, and hit every API your stored credentials authorize.
For most workflows, that’s the point. You want the agent to read your code, modify your project, ship your work. But there’s a quieter implication: the agent can also read your
.envfiles. It can invoke your secret-management tooling. It can grep forAPI_KEY=across your home directory. And nothing in the agent stack says “wait, you didn’t ask for this.”Same-UID isolation isn’t isolation. It’s the absence of isolation labeled politely.
The usual answer to “keep secrets safe from your coding agent” is: don’t store them where the agent can find them. Use a cloud secret manager. Rotate aggressively. These are good practices, and for local development, they’re often impractical. The agent is going to encounter secrets whether or not your security-best-practices doc approves.
So over the last week, I built an audit subsystem into lsm, my Local Secrets Manager. The whole thing is designed to answer one forensic question: did anything weird touch my secrets last night?
The Threat Model
A defense without a threat model is theater, so let me be specific.
The threat isn’t a sophisticated remote attacker. lsm is public, open-source code. The threat isn’t a buggy lsm either; bugs happen, and the user can read the source.
The threat is the agent layer running adjacent to lsm. Coding agents have legitimate access to a wide swath of your filesystem. They’re imperfect at intent inference. They sometimes get prompt-injected. They sometimes run in the background while you’re asleep. When an agent calls
lsm get prod DATABASE_URL, the action is indistinguishable from you doing the same thing. The audit log’s job is to make those calls retrospectively distinguishable.A secondary threat is an agent covering its tracks. If something reads a secret and then edits the audit log to erase the evidence, the log is worse than useless.
What Got Built
The audit subsystem records every access as a structured event: a sequence number, a timestamp, the action, the app and environment, an
Actorblock describing the calling process, and two cryptographic fields linking each event to the previous one.The
Actorblock was the interesting design problem. It captures parent process ID, parent process name, TTY device path (or empty if there’s no terminal), current working directory, an agent marker derived from environment variables that tools like Claude Code, Cursor, Aider, and Continue set, and the calling user ID. Every field is captured every time. Noomitempty. UID zero is a real, meaningful value, and silently dropping it would be a footgun.Events land in a hash-chained JSONL file at
~/.lsm/audit.jsonl. Each row carries the SHA-256 of the previous row plus its own body. If anyone edits, inserts, or deletes a row in the middle, the next row’sprevno longer matches andlsm audit verifysurfaces the break.The chain doesn’t catch tail truncation. If you chop off the end of the file, what’s left is internally consistent. A sidecar file storing the last expected hash is the obvious fix, and I deliberately rejected it. lsm is public code. Any local attacker who knows about the sidecar can rewrite both files in lockstep. Tail-truncation detection is deferred to the off-machine path: when events ship to a remote stack, the last hash naturally lives somewhere the local attacker doesn’t control.
Reading the Log
Three commands cover the read side.
lsm audit taildoes what you’d expect.lsm audit show <seq>prints a single event.lsm audit queryis the workhorse, with every field as a filterable dimension:--app,--env,--event,--parent-comm,--agent-marker,--tty present|absent,--since,--until. Output is JSONL when piped and columnar text when interactive.Then there’s
lsm audit suspicious, which runs four hard-coded detectors in one pass:- Outside hours. Events whose timestamps fall outside 07:00–23:00. The 3 a.m. canary.
- Burst. More than N events from a single parent process within a sliding window. The runaway-agent canary.
- New parent_comm. Process names not seen in the prior 30 days. The “what is this new thing” canary.
- Non-interactive, no agent. No TTY, no recognized agent marker. The “what is even running this” canary.
A single event can stack reasons. A 3 a.m. burst from an unknown parent is unambiguously interesting.
The detector doesn’t learn baselines, doesn’t call out to an ML model, doesn’t require a service. High-signal patterns are obvious patterns, and obvious patterns are well-served by hard-coded predicates.
Shipping Events Off the Box
If you already run an observability stack, lsm can ship audit events over OTLP (the OpenTelemetry wire protocol). Three design choices matter here.
The local file sink is always authoritative. The remote sink is a mirror, not a replacement. An lsm operation never fails because the remote endpoint is down.
Redaction is allowlist-based. App and environment names are HMAC-hashed with a per-host salt before becoming labels. The TTY device path is dropped and replaced with a
tty_present: true/falseboolean. Secret values,cwd,hash,prev, and the schema version never leave the host. Secret names are replaced withkey_present: truemarkers; the remote observer can see that a key was accessed, never which key.Events whose name starts with
audit.(chain failures, suspicious matches, sink drops) are always local. Telling a remote attacker that local integrity has been compromised is counterproductive.What’s Still Open
The most important non-feature: no command in lsm emits events yet.
setdoesn’t log.getdoesn’t log.deletedoesn’t log. The plumbing is complete, the calls are not wired in. Each emit site needs careful thought about which fields are appropriate, whether the event should be local-only, and how it interacts with sensitive operations. That’s the next chunk of work.The agent-coding era is normalizing a model where AI tools have wide-ranging access to developer machines. The premise that the agent operates as a fully-trusted local user is unlikely to change soon. Managing the risk means visibility. It means being able to answer “what touched my secrets last night” with a record the agent couldn’t silently rewrite.
The code is at github.com/llbbl/lsm. The full design lives in
docs/observability.md.I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
/ DevOps / AI / Programming / security
-
Buying Supply Chain Security in 2026: A Vendor Map
The last post was for solo developers and people without a security budget. This one is for everyone else: the platform engineers, the security leads, and the directors who are getting pitched by four different supply chain security vendors a week and need to figure out which, if any, of them are worth signing a contract with.
The honest answer is that the vendor landscape in 2026 is overheated. Every SCA company is now also a malicious-package firewall company. Every malicious-package firewall company is also pitching AI-native remediation. The pricing pages are mostly “Contact Sales.” And underneath all of it, the actual problem these tools solve splits cleanly into three layers, and you should know which layer you’re buying.
The Three Layers
Layer 1: Update automation. Dependabot (free, GitHub-native) and Renovate (free, more configurable) generate pull requests when new versions of your dependencies are released. They don’t find vulnerabilities. They just shrink the window where you’re running outdated code. Dependabot is the right answer for most teams under 50 engineers. Renovate is what you reach for when you’re tired of triaging 80 individual PRs a week and want grouped updates with auto-merge based on community confidence signals. Neither costs anything. Both should be on.
Layer 2: Software Composition Analysis (SCA). Parses your lockfiles, matches dependencies against CVE databases, tells you what’s vulnerable. The open-source side of this is fully mature: Trivy, Grype, OWASP Dependency-Check, and OWASP Dependency-Track collectively cover most of what you’d pay Snyk for ten years ago. Dependency-Track in particular is a serious tool. It ingests CycloneDX and SPDX SBOMs, tracks portfolio-wide risk, and integrates EPSS scoring. If you self-host it, the bill is zero.
The thing the commercial vendors actually sell at this layer is reachability analysis. A vulnerability in a transitive dependency that you import but never actually call is technically a CVE in your inventory. Realistically it’s noise. Snyk, Endor Labs, and Mend.io all build call-graph analysis that determines whether a vulnerable code path is actually invoked by your application. Endor Labs claims their reachability reduces actionable alerts by 90 to 95%. That number is marketing, but the underlying capability is real, and it’s the single biggest differentiator between commercial SCA and the open-source stack.
Layer 3: Malicious package firewalls. This is the layer that didn’t exist five years ago. Tools like Socket, Phylum, Endor Labs, and Sonatype Repository Firewall sit between your developers and the public registries and analyze package behavior before installation. Socket evaluates 70+ behavioral indicators: does the package read OAuth tokens from disk, does it use
marshal.loadsto self-deobfuscate, does it inject into HTTP headers. This is the only layer that defends against zero-day malicious packages, because SCA fundamentally can’t. There’s no CVE for “this package was uploaded ten minutes ago and steals AWS keys.”What This Actually Costs
The pricing pages tell you most of what you need to know about who each vendor is for.
Vendor Pricing Who it’s for Dependabot Free Everyone on GitHub Socket Free up to 1000 scans/mo, Team $25/dev/mo, Business $50/dev/mo Developers who want low-friction zero-day protection Snyk Free tier (100-300 tests/mo per product), Team $25/dev/mo (5-10 dev cap), Ignite ~$105/dev/mo, Enterprise custom Teams that want SCA + SAST + IDE integration in one bundle Endor Labs Custom (free tier for small OSS teams) Orgs drowning in CVE noise; multi-language including C/C++ and Rust Mend.io $300-$1000/dev/year Enterprise environments that want consolidated dashboards Sonatype $6K-$150K+ in bundled tiers Large regulated enterprises that need a centralized artifact gateway Phylum Custom enterprise Teams that want programmatic policy via Open Policy Agent Two patterns stand out. Socket and Snyk are product-led growth plays with transparent per-developer pricing, predictable as you scale, accessible at the lower end. Sonatype, Mend.io, and Phylum are enterprise sales motions with significant minimums and multi-month implementation cycles. Endor Labs sits awkwardly in the middle (mid-market and enterprise deals) with credible reachability claims that are hard to replicate with open source.
The Real Cost of “Free”
The argument for going all-in on open source, Dependabot plus Trivy plus Dependency-Track plus maybe Socket’s free tier, looks compelling on the spreadsheet. The honest math is more complicated.
Running this stack at a 100-engineer organization requires somebody to maintain the Dependency-Track server, tune the rulesets to keep false positives from drowning your security team, manually triage alerts that have no reachability context, and respond to the inevitable “is this critical CVE actually exploitable in our environment?” questions from leadership. Realistic estimates put that workload around 20 to 30 hours per week — call it half an FTE of senior engineering time, which fully-loaded lands in the low six figures per year. That’s not zero, and it’s the line item that “we’ll just use open source” plans consistently leave out of the spreadsheet.
The flip side is the Endor Labs ROI pitch: 90% noise reduction means 9 fewer FTEs needed for triage in a 300-dev org, which they price at roughly $1.5M in saved salary against a five-figure license. That’s a vendor calculation, so take it with the appropriate salt. But the underlying logic that alert noise has real labor cost is correct, and it’s the part most “we’ll just use open source” plans underestimate.
What I’d Actually Recommend
For a team of 5 to 50 engineers: Dependabot or Renovate on, Socket’s free tier or paid Team plan for firewall coverage, and
npm audit/pip-audit/cargo-auditrunning in CI. Total spend: $0 to roughly $1,500/month at the high end. This is the configuration that covers 80% of the threat for a small fraction of what a Snyk or Mend contract costs.For 50 to 300 engineers: the math starts favoring a paid SCA platform with reachability. Snyk if you also want SAST in the same tool. Endor Labs if you have a polyglot codebase (especially anything with C++ or Rust) and severe alert fatigue. Keep Socket or Phylum as a separate firewall layer. The firewall vendors are still meaningfully better at malicious-package detection than the SCA vendors who bolted it on.
For 300+ engineers in a regulated industry: you probably need Sonatype or JFrog as a centralized proxy whether you want them or not, because compliance demands a single audited path from developer to registry. Bundle it with Endor Labs or Mend for the reachability layer.
What I would not do is buy the platform pitch, the “one tool for SCA + SAST + secrets + container scanning + firewall + AI remediation.” Those bundles exist because the vendors want a bigger contract, not because the unified product is actually best-of-breed at any single thing. The companies winning each individual layer (Socket for firewalls, Endor Labs for reachability, Trivy for open-source SCA) are doing so by being focused.
Closing the Series
Four posts in: the threat model, the per-ecosystem mitigations, local isolation for the budget-constrained, and now the commercial landscape for everyone else. The unifying thesis across all of them is that supply chain security is not solved by a single tool or a single layer. It’s a stack. Lockfiles at the bottom, audit tooling above that, behavioral analysis on top, isolation as the last line of defense. The right composition depends on who you are and how much risk you can afford to absorb. If your stack right now is “we trust the registry,” you are the threat model.
Sources
- Supply Chain Security Tool Selection Framework - SoftwareSeni
- Endor Labs vs Snyk: SCA, SAST, and Containers Compared
- Malware Package Firewall: Block Threats Before They Hit Your Code
- Socket Pricing
- Introducing Socket Firewall
- Snyk Software Pricing & Plans 2026 - Vendr
- Endor Labs Pricing
- Mend.io Pricing
- Sonatype Nexus Pricing Guide 2026 - CloudRepo
- Open Source vs Commercial SCA Tools Comparison - Safeguard
- OWASP Dependency-Track
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
/ DevOps / security / Tooling / Supply-chain
-
Sandboxing AI Agents Without Buying Anything
The previous post (and the one before it) covered the threat model and the per-ecosystem mitigations: lockfiles,
--ignore-scripts,cargo-audit, Trusted Publishing. All of that helps. None of it answers the question that keeps me up at night, which is: what happens when an AI agent on my laptop installs a malicious package, and the malicious package was the literal point of the operation?This is the new shape of the threat. You’re not getting compromised because you typed
npm installwrong. You’re getting compromised because Claude or Cursor confidently invented a package name that didn’t exist, an attacker registered it five hours ago, and the agent ranpip install hallucinated-thingon your behalf without asking. The agent has shell access. Your SSH keys are right there. Your~/.aws/credentialsfile is right there. The entire premise of giving an AI agent the ability to just figure it out depends on it being able to execute untrusted code at the speed of conversation, which is also the worst possible threat model.If you’re a solo developer, an open-source maintainer, or a startup with no budget for Socket or Endor Labs licenses (more on those next post), the answer isn’t a commercial firewall. The answer is local isolation, and the tools have gotten dramatically better in the last 18 months.
Containers as the Baseline
The minimum viable isolation in 2026 is don’t run untrusted code as your user on your host OS. The cleanest way to do that on macOS or Linux is a devcontainer, a fully described, reproducible Linux environment that VS Code, Cursor, and the Claude Code CLI all natively support. You give the agent the container as its sandbox. Project files mount in. SSH keys, AWS credentials, and the rest of your home directory don’t.
The container runtime matters. Docker Desktop on macOS is a memory pig, 3 to 4 GB resident at idle, with sluggish startup times that make iterative work miserable. OrbStack is the obvious replacement: free for personal use, native Apple Silicon, dynamically allocates memory instead of reserving fixed blocks, and benchmarks show container startup times around 0.2 seconds versus Docker Desktop’s multi-second cold starts. If Docker Desktop is eating half your RAM before you even start Claude Code, OrbStack will give you that memory back.
The thing to internalize, though, is that a container is not a security boundary by default. It’s a deployment mechanism that happens to have isolation properties when configured correctly. Misconfigured developer containers have been implicated in some of the largest crypto-industry breaches of the last few years. The pattern: a container running with privileged flags, or mounting the wrong host directory, turns into a path straight to the host. Containers help. They don’t save you from yourself.
The configuration mistakes that void the isolation:
- Mounting
~/.sshinto the container so the agent cangit push. Now any process inside the container can read your SSH keys. - Mounting your entire home directory as a convenience. Now everything is accessible.
- Running with
--privilegedor sharing the host’s Docker socket. Container escape becomes trivial. - Letting the agent run
sudoinside the container. The container’s root can chain to host kernel exploits.
Least privilege, applied seriously. The agent gets the project directory and nothing else. If it needs to commit, it pushes through a credential helper that lives on the host, not by mounting your SSH keys.
Lighter-Weight Sandboxes
Spinning up a full container for every test this snippet the LLM wrote interaction is too heavy. There’s a middle layer worth knowing about.
Python. Pyodide compiles CPython to WebAssembly, which means Python code runs in a deny-by-default memory sandbox with no filesystem or network access unless you explicitly grant it. Works great for evaluating LLM-generated snippets, struggles with C extensions and heavy dependencies. safe-py-runner is the pragmatic alternative: it runs Python in a restricted subprocess with timeouts, memory limits, and I/O marshaling. No container needed. For code that absolutely cannot touch your machine, remote V8-isolate services like Deno Sandbox boot pre-snapshotted Python environments in the cloud and air-gap execution entirely.
Rust. The
build.rsproblem from the last post has no first-class solution yet, but on Linux you can wrapcargo buildin Landlock, a kernel feature available on 5.13+ that lets unprivileged processes restrict their own filesystem access. Combined with seccomp-bpf for syscall filtering and cgroups v2 for resource limits, you can run a build script that genuinely cannot read your SSH keys or open arbitrary network sockets. Projects like sandbox-rs wrap these primitives into something usable without writing your own seccomp filters. None of this works on macOS without a Linux VM in the way, which is another reason OrbStack plus a devcontainer is the path of least resistance for most people.The Mindset Shift
The honest version of all of this: if you’re running AI agents locally, you have to assume they will eventually install something malicious. Not might. Will. The question is whether the blast radius is the contents of one project directory inside a container, or every credential on your machine plus your entire git history. That gap is what isolation buys you.
Containers, Landlock, WASM sandboxes, none of these are particularly hard to set up. They’re just things most developers haven’t bothered with because the threat model didn’t feel real. After Shai-Hulud, faster_log, and a year of watching AI agents
pip installwhatever they invent, the threat model is real.Next post I’ll wrap up the series with the commercial side: Socket, Snyk, Endor Labs, Mend, Sonatype, the pricing comparison, and the actual ROI math for whether any of it makes sense for teams below 50 developers.
Sources
- State of Dependency Management 2025 — Endor Labs
- Securing AI Coding Assistants: A Total Cost Analysis — Endor Labs
- A step closer to isolation — devcontainer-wizard — The Red Guild
- OrbStack vs Docker Desktop: Performance Facts for Mac
- Apple Containers vs Docker Desktop vs OrbStack benchmark
- How to Safely Run AI Agents Like Cursor and Claude Code Inside a DevContainer
- DevContainers for Secure AI: Isolated & Scalable
- safe-py-runner: Secure Python execution for LLM Agents
- mcp-run-python — Pydantic
- How to Run Rust Binaries Without Root Using Sandboxing — OneUptime
- sandbox-rs
- Explore sandboxed build scripts — Rust Project Goals
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
/ DevOps / AI / security / Supply-chain / Containers
- Mounting
-
Python and Rust Have the Same Supply Chain Problem as NPM
Last post I walked through the threat model for supply chain attacks and dug into the NPM ecosystem specifically: postinstall scripts,
npm ci, pnpm’s release-age cooldown. The same structural problems exist in Python and Rust, but the failure modes are different and the tooling has evolved in some surprising directions. Worth understanding both, because if you write any backend code in 2026 you’re probably touching at least one of these ecosystems.Python: setup.py Is a Remote Code Execution Primitive
The thing most Python developers don’t appreciate is that
pip installruns arbitrary code by default. Not after install. During install. If a package ships asetup.py, that file is executed in a Python interpreter the moment pip resolves the dependency. Whatever the author wrote, including reading~/.aws/credentials, scraping environment variables, or opening a reverse shell, runs as your user with full filesystem access.This is the part that confuses people coming from other ecosystems:
venvandvirtualenvdon’t help. They isolate Python package versions to avoid conflicts. They are not a security boundary. A package installed inside a virtualenv has the exact same privileges as the user who ranpip install. None of this is a bug, exactly. It’s just an artifact ofsetup.pybeing a regular Python script that pip has always been willing to execute.The defense-in-depth stack for Python looks like this:
Stop using pip. I mean it. pip is the worst package manager in mainstream use today and it is the single biggest reason Python’s supply chain story is a disaster. It has no native lockfile.
requirements.txtis a shopping list, not a lockfile; it tells pip what to fetch, not what you actually got last time. Runpip install -r requirements.txttwice on two different days and you can get two different dependency trees, because pip resolves transitive deps fresh every time against whatever happens to be on PyPI in that moment. Builds aren’t reproducible. Hashes aren’t verified by default. There’s no separation between “what I asked for” and “what was actually resolved.”Every other ecosystem solved this a decade ago. npm has
package-lock.json. Cargo hasCargo.lock. Bundler hasGemfile.lock. pip has vibes.The
--require-hashesflag exists, technically, but it’s duct tape on a broken design. You have to generate the hashes with a separate tool (pip-tools), maintain them by hand, and remember to pass the flag on every install. Nobody does this in practice. The Python Packaging Authority spent fifteen years insisting pip was fine while every other community built proper lockfile-based managers.Use uv or Poetry. Both produce real lockfiles with SHA-256 hashes for every direct and transitive dependency, both make installs reproducible by default, both are dramatically faster than pip. uv in particular is the obvious default for new projects in 2026, it’s a drop-in replacement that’s roughly 10-100x faster and treats the lockfile as a first-class artifact instead of an afterthought. Hash verification isn’t a flag you have to remember. It’s how the tool works.
This doesn’t protect you from a malicious package you pinned on day one. But it does slam the door on silent registry tampering, makes “what’s actually deployed?” a question with an answer, and gets you out of the pip swamp.
pip-auditfor known vulnerabilities. Scans your environment or requirements file against the OSV database, PyPA advisories, and GitHub advisories. Run it in CI. Combined with a real lockfile you get a tight loop: pin exact versions, scan those versions for CVEs, fail the build if anything critical shows up.Trusted Publishing (OIDC). If you maintain a package on PyPI, get rid of your long-lived API token and switch to OIDC-based publishing. Your CI runner generates ephemeral, short-lived tokens scoped to a specific repository, branch, and workflow. Leaked PyPI tokens have been the source of multiple high-profile compromises. Trusted Publishing makes the credential effectively un-leakable because it doesn’t exist as a persistent secret.
The thing I’d actually call out, though, is that none of the Python tooling addresses the
setup.pyexecution problem at install time. Hash pinning verifies you got the right bytes. It doesn’t tell you those bytes aren’t malicious. For that you’re back to either sandboxing the install (Docker, devcontainers) or trusting the registry’s malware detection, which lags by hours to days.Rust: The Safety Guarantees Stop at the Compiler
Rust’s reputation for safety is real, but it’s a property of the compiled language, not the supply chain. The borrow checker doesn’t help you when the crate you’re depending on exfiltrates your SSH key during
cargo build.The mechanism is
build.rs. Crates can include a build script that runs before the compiler, with full user privileges. Procedural macros do the same thing at compile time. In both cases, the code can read files, open network sockets, do whatever it wants. A maliciousbuild.rsis effectively an unsandboxedunsafeblock that bypasses code review because nobody reads build scripts. The Rust core team has been discussing sandboxing for years, but nothing has shipped.This isn’t theoretical. Two examples from the last six months:
- September 2025:
faster_logandasync_printlnwere caught scraping Ethereum and Solana private keys at runtime and exfiltrating them to Cloudflare workers. - March 2026:
chrono_anchor,dnp3times, andtime-sync, all masquerading as time utilities, were transmitting.envfile contents to threat actors.
Both clusters used compromised GitHub OAuth credentials to push under legitimate-looking namespaces. crates.io authenticates via GitHub, so a phished GitHub account is a phished crates.io account.
The defensive tooling is actually better than what most ecosystems have:
Tool What it does cargo-auditScans Cargo.lockagainst the RustSec Advisory Database. Run in CI.cargo-denyLints the dependency graph. Block specific crates, enforce license policies, restrict registries. cargo-crevDecentralized “web of trust” where developers cryptographically sign crate reviews. Elegant, but heavy lift in practice. cargo-vetMozilla’s pragmatic answer to crev. Centralized audit records per org, with the ability to import audits from peer orgs (Google, Mozilla, Embark) instead of re-auditing every transitive dep yourself. If you’re picking one to start with,
cargo-auditis the easy baseline. It’snpm auditfor Rust and you should be running it in CI yesterday.cargo-denyis the next step up. It lets you actually enforce policy, which is what you want once you’ve usedcargo-auditlong enough to be tired of triaging the same warnings.cargo-vetis the interesting one for any team beyond about five engineers. The insight is that you don’t actually need to audit every crate. You just need to know that someone you trust did. By importing audit records from Mozilla and Google, a small team can effectively delegate the audit work for hundreds of common dependencies without running anything themselves. It’s the closest thing the Rust ecosystem has to a working trust network, and it works because the cryptographic overhead lives at the org level instead of being pushed onto individual developers.The Pattern Across All Three Ecosystems
NPM, PyPI, and crates.io all share the same fundamental design flaw: package installation executes attacker-controlled code by default. NPM has
postinstall. Python hassetup.py. Rust hasbuild.rsand proc macros. Different files, same problem.The mitigations also rhyme. Lock your versions to specific hashes. Run an audit tool in CI. Where possible, prevent install-time execution entirely (
--ignore-scripts, pre-built wheels, sandboxed build scripts when they finally land in Cargo). Where you can’t, isolate the install with devcontainers, ephemeral CI runners, anything that contains the blast radius when a dependency turns out to be hostile.Next post I’ll get into the isolation side specifically: devcontainers, OrbStack, Landlock, and the practical question of how a solo developer with no security budget actually keeps their laptop from getting owned by an AI agent that just
pip installed a hallucinated package name.Sources
- Securing Package Managers: Why NPM, PyPI, and Cargo Are High-Value Targets
- Defense in Depth: A Practical Guide to Python Supply Chain
- PyPI Security: How to Safely Install Python Packages
- Rust Supply Chain Security — Managing crates.io Risk
- crates.io: Malicious crates faster_log and async_println
- Five Malicious Rust Crates and AI Bot Exploit CI/CD Pipelines
- About RustSec Advisory Database
- cargo-vet FAQ
- Auditing Rust Crates Effectively (arXiv)
- Explore sandboxed build scripts — Rust Project Goals
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
/ DevOps / Python / security / Rust / Supply-chain
- September 2025:
-
Your Software Is Mostly Strangers' Code
Modern applications aren’t really written anymore. They’re assembled. Seventy to ninety percent of a typical proprietary codebase is open-source code pulled from public registries, NPM, PyPI, crates.io, maintained by thousands of people you’ve never met. Every
npm installis an act of implicit trust extended to strangers, and that trust model has quietly become the weakest link in most security architectures.Attackers figured this out a long time ago. Compromising one popular package gives you a blast radius that phishing campaigns can only dream about: CI pipelines, developer laptops, production workloads, client devices, all simultaneously. SolarWinds. The XZ Utils backdoor. The Shai-Hulud worm, which self-propagated through 170+ npm and PyPI packages by hijacking GitHub Actions OIDC tokens and quietly minted new publish credentials as it spread. The ByBit developer compromise. These aren’t outliers anymore. They’re the shape of the threat.
I want to dig into the mechanics of how this actually happens, and then look at the ecosystem most developers touch every day: NPM.
How Supply Chain Attacks Actually Work
The first thing to understand is that supply chain attacks aren’t really “vulnerabilities” in the classic sense. A buffer overflow is an accidental weakness. A malicious package is intentional code, written to steal credentials, drop a reverse shell, or exfiltrate environment variables the moment it lands on your machine. Traditional appsec tools were built to find the former. They are largely blind to the latter.
The attack patterns cluster into a few categories.
Typosquatting. Publish
axoisand wait for someone to fat-fingeraxios. Sounds trivial, but it works constantly because developers install packages at high velocity and rarely double-check spelling.Dependency confusion. If your company has an internal package called
corp-auth, an attacker publishes a public package with the same name and a higher version number. Many package managers default to “highest version wins,” and your build pulls the public one instead of your internal one.Maintainer hijacking. Compromise a real maintainer through phishing, credential stuffing, or a missing 2FA setup, and push a poisoned update to a package that already has millions of weekly downloads. The Axios compromise in March 2026 followed exactly this pattern. The XZ Utils backdoor was a slower variant. The attacker spent months building trust as a “helpful” co-maintainer before slipping a backdoor into the build.
The thing that makes all of this so effective is the automation downstream. Unpinned versions, auto-merging update bots, transitive dependencies five layers deep. Once a malicious version hits the registry, it propagates fast.
Why NPM Is the Highest-Stakes Ecosystem
NPM serves tens of billions of downloads a week. A typical JavaScript project today pulls in well over a thousand transitive dependencies. Ten years ago that number was in the dozens. The dependency graph is just structurally enormous, and it’s getting worse.
The specific architectural problem in NPM is the lifecycle script, specifically
postinstall. NPM lets package authors define scripts inpackage.jsonthat run automatically when the package is installed. This was designed for legitimate reasons: compiling native bindings, configuring environments. But it also means arbitrary shell commands execute on your machine the moment you typenpm install. No code review. No second thought. Just immediate execution as your user.There are a few practical mitigations, and they’re worth knowing whether you’re a solo developer or running platform security at a large org.
Disable lifecycle scripts. Either pass
--ignore-scriptsad hoc, or set it globally:npm config set ignore-scripts trueThis breaks some legitimate packages (esbuild, bcrypt, anything compiling native code). To manage that, tools like
can-i-ignore-scriptsscan yournode_modulesand generate an allowlist of packages that genuinely need scripts to run. Frameworks like@lavamoat/allow-scriptsformalize this with a deterministic config you can check into the repo.Use
npm ciin CI, notnpm install. This is non-negotiable for production builds.npm installwill happily resolve newer minor versions inside your semver ranges and rewritepackage-lock.json.npm cirefuses to do that. If the lockfile doesn’t match exactly, the install fails. That’s the behavior you want when the question is “did anything change that I didn’t approve.”Consider switching to pnpm. pnpm 10+ has been quietly building some of the best structural defenses in the ecosystem. Postinstall scripts are off by default and require an explicit
allowBuildslist.blockExoticSubdepsprevents transitive deps from resolving via random Git URLs or tarballs.The killer feature, though, is
minimumReleaseAge. As of pnpm v11 (May 2026), the default is 1440 minutes, so pnpm simply refuses to resolve any package version less than 24 hours old. Most malicious packages get pulled from the registry within hours of being detected. A 24-hour cooldown turns the community into your early warning system, with no behavioral analysis or commercial tooling needed.That last one is the single highest-leverage change you can make as an individual developer. It costs nothing, it doesn’t break your workflow, and it neutralizes most day-zero registry malware before it ever reaches you.
Next post I’ll dig into the Python and Rust sides of this. pip’s
setup.pyexecution problem, Rust’sbuild.rsissue, and the surprisingly mature auditing toolchain the Rust community has built aroundcargo-audit,cargo-deny, andcargo-vet.Sources
- Securing Package Managers: Why NPM, PyPI, and Cargo Are High-Value Targets
- Defending Against NPM Supply Chain Attacks: A Practical Guide
- NPM Ignore Scripts Best Practices
- Mitigating supply chain attacks
- Get safe and remain productive with can-i-ignore-scripts
- The Landscape of Malicious Open Source Packages: 2025 Mid-Year Threat Report
- The Evolving Software Supply Chain Attack Surface
- Introducing OpenSSF’s Malicious Packages Repository
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
/ DevOps / security / javascript / Npm / Supply-chain
-
SAST vs AI PR Review: Two Tools, Different Jobs
If you have worked in DevSecOps, you might be wondering if AI pull request review tools are going to replace traditional SAST scanners. Short answer: no. Longer answer: they’re solving different problems, and if you’re picking one over the other, you might be making a mistake.
Here is how I think about it.
SAST is the Compliance Gatekeeper
Static Application Security Testing tools, think Semgrep, SonarQube, Checkmarx, Fortify, parse your source code (usually into an Abstract Syntax Tree) and hunt for known vulnerability patterns. They don’t run the code. They just read it and “pattern-match” against rules.
The focus here is security, compliance, and strict rule enforcement. SAST is the automated gatekeeper that makes sure your code clears the OWASP Top 10 bar before it merges.
What SAST does well:
- It’s deterministic. If a rule matches a pattern, the engine flags it every single time. Run it twice on the same code, get the same result.
- It satisfies auditors. Frameworks like PCI-DSS, SOC 2, and HIPAA expect documented secure-development practices, and a formal SAST scanner is the easiest way to produce that evidence. AI agents don’t count here, at least not yet.
- It can do real taint analysis. Enterprise tools can track untrusted input from the moment it enters your app to the moment it hits a dangerous sink.
Where SAST falls down:
- The false positive rate is brutal. Rigid rules with no context means a lot of noise. Developer fatigue is real, and once your team starts ignoring scanner output, you’ve lost the game.
- It can’t see your business logic. A SAST tool has no idea what your application is supposed to do, so it can’t tell you when the logic itself is broken.
- Comprehensive scans are slow. Hours on large codebases isn’t unusual, though Semgrep has been doing good work on this front.
AI PR Agents are the Peer Reviewer
Tools like CodeRabbit, Qodo, Greptile, GitHub Copilot Code Review, Cursor Bugbot, and Claude Code (set up as a review skill) plug into your version control and read the PR diff with the surrounding code context. They behave less like a scanner and more like a colleague who actually read your changes.
The focus is developer productivity, code quality, logic bugs, and contextual feedback.
What they do well:
- They understand intent. LLMs can reason about why the code is changing, not just whether it matches a rule. That’s a different category of feedback.
- The signal-to-noise ratio is good. When an AI flags something, it usually comes with an explanation that makes sense. Less noise, more useful comments.
- They suggest fixes. Not just “this is wrong” but “here’s a diff you can apply.” That’s huge for actually closing the loop on review feedback.
- The scope is broader. Architecture, performance, style, security, all in one pass.
Where they fall down:
- They’re non-deterministic. Same vulnerability, two PRs, two different outcomes. That’s not a bug, that’s how LLMs work, and it’s why auditors don’t trust them.
- They don’t satisfy compliance. No auditor is going to accept “the AI looked at it” as a substitute for a formal scanner.
- Hallucinations happen. Invented issues, misread intent, suggestions that refactor things that didn’t need refactoring. You still need a human filtering the output.
The Quick Comparison
Feature SAST AI PR Review Primary Goal Security & Compliance Code Quality & Productivity Analysis Method Deterministic rules & AST Non-deterministic LLMs Business Logic Blind Context-aware False Positives Often high Usually low Compliance Proof Accepted as evidence Not accepted Feedback Loop Dashboard / CI output PR comments / chat The Lines Are Starting to Blur
The interesting thing happening right now is convergence from both directions.
On the SAST side, tools like DryRun Security are pitching themselves as “AI-native SAST,” trying to keep the deterministic backbone while using LLMs to filter out the false positives that make traditional scanners painful to live with.
On the AI agent side, CodeRabbit and Greptile keep getting better at catching real security vulnerabilities, not just style issues. They’re slowly creeping into territory that used to belong exclusively to SAST.
This is going somewhere, but it’s not there yet.
Where to Start Your Evaluation
Treat them as complementary, not competitive.
For SAST, evaluate against your audit footprint, the languages in your codebase, and how much false-positive triage your team can absorb. Semgrep, SonarQube, Checkmarx, and Fortify all sit in different price-and-friction zones, and the right one depends on what your business actually needs to prove.
For AI PR review, evaluate based on how it fits your existing review workflow, what languages and frameworks it understands well, and the signal-to-noise ratio in practice on your codebase. CodeRabbit, Qodo, Greptile, Copilot Code Review, Bugbot, and a Claude Code review skill all approach the problem differently.
If you pick one category and skip the other, you’re either passing compliance with mediocre code review, or getting great review feedback while failing your next audit. Neither is a win.
The AI tools aren’t replacing SAST. They’re filling in the gap SAST was never designed to cover.
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
/ DevOps / AI / Programming / security
-
npmx.dev Is the NPM Frontend We've Been Asking For
If you’ve spent any time on npmjs.com, you know the drill. You land on a package page, eyeball the tarball size, squint at the dependency list, then bounce out to bundlephobia, then to Are The Types Wrong, then to Socket.dev, just to figure out if this thing is safe to install. That’s not a workflow. That’s a scavenger hunt.
For years, the JavaScript community has been filing requests on the official npm tracker asking for this stuff to live in one place. As Andrew Nesbitt notes, native dark mode was the single most upvoted request on the tracker for something like five years before it shipped. Real install sizes (transitive, not just the tarball). UI warnings about compromised packages and hidden postinstall hooks. Concrete version resolution instead of squinting at semver ranges. The list is long, and it mostly went nowhere.
So the community built npmx.dev instead.
What It Actually Is
It’s important to note: npmx.dev is not a separate registry. It doesn’t mirror packages, and
npm installstill pulls from the official Microsoft/GitHub-owned registry. What npmx is, strictly, is an alternative frontend. It hits the official npm APIs in real time, caches the results at the edge, and renders a much better page.The stack is Nuxt on top of the full VoidZero toolchain with Vite, Vitest, Rolldown, oxlint, oxfmt, and the other usual packages you’d expect. It also leans heavily on SSR and ISR for near-instant page loads. VoidZero (the company behind Vite) sponsors the project, which makes sense, since it’s open-source MIT and the operational costs are basically web traffic and compute, not package storage.
What You Actually Get
I really like the dependency view. Instead of a flat list of what the package declares, you get an expandable tree with recursive vulnerability tracking via OSV. You can drill into transitive dependencies and see which ones are flagged. That’s the thing every security-conscious developer has been doing manually for years.
A few other wins worth calling out:
- Real install size. Not the tarball. The actual disk footprint with all dependencies resolved.
- postinstall scripts surfaced. If a package is going to run arbitrary code on your machine at install time, npmx puts that on the page where you can see it. Not buried in the manifest.
- Concrete resolved versions displayed alongside the semver range, so you know what you’re actually getting.
- Banners suggesting lighter alternatives for bloated legacy packages. Opinionated, and I’m here for it.
A Quick Note on OSV
The vulnerability data comes from the OSV ecosystem, and it’s worth understanding what that is, because it’s one of the more important pieces of open-source infrastructure right now.
OSV is a centralized aggregator that pulls from GitHub Security Advisories, PyPA, RustSec, the Global Security Database, and many ecosystem-specific feeds. The schema is standardized JSON, machine-readable, and maps vulnerabilities to exact versions or commit hashes. That matters because it’s what lets automated scanners avoid the false-positive flood that makes most security tooling annoying to use.
It’s an OpenSSF project under the Linux Foundation. Google maintains the infrastructure; GitHub, the Rust Foundation, the PyPA, Red Hat, and a long list of ecosystem maintainers contribute data. Vendor-neutral, free, and the boring kind of governance you want for security data.
Making It Your Default
Here is how you can make it the actual default npm frontend. Since npmx.dev maintains URL parity with npmjs.com, you can redirect every npm link you click without thinking about it. A couple of options depending on your setup.
A few options:
Kagi users can add a global URL redirect rule:
^https://www.npmjs.com|https://npmx.devClick an npm link in your search results, land on npmx instead.
Browser extensions work for everyone else. There’s a dedicated
npmx-redirectextension for Chrome and Firefox that does only this one thing. Or use Redirector with a regex rule:- Include:
^(?:https?://)?(?:www\.)?npmjs\.com/(.*) - Redirect to:
https://npmx.dev/$1
Custom site search. In Chrome, Brave, Arc, or Safari, add a new site search with shortcut
nx(or@npm) pointing at:https://npmx.dev/search?q=%sNow you type
nx, space,zod, enter. Done. You never see npmjs.com again unless you want to.Why This Matters
The npm registry is critical infrastructure for, conservatively, a huge chunk of the software running on the internet. The fact that the community had to build its own frontend to surface basic security and observability data tells you something about where the priorities have been.
I’ll be using npmx as my default going forward. I’m not going back.
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
Sources
/ open source / security / javascript / Tooling / Npm
-
Your Data Lake's Vulnerability Problem Is Really an Identity Problem
I’ve been reading through the post-mortems on the last few years of data lake breaches, and the pattern is depressing. We keep blaming the platforms. We should be blaming ourselves.
Let me give you an example.
The Snowflake Breach Wasn’t a Snowflake Breach
In mid-2024, at least 165 organizations got hit through their Snowflake instances. AT&T lost over 50 billion call records. Ticketmaster, Santander, Advance Auto Parts. The headlines wrote themselves: Snowflake hacked.
Except Snowflake wasn’t hacked. Mandiant, CrowdStrike, and Snowflake all reached the same conclusion in their forensics. No zero-day. No flaw in the cryptographic platform. No internal compromise of Snowflake’s corporate network. No brute-force attacks against API limits.
What actually happened? UNC5537, a financially motivated group also tracked as Scattered Spider and ShinyHunters, walked through the front door with valid stolen credentials. Those credentials were harvested over years by commodity infostealer malware (VIDAR, LUMMA, REDLINE) running on the personal laptops of third-party contractors. The same laptops these contractors used for gaming and pirated software also held the keys to their clients' enterprise data lakes.
One contractor laptop. Multiple enterprise environments compromised. That’s the actual story.
79.7% of the accounts UNC5537 used had prior credential exposure. Some had been valid and un-rotated since November 2020.
The Two Doors They Walked Through
The first attack vector was the SSO side door. Plenty of victim organizations had a perfectly fine enterprise IdP enforcing strong passwords and MFA. They just forgot to make SSO mandatory. A local authentication pathway was left active alongside it. Attackers logged in directly with stolen local credentials, completely bypassing the IdP, and the MFA requirement never fired.
The second was credential stuffing against inactive, orphaned, and demo accounts belonging to former employees. Nobody audits those. Nobody enforces MFA on those. So they don’t get protected by the controls that exist on the production accounts.
Once inside, the kill chain was almost boring.
SHOW TABLESto enumerate.CREATE TEMPORARY STAGEto make an ephemeral staging area that disappears when the session ends, erasing forensic evidence.COPY INTOwithGZIPcompression to keep the payload small enough that volumetric alarms didn’t trigger.GETto pull it down to a VPS in some offshore jurisdiction. Done.No IP allowlisting was in place anywhere. The connections from Mullvad and PIA exit nodes were treated with the same trust as an employee on the corporate VPN.
The Bucket Problem Hasn’t Gone Away Either
Alongside the identity attacks, the boring stuff keeps working. Misconfigured S3 buckets are still the most reliable way to expose a data lake. In late 2024, an open bucket used as a shared network drive was found containing raw customer data, cryptographic keys, and secrets. In 2025, a US healthcare provider left millions of patient records readable for weeks before anyone noticed.
Then there’s Codefinger. In January 2025, that group used compromised AWS credentials to access S3 buckets and then weaponized AWS’s own Server-Side Encryption with Customer-Provided Keys (SSE-C) to ransomware the data in place. They didn’t even need to exfiltrate it. They just encrypted it with a key the victim didn’t have and demanded Bitcoin.
That’s a native cloud feature being turned against you because somebody granted too many permissions to a service account.
The Boring Conclusions Are the Important Ones
Identity is the perimeter now. The encryption-at-rest story we’ve been telling ourselves for a decade is irrelevant when the attacker authenticates as a real user. Stop treating SSO as optional. Stop leaving local auth paths open next to it. Enforce MFA on every account, including the demo and service accounts you forgot about.
Your data lake should not be reachable from the public internet. Route everything through PrivateLink or the equivalent in your cloud. Allowlist the IPs that should be touching analytical workloads, and don’t make exceptions for “just this one contractor.”
And as you start handing access to AI agents, remember that static roles aren’t going to cut it. Just-in-time entitlements and contextual access control are the only way you’re going to keep up with autonomous systems making queries on your behalf.
The data lake industry spent years arguing about table formats, vendor lock-in, and egress fees. Meanwhile, attackers were just collecting passwords from gaming laptops and walking in.
Fix the doors first.
Sources
- UNC5537 Targets Snowflake Customer Instances (Mandiant / Google Cloud) — Forensic analysis, kill chain, infostealer attribution
- Snowflake Data Breach: Lessons Learned (AppOmni) — SSO side door, MFA bypass mechanics
- Major AWS S3 Bucket Breach Exposes Data (NHIMG) — Codefinger SSE-C ransomware tactic
- Misconfigured Cloud Assets: How Attackers Find Them (CybelAngel) — Recent open-bucket exposure incidents
- 5 Key Lessons from the Snowflake Data Breach (Tanium) — Defensive posture summary
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
-
Your Data Lake Has a Permissions Problem
Consolidating every business unit’s data into one giant lakehouse sounds like a win until you realize the security model from your old data warehouse can’t scale to it. You took ten silos, each with their own access rules, and merged them into one location. Now everyone wants in, and your security team is the bottleneck.
Let me walk through three places where the cracks usually show up.
RBAC Falls Over Faster Than You Think
Role-Based Access Control is the model most teams start with. Permissions are tied to a job function. Sales reps get read access to sales tables, data engineers get write access to staging, and so on. It works fine when you have ten roles.
It does not work when you have a thousand.
Say your sales reps should only see accounts in their territory, and only accounts they personally manage. Under pure RBAC, you need a unique role for every territory-by-account-owner combination. That’s role explosion, and it’s how compliance audits become impossible and legitimate access slows to a crawl. The roles list grows faster than anyone can review it, which means stale permissions sit there forever.
The answer is Attribute-Based Access Control. Instead of asking “what role is this user in,” the system asks “what attributes does this user have, what attributes does this data have, and what’s the policy at this exact moment.” Tag a column as
PII. Tag a schema asHR. Write one policy that says anyone outside the HR compliance group sees masked data when they touch a PII column. Done. That single policy replaces hundreds of bespoke roles.This is what Unity Catalog and Starburst Galaxy are built around, and it’s the model that will scale with the data.
Column and Row Security Should Be Boring
Once you have ABAC and a real metadata catalog, column-level masking and row-level filtering become a non-event. You write a SQL expression that masks the first five digits of an SSN for lower-privileged roles. You write a row filter that silently appends
WHERE region = 'user_region'to every executive’sSELECT *.The key word is silently. The user doesn’t see a different table. They don’t have a sanitized copy. The policy is enforced at the catalog layer, so it works the same whether they’re querying through Spark, Trino, a BI dashboard, or a pipeline. One source of truth, one policy, every engine.
If you’re still maintaining separate “sanitized” copies of tables for different audiences, you’re doing it the 2015 way and you’re going to drift.
The IAM Default Problem
Most cloud services ship with default IAM roles, and a surprising number of those defaults attach
AmazonS3FullAccessor something equally permissive.SageMaker does it. The Ray autoscaler role does it. There are more.
Picture the failure mode. An attacker compromises some peripheral app, maybe a forgotten Jupyter notebook, maybe a misconfigured Lambda. That workload has an IAM role attached because that’s how cloud workloads talk to S3 without hardcoded credentials. The attacker inherits the role. And because the role has full S3 access, they’re not constrained to the bucket the application actually uses. They can enumerate every bucket in the entire account.
That’s how a single compromised container becomes a full data lake breach. Researchers call it a bucket monopoly attack. I call it the most predictable incident in the industry.
The fix is not glamorous. Stop using
s3:*in any policy. Write resource-scoped policies that name the exact buckets and prefixes a workload needs. Audit the default roles every cloud service hands you and replace them. Use Security Lake or Detective to flag cross-service API calls that don’t match normal patterns. None of this is fun. All of it is necessary.And Then There’s the Agent Problem
The new wrinkle is that humans are no longer the primary consumers of your data. Autonomous agents are. They issue more queries, hit more tables, and move faster than any human team.
Long-lived credentials and static roles don’t fit that workload. The pattern emerging is Just-In-Time entitlements, where an agent gets a narrow, ephemeral permission for the duration of a single execution thread, then loses it. Pair that with declarative policy metadata baked into the data assets themselves, so the agent knows what it’s allowed to do with a dataset before it ever runs the query.
We’re early on this. Most organizations are still working through the basics, and that’s fine. But if you’re designing access controls today, design them assuming the next thing hitting your lake isn’t a person.
What to Actually Do
If you’re auditing your own data lake security, the order I’d work in:
- Find every IAM role with a wildcard permission. Replace them.
- Move from RBAC to ABAC at the catalog layer. Stop creating new roles.
- Pull your data lake off the public internet. PrivateLink, private endpoints, IP allowlists for the legacy stuff that can’t move.
- Then start thinking about agents.
The lakehouse pitch is unification. The lakehouse reality is that unification multiplies the cost of every bad permission. Get the basics right before you bolt on anything fancy.
Sources
- AWS Default IAM Roles Found to Enable Lateral Movement (The Hacker News) — SageMaker / Ray autoscaler default roles, bucket monopoly attacks
- What Is Fine-Grained Data Access Control? (TrustLogix) — RBAC role explosion, ABAC fundamentals
- Core concepts for ABAC (Databricks Unity Catalog docs) — Tag-driven policy enforcement
- Top 12 Data Governance Predictions for 2026 (Hyperight) — Just-in-time entitlements, declarative policy metadata
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
-
Why javascript:void(0) Needs to Stay in the Past
A few months ago, I ran across a
javascript:void(0)in the wild. I’m not going to get into the specific context because it doesn’t really matter, also it gets a bit too personal for this blog post. But I took a screenshot, thought “huh, that’s weird,” and promptly forgot about it.Then I was scrolling back through my screenshots and found it again. So here we are. Let’s talk about
javascript:void(0)and why you shouldn’t be using it in 2026.Here’s the screenshot that started this:

What Does It Actually Do?
If you’ve never encountered this pattern before, here’s the quick version.
voidis a JavaScript operator, so not a function, and its only job is to evaluate the expression next to it, throw away the result, and returnundefined. That’s it. That’s the whole job.When a browser receives
undefinedfrom clicking a link, it does nothing. No navigation, no page refresh. It’s a way to override the browser’s default behavior for an anchor tag.In practice, it looked like this:
<a href="javascript:void(0);" onclick="openModal()">Click Me</a>The
hrefprevents the browser from doing anything, and theonclickfires whatever JavaScript you actually wanted to run. Clever? Sure. A good idea today? No.Why Did We Use It?
Back in the day, we wanted to stop the browser from doing its default thing, following a link, so we could trigger events and make web pages more interactive. This was typically done on anchor tags because, well, that’s what we had. JavaScript didn’t give us a better way to handle it at the time, so
javascript:void(0)became the go-to pattern.It worked. But “it works” and “it’s a good idea” are two very different things.
Three Reasons to Stop Using It
1. It Breaks the Anchor Tag’s Purpose
The biggest issue is that
javascript:void(0)completely overrides what an anchor tag is supposed to do. An<a>tag exists to link to things. When you stuff JavaScript into thehref, you’re hijacking the element’s entire reason for existing.We’ve moved on from needing to do this. If you want something clickable that triggers behavior, use a
<button>. If you want a link that also has JavaScript behavior, give it a real URL as a fallback.2. Separation of Concerns
Modern best practices tell us that HTML should define the structure of the page, and JavaScript should define the behavior. When you’ve got JavaScript living inside an
hrefattribute or relying on inlineonclickhandlers, you’re mixing the two in ways that make code harder to maintain and reason about.The better approach? Use
event.preventDefault()in your JavaScript:<a href="/fallback-page" id="myLink">Click Me</a>document.getElementById('myLink').addEventListener('click', function(event) { event.preventDefault(); openModal(); });This way, if JavaScript is disabled or fails to load, the link still works. There’s a fallback behavior, which matters for accessibility and backwards compatibility. The HTML stays clean, and the behavior lives where it belongs, in your JavaScript files.
Now, I will say that plenty of modern front-end frameworks add their own semantic patterns and play pretty loosey-goosey with this separation of concerns rule. But even React’s
onClickhandlers and Vue’s@clickdirectives are compiled and managed in a way that’s fundamentally different from jamming raw JavaScript into an HTML attribute.3. Content Security Policy Will Block It
I’d like to believe Security still matters in 2026 so lets talk about the Content Security Policy (CSP).
CSP is a set of rules that a web server sends to the browser via HTTP headers, telling the browser what resources the page is allowed to load or execute. Before CSP, browsers just assumed that if code was in the HTML document, it was meant to be there. Web pages were incredibly vulnerable to cross-site scripting (XSS) attacks.
With CSP, the server tells the browser: “Only execute JavaScript if it comes from my own domain. Do not execute any code written directly inside the HTML file.”
A proper CSP header looks something like this:
Content-Security-Policy: default-src 'self'; script-src 'self';This is great for security. But guess what
javascript:void(0)is? Inline JavaScript. A strict CSP will block it.So if you see a site still using
javascript:void(0), check the response headers. Chances are you’ll find something like:Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline';See that
'unsafe-inline'addition? That’s the security risk. By addingunsafe-inline, the developer is telling the browser to trust all inline scripts. Every single one. So if an attacker manages to inject JavaScript onto the page, the browser will execute it without hesitation.You’re weakening your entire site’s security posture just to keep a legacy pattern alive. That’s not a tradeoff worth making.
You Probably Don’t Even Need to Think About This
If you’re working with any modern JavaScript framework, this problem is already solved for you.
React, Svelte, Vue, Solid, whatever you’re using, they all ship components that handle default browser behavior the right way. Take forms as an example. The raw HTML
<form>element will, by default, submit and trigger a full page navigation. That’s why developers used to manually callevent.preventDefault()everywhere. But now, frameworks like Next.js, Remix, and SvelteKit give you a<Form>component (or equivalent) that overrides that default behavior for you. No page reload. No manual prevention.The same applies to links, buttons, and pretty much any interactive element. The framework’s component handles the wiring so you don’t have to remember the low-level browser quirks. You import the component, use it, and move on.
That’s the real reason
javascript:void(0)feels so out of place in 2026. It’s not just that we have better patterns available, but the tooling has abstracted the problem away entirely. The history is worth knowing, because understanding why things work the way they do makes you a better developer! -
What Companies Are Actually Paying for Application Security
In the Application Security Testing (AST) market, Static Application Security Testing (SAST) and Software Composition Analysis (SCA) represent the two most critical pillars of preventative cyber defense.
So as a part of that, we should talk about the thing that people normally can’t or don’t talk about and that is cost. Vendors like to hide their pricing behind “contact sales” buttons, and buyers end up negotiating based on hard to find information.
So here’s an unofficial look at what companies are actually paying, pulled from a Deep Research report provided by Gemini. At the very end there is a list of resoruces where you can learn more about these subjects. However it is important to mention, there are not a lot of viable options for the home/hobby market.
What the Market Looks Like
Vendor Average Mid-Market / SMB Spend (Annual) Average Large Enterprise Spend (Annual) Economic Dynamics and Negotiation Factors Snyk ~$47,428 ~$222,516 Costs scale rapidly with developer headcount. Highly susceptible to volume discounting. Total cost includes separate quoting for onboarding and services. Black Duck (Coverity) $60,000 – $120,000 (50-100 devs) $150,000 – $300,000+ (150+ devs) Full platform deployments (SAST + SCA) often range from $300k to $600k+. Volume discounts and custom enterprise agreements are typical. Premium support adds 20-30%. Checkmarx $35,000 – $75,000 $100,000 – $250,000+ Pricing is considered complex. Hidden costs include mandatory professional services, premium support, and infrastructure overhead, adding 15-35% to year-one totals. Veracode $40,000 – $80,000 $100,000 – $250,000+ Application-based pricing feels predictable until microservice architectures cause application counts to explode. Discounts are heavily available for SAST+DAST+SCA bundles. SonarQube $30,000 – $50,000 (up to 5M LOC) $80,000 – $180,000 (5M - 20M+ LOC) Highly predictable LOC model. However, self-managed deployments incur separate infrastructure and administrative overhead costs not reflected in the software license. HCL AppScan $50,000+ $100,000 – $500,000+ Unified platform pricing for large deployments can easily exceed $1M. Implementations often require months of setup and heavy professional service fees. Official Licensing Models and Published Structures
Vendor / Platform Primary Pricing Metric Published Entry-Level / Standard Tier Pricing Enterprise Pricing Status Key Inclusions & Pricing Caveats Snyk Per Contributing Developer Team Tier: ~$52–$98 per developer/month ($624–$1,176/year). Custom / Unpublished Includes Snyk Code (SAST) and Open Source (SCA). Enterprise plans drop per-seat costs at high volume but require minimum seat counts. SonarQube Lines of Code (LOC) Analyzed Developer Edition: ~$15,000 for 1M LOC. Smaller tiers available (e.g., ~$2,500 for 100k LOC). Annual Pricing; Talk to Sales Prices scale strictly by the largest branch of private projects. Enterprise Edition adds legacy languages. Advanced Security is an add-on. GitHub Advanced Security Per Active Committer $19/user/month (Secrets) + $30/user/month (Code) = $49/user/month. Custom / Add-on to Enterprise ($21/user base) GHAS is strictly an add-on to the GitHub Enterprise plan. Tied directly to commit activity within a 30-day window. Mend.io Per Contributing Developer AppSec Platform: Up to $1,000 per developer/year. Included in upper bound limit Includes SAST, SCA, Renovate, and AI Inventory. No limits on LOC, scans, or applications. AI Premium is an extra $300/dev. Checkmarx Custom (Historically Per App or Node) Team Plans: ~$1,188/year base. Enterprise base starts ~$6,850/year. Custom / Unpublished Highly modular pricing based on developer count, module selection (SAST, SCA, DAST), and deployment model. Veracode Per Application or Per Scan Basic plans start at ~$15,000/year for up to 100 applications. Custom / Unpublished Pricing heavily depends on application count, scan frequency, and support levels. SCA alone starts around $12,000/year. Black Duck (Coverity) Per Team Member / Custom Coverity SAST: $800–$1,500 per team member annually. Custom / Unpublished Pricing scales with user access. Often bundled. Perpetual licenses with 18-22% annual maintenance fees exist for legacy deployments. Contrast Security Custom (GiB hour / usage) Essential tier: $119/mo. Advanced: $359/mo. Enterprise base ~$6,850/yr. Custom / Unpublished Pricing varies by package (AST vs. Contrast One managed service) and workload throughput. HCL AppScan Per Scan / Enterprise License SaaS: ~$313 per scan (min 5 scans). Basic Codesweep: $29.99/scan. Custom / Unpublished Enterprise suite pricing is highly customized, often requiring significant upfront capital expenditure. Feature Comparison
Feature / Capability Snyk Veracode Black Duck Checkmarx Mend.io GitHub (GHAS) SonarQube Endor Labs Primary Strength Developer Adoption & Speed Enterprise Governance & Low FPs License Compliance & Deep SAST Unified ASPM & Repo Scanning Automated Remediation Native Ecosystem Integration Code Quality & Baseline Security Noise Reduction & Reachability Reachability Analysis Basic No No No Advanced No No Full-Stack (95% reduction) Automated AI Fixes Yes (DeepCode) Yes (Proprietary Data) No Yes (Limited IDE) Yes Yes (Copilot) Yes (CodeFix) Yes (Without upgrades) Compilation Required No Yes (Binary) Yes (Coverity) No No No No No Broad Language Support High (14+) Very High (100+) High (22+) High (35+) Very High (200+) Moderate High (40) Moderate License Compliance Moderate Moderate Enterprise-Grade Moderate Enterprise-Grade Basic Basic Moderate Learning
-
I got tired of plaintext .env files, so I built LSM.
lsm execwill inject secrets at runtime so they never touch the filesystem. Doppler’s idea, minus the monthly bill.How are you managing local secrets?
/ Programming / Tools / security
-
I wrote about securing node_modules. Socket, Snyk, Dependabot — each catches different things. Hopefully answering when to use AI to rewrite simple deps you barely use.
Anyone want to build that CLI?
/ Programming / security / javascript
-
Fingerprint | Identify Every Web Visitor & Mobile Device
The Fingerprint device intelligence platform works across web and mobile applications to identify all visitors with industry-leading accuracy — even if they’re anonymous.
/ Tools / links / platform / security / analytics / tracking / visitor
-
Local Secrets Manager - Dotenv Encrypter
I built a thing to solve a problem. It has helped me, maybe it will help you?
It all starts with a question.
Why isn’t there a good local secrets manager that encrypts your secrets at rest? I imagine a lot of people, like me, have a number of local applications. I don’t want to pay per-seat pricing just to keep my sensitive data from sitting in plaintext on my machine.
I built an app called LSM Local Secrets Manager to solve that problem. The core idea is simple. Encrypt your
.envfiles locally and only decrypt when you need them (sometimes at runtime).The Problem
If you’ve got a bunch of projects on your machine, each with their own
.envor.env.localfile full of API keys you’re definitely not rotating every 90 days. Those files just sit there in plaintext. Any process on your system can read them. And with AI agents becoming part of our dev workflows, the attack surface for leaking secrets is only getting easier.ThE CLAW EnteRed ChaT
I started looking at Doppler specifically for OpenCLAW. Their main selling feature is injecting secrets into your runtime so they never touch the filesystem. I was like, cool. Also I like that Doppler stores everything remotely. The only thing was the cost did not make sense for me right now. I don’t want to pay $10-20 a month for this set of features.
So what else is there?
Well GCP Secret Manager has its own set of issues.
You can’t have duplicate names per project, so something as common as
NODE_ENVacross multiple apps becomes a more work than you want to deal with. Some wrapper script that injects prefixes? No thanks. I imagine there are a thousand and one homegrown solutions to solve this problem. Again, no thanks.So what else is there?
You Find A Solution
AWS Secret Manager
A Problem for Solution Problem
AWS IAM
🫣
I have a lot more to say here on this subject but will save this for another post. Subscribe if you want to see the next post.
The Solution
The workflow is straightforward:
lsm init— Run this once from anywhere. It generates your encryption key file.lsm link <app-name>— Run this inside your project directory. It creates a config entry in~/.lsm/config.yamlfor that application.lsm import— Takes your existing.envor.env.localand creates an encrypted version.lsm clean— Removes the plaintext.envfiles so they’re not just sitting around.lsm dump— Recreates the.envfiles if you need them back.
But wait there’s more.
Runtime Injection with
lsm execRemember that cool thing I just told you about? Instead of dumping secrets back to disk, you run:
lsm exec -- pnpm devI feel like a family man from Jersey, who don’t mess around. Aye, you got, runtime injection. I got that.
Well that’s
lsmanyways. It can decrypt your secrets and inject them directly into the runtime environment of whatever command follows the--. Your secrets exist in memory for the duration of that process and nowhere else. No plaintext files hanging around for other processes to sniff.Credit to Doppler for the idea. The difference to what we are doing is your encrypted files stay local.
What’s Next
I’ve got some possible ideas of improvements to try building.
- Separate encrypt/decrypt keys — You create secrets with one key, deploy the encrypted file to a server, and use a read-only key to decrypt at runtime. The server never has write access to your secrets.
- Time-based derivative keys — Imagine keys that expire or rotate automatically.
- Secure sharing — Right now you’d have to decrypt and drop the file into a password manager to share it. There’s room to make that smoother.
I’m not sure how to do all of that yet, but we’re making progress.
Why Not Just Use Doppler?
There are genuinely compelling reasons to use Doppler or similar services. I mean bsides the remote storage, access controls and auditable logs. There’s a lot to love.
For local development across a bunch of personal projects? I don’t think you should need a SaaS subscription to keep your secrets encrypted.
LSM is still early, but the core workflow is there and it works.
Give it a try if you’re tired of plaintext
.envfiles scattered across your machine.
/ DevOps / Programming / Tools / security
-
Doppler | Centralized cloud-based secrets management platform
Doppler’s secrets management platform helps teams secure, sync, and automate their secrets across environments and infrastructure. Experience enhanced security, agility, and automation with our cloud platform.
/ DevOps / links / platform / security / cloud security
-
Claude Code Now Has Two Different Security Review Tools
If you’re using Claude Code, you might have noticed that Anthropic has been quietly building out security tooling. There are now two distinct features worth knowing about. They sound similar but do very different things, so let’s break it down.
The /security-review Command
Back in August 2025, Anthropic added a
/security-reviewslash command to Claude Code. This one is focused on reviewing your current changes. Think of it as a security-aware code reviewer for your pull requests. It looks at what you’ve modified and flags potential security issues before you merge.It’s useful, but it’s scoped to your diff. It’s not going to crawl through your entire codebase looking for problems that have been sitting there for months.
The New Repository-Wide Security Scanner
Near the end of February 2026, Anthropic announced something more ambitious: a web-based tool that scans your entire repository and operates more like a security researcher than a linter. This is the thing that will help you identify and fix security issues across your entire codebase.
First we need to look at what already exists to understand why it matters.
SAST tools — Static Application Security Testing. SAST tools analyze your source code without executing it, looking for known vulnerability patterns. They’re great at catching things like SQL injection, hardcoded credentials, or buffer overflows based on pattern matching rules.
If a vulnerability doesn’t match a known pattern, it slips through. SAST tools also tend to generate a lot of false positives, which means teams start ignoring the results.
What Anthropic built is different. Instead of pattern matching, it uses Claude to actually reason about your code the way a security researcher would. It can understand context, follow data flows across files, and identify logical vulnerabilities that a rule-based scanner would never catch. Think things like:
- Authentication bypass through unexpected code paths
- Authorization logic that works in most cases but fails at edge cases
- Business logic flaws that technically “work” but create security holes
- Race conditions that only appear under specific timing
These are the kinds of issues that usually require a human security expert to find or … real attacker.
SAST tools aren’t going away, and you should still use them. They’re fast, they catch the common stuff, and they integrate easily into CI/CD pipelines.
Also the new repository-wide security scanner isn’t out yet, so stick with what you got until it’s ready.
/ DevOps / AI / Claude-code / security
-
Defending Your Node Modules: Security Tools and When to Rewrite Dependencies
This week I’ve been on a bit of a JavaScript kick; writing about why Vitest beats Jest, comparing package managers, diving into Svelte 5. But there’s one topic that we shouldn’t forget: security.
node_modulesis the black hole directory that we all joke about and pretend is fine.Let’s talk about how to actually defend against problems lurking in those deep depths when a rewrite might make sense.
The Security Toolkit You Actually Need
You’re going to need a mix of tools to both detect bad code and prevent it from running. No single tool covers everything (or should), so here are some options to consider:
Socket does behavioral analysis on packages. It looks at what the code is actually doing. Is it accessing the network? Reading environment variables? Running install scripts? These are the sketchy behaviors that signal a compromised or malicious package. Socket is great at catching supply chain attacks that traditional vulnerability scanners miss entirely.
Snyk handles vulnerability scanning. It checks your entire dependency tree against a massive database of known vulnerabilities and is really good at finding transitive problems, those vulnerabilities buried three or four levels deep in your dependency chain that you’d never find manually.
LavaMoat takes a different approach. It creates a runtime policy that prevents libraries from doing things they shouldn’t be doing, like making network requests when they’re supposed to be a string formatting utility. Think of it as a permissions system for your dependencies.
And then there’s Dependabot from GitHub, which automatically opens pull requests to update vulnerable dependencies. This is honestly the minimum of what you should be doing. If you’re not running Dependabot like service, start now.
Each of these tools catches different things. Socket finds malicious behavior, Snyk finds known vulnerabilities, LavaMoat enforces runtime boundaries, and Dependabot keeps things updated. Together, they give you solid coverage.
When to Vendor or Rewrite a Dependency
Now let’s talk about something I think more developers should be doing: auditing your dependencies and asking when a rewrite makes sense.
With AI tools available now, this has become incredibly practical. Here’s when I think you should seriously consider replacing a dependency with your own code:
-
You’re using 1% of the library. If you imported a massive package just to use one function, you don’t need the whole thing. Have your AI tool write a custom function that does exactly what you need. You shouldn’t be importing a huge library for a single utility. It’s … ahhh, well, stupide.
-
It’s a simple helper. Things like
isEven,leftPad, or a basic string formatter. AI can write these in seconds, and you eliminate an entire dependency from your tree. Fewer dependencies means a smaller attack surface. -
The package is abandoned. The last update was years ago, there’s a pile of open issues, and nobody’s home. You’re better off asking your LLM to rewrite the functionality for your specific project. Own the code yourself instead of depending on something that’s collecting dust.
When You Should Absolutely NOT Rewrite
This is just as important. Some things should stay as battle-tested community libraries, no matter how good your AI tools are:
-
Cryptography, authentication, and authorization. It would be incredibly foolish to try to rewrite bcrypt or roll your own JWT validation. These libraries have been audited, attacked, and hardened over years. Use them.
-
Complex parsers with extensive rule sets. A markdown parser, for example, has a ton of edge cases and rules that need to be exactly right. You don’t want to accidentally ship your own flavor of markdown. Same goes for HTML sanitizers, getting sanitization wrong means introducing XSS vulnerabilities. Trust the community libraries here.
-
Date and time math. Time zones are a deceptively hard problem in programming. Don’t rewrite
date-fnsordayjs. Just don’t. -
Libraries that wrap external APIs. If something integrates with Stripe, AWS, or any API that changes frequently, you do not want to maintain that yourself. The official SDK maintainers track API changes so you don’t have to. Just, no and thank you.
The pattern is pretty clear: if getting it wrong has security implications or if the domain is genuinely complex with lots of edge cases, use the established library. If it’s simple utility code or you’re barely using the package, consider a rewrite.
A Fun Side Project Idea
If you’re looking for yet another side project (YASP), that is one that would be a super useful CLI tool. I’d probably reach for Go and build a TUI tool that scans your
node_modulesand generates a list of rewrite recommendations.I think that’d be a really fun build, and honestly something the JavaScript ecosystem could use.
/ security / javascript / Node / Dependencies / Devtools
-
-
If you have an iPhone, turn on Advanced Data Protection. Without it, Apple can access your iCloud data and the government can access it with a warrant. What you may not realize is that if you message someone who doesn’t have ADP enabled, your messages get stored unencrypted in their backup. Your security is only as strong as your contacts' settings.
-
So, Zero Trust is this idea that you never trust and you always verify.
Cloudflare Tunnels, TailScale, and Ngrok are three different approaches to Zero Trust networking.
Cloudflare Tunnel is a reverse proxy. TailScale is more of a mesh-based VPN. Cloudflare Tunnel is an ingress as service, which means that it makes it really easy to spin up public URLs.
What you need depends on your use case.
/ Networking / security / Homelab
-
Stay updated with the latest news, feature releases, and critical security and code quality blogs from CodeAnt AI.
/ AI / Programming / blogging / links / security