The dangerous moment in a Python project is not when you write code. It is when you type pip install. That command reaches out to an index, downloads an archive built by a stranger, and runs their setup.py as your user, with your environment variables, your SSH keys, and your cloud credentials sitting one os.environ away. You have not imported anything yet. You have not read a single line of what you pulled. The code already ran. And here is the part people miss: pip install --dry-run runs it too, because pip has to build the package to learn what it needs.
So the install step is a trust boundary, and it is one most teams cross thousands of times a year without a gate. A typo in a requirements file, a maintainer’s account taken over on a Friday night, a beloved library quietly handed to a new owner who ships one poisoned release before anyone notices. None of these are exotic. They are the ordinary texture of a public package index with hundreds of thousands of names and no bouncer at the door.
depwal is that bouncer. It is a dependency firewall for pip and poetry: it scores every package in your dependency graph against a set of supply-chain heuristics and refuses the install if something looks wrong, before the resolver runs and before any setup.py executes.
The attack lives in the metadata
Most people picture a supply-chain attack as clever, obfuscated code hidden deep in a file. Sometimes it is. But you cannot read every line of every transitive dependency on every install, and neither can your CI. What you can do is notice when the story around a package changes.
A package has a paper trail. Who publishes it. What license it carries. Which repository it points at. How long it has existed and how often it ships. An attacker who takes over a name has to touch that paper trail: the maintainer email flips to theirs, the source repo moves or vanishes, a permissive license quietly appears, a project that shipped twice a year suddenly ships at 2am. The malicious payload is invisible until it runs. The change in provenance is visible right now, in JSON, over HTTPS, before you download anything you would have to execute.
It scores every package in your dependency graph against supply-chain heuristics and refuses the install if something looks wrong.
depwal READMEThat is the whole bet. Screen the metadata, not the code. It will not catch everything, and later in this piece I will be specific about what it misses. But it turns the most common attacks from an after-the-fact incident into a blocked exit code.
Three phases, and none of them run your code
The reason a firewall can sit in front of pip install at all is that depwal is careful never to do the dangerous thing pip does. It works in three phases, and each one is deliberately safe.
The wheels-only step is the quiet clever bit. A normal resolver may build a source distribution just to read its dependencies, which means running its code. By forcing --only-binary :all:, depwal learns the shape of your dependency tree without ever handing control to a package author. For the same reason it refuses path, URL, and VCS specs by default: pip always builds those, so there is no safe way to screen them first. Watch a clean package walk the pipeline and a suspicious one get stopped at the gate.
Screen → Resolve → Gate → Install.
The resolve step expands the closure with --only-binary :all:, so no
setup.py ever runs. Switch to the typo and it never reaches install - the
gate stops it and returns exit 1.The gate returns an honest Unix contract: exit 0 if the install is allowed, 1 if it was blocked, 2 on error. That is what makes it a drop-in for anything that already runs pip install, from a Makefile to a CI job to a pre-commit hook.
Scoring the story, not the code
Inside the gate is a scoring model. Every signal depwal can read contributes points, the points sum, and the total is clamped to 100. You set a risk_threshold - the default is 70 - and any package whose score reaches it is blocked. The signals are not a black box; each one names a concrete, checkable fact about the package.
Three of them are hard rules worth 100 on their own, because they mean the package cannot be trusted at all: the name is not on PyPI, the exact version was never published, or that version has been yanked. Everything else is softer and additive. A typosquat - a name one edit away from a popular package - is worth 45. A maintainer email that differs from the previous release scores 30. A first release under a month old adds 30. A source distribution with no wheel, which means setup.py will run on install, adds 20. A license that changed since the last release adds 20. A repository that moved to a new owner, or got archived, adds its own points. Alone, most of these are noise. Stacked, they are a package telling you a story that does not hold together.
Toggle the signals on a package and watch the score climb toward the threshold, and the verdict flip from allow to review to block.
typosquat is 45, an owner change 30, an sdist-only build 20.
Cross the threshold and it is blocked; land in the gray band above the floor and it
is worth a second look. Drag the threshold to see how strict you have made your firewall.The design choice that matters here is additivity. depwal is not trying to prove a package is malicious, which is undecidable from metadata. It is measuring how many independent things look off at once. A brand-new package with no repository and a name that shadows requests is not proven guilty, but it has three unrelated tells, and three tells is enough to make a human look before a machine installs.
One edit away from a name you trust
The single highest-scoring soft signal is the typosquat, and it earns that weight because it is how a startling number of real attacks begin. You mean to install requests. Your finger, or your agent’s tokenizer, produces reqeusts. There is a package sitting on that name, waiting, and it does something very different from what you wanted.
depwal measures the edit distance between the name you asked for and the names on a bundled list of the most-downloaded packages. One insertion, deletion, substitution, or transposition away from a popular name is the classic typosquat pattern, and it trips the rule. The check is cheap and it runs before anything else, because the whole point is to catch the mistake at the moment you make it, not after the resolver has already fetched the impostor. Step through a few look-alikes and watch the distance collapse to one.
requests collapse to one - a single insert,
delete, substitution, or transposition. That trips the typosquat rule before the
resolver ever fetches the impostor. The last candidate is a real neighbor, far enough away to pass.Names are compared after PEP 503 normalization, so Zope.Interface and zope-interface are treated as the same package and the dots-versus-dashes tricks do not slip through. The list is bundled rather than fetched live, which keeps the check fast and offline-capable, at the cost of going a little stale between releases. That is a real tradeoff, and depwal is upfront about it.
Diffing the last release
Several of the strongest signals are not about the package in isolation. They are about the difference between this release and the one before it. This is where the “change of owner, change of license” heuristics live, and it is worth understanding exactly how the comparison works, because it shapes what depwal can and cannot see.
There is no stored baseline. depwal does not keep a database of what every package looked like last year. When it screens a version, it pulls that version’s metadata and the immediately preceding release’s metadata and diffs the two. Did the maintainer email change? Did the declared license change? Did the source repository move to a different owner, or get archived, or disappear? Each answered “yes” is a signal with a point value, and together they describe a handover - the exact event an account-takeover attack has to produce. Flip each field and watch the matching signal fire.
owner_changed +30, a license flip +20. A
handover lights up several at once.Comparing only against the previous release is a deliberate simplification. It keeps the tool stateless and fast, and it catches the common case, where the poisoned version is the very next one after a takeover. It also means a slow, patient attacker who drifts the metadata across several releases can stay under the per-release radar. depwal trades that tail risk for a tool you can run on every install with no infrastructure behind it.
The gray band, and letting a model look
Heuristics have a middle. Below a floor, a package is clearly fine and you do not want to spend anything looking at it. Above your threshold, it is blocked and there is nothing to discuss. In between sits a gray band: packages that pass, but score high enough to be worth a second opinion. depwal can hand exactly those packages to a language model.
You point it at one with --llm "command" and set --llm-floor N, which defaults to 40. Only packages scoring at or above the floor are sent, so the model never wastes cycles on the obvious cases. And the reviewer operates under two rules that keep it from becoming a new weakness. It can only tighten a verdict, never loosen one - the model can turn a pass into a block, but it can never talk depwal into allowing something the heuristics flagged. And it fails open: a timeout or an error leaves the heuristic verdict exactly as it was, so a flaky model endpoint cannot wedge your CI. Move a package’s score into the band and let the reviewer push it - never back toward allow.
There is a second, subtler defense. The model is sent only computed signals - the name, the version, the score, the rules that fired. It never sees the package description or README. That is on purpose: a package author who could get their marketing copy in front of your reviewer could try to talk it into a pass. Sending numbers instead of prose closes that door. It is the same instinct behind treating the install as untrusted in the first place.
Heuristics only
- Deterministic, fast, offline
- Every package gets the same rules
- A pass is a pass
Heuristics plus a reviewer
- Adds judgment on the gray band
- Only sees signals, never prose
- Can tighten, never loosen; fails open
Where it sits in your workflow
A firewall is only useful where the traffic flows, so depwal meets you at the places you already install. depwal check gates a poetry.lock, uv.lock, or requirements.txt and is the natural fit for CI and pre-commit. depwal install fastapi and depwal add fastapi screen first and then hand off to pip or poetry, so an interactive install is gated without a second command. depwal audit scores what is already in your environment, and depwal explain pandas3 prints the signal breakdown for a single package so you can see why it was flagged.
Configuration lives in pyproject.toml under [tool.depwal], alongside everything else about your project. You set risk_threshold and a mode - block, warn, or audit - and you keep an allowlist and denylist of package names. A default-deny posture is one line: deny *, then allow the handful of names you actually trust. Flags beat environment variables beat the config file beat the defaults, so a one-off --allow in a pinch does not require editing anything permanent.
The advice that matters most is to start in warn. Heuristics have false positives, and the honest way to find yours is to run the tool over your real dependency graph, watch what it flags, and only switch to block once the noise is tuned out. A firewall that cries wolf gets disabled, and a disabled firewall protects nobody.
What it deliberately can’t do
The fastest way to lose trust in a security tool is to let it imply guarantees it does not offer, so depwal is blunt about its edges, and so am I.
depwal reads the label, not the contents. It never downloads and inspects package code. A malicious payload shipped by a legitimate maintainer, from an account and a repo whose metadata never changed, presents a clean story and sails through. Metadata screening catches the handover, not the insider.
It is also not a vulnerability scanner. It will not tell you that a version you trust has a known CVE - that is the job of pip-audit or osv-scanner, and depwal is meant to run alongside them, not replace them. Its audit command is detection, not prevention: by the time a package is installed, its setup.py has already run, so auditing your environment tells you what got in, not what to keep out. The bundled list of popular names grows stale between releases. And the metadata diff, as covered above, sees only one release back.
None of this makes it less worth running. A lock that stops the casual burglar is worth having even though a determined one can pick it. depwal raises the floor on the most common, most automatable attacks - the typo, the takeover, the freshly minted impostor - and it does so cheaply enough to run on every install.
Put a gate on the trust boundary
The install step was always a place where you ran a stranger’s code with your privileges. For a long time the mitigation was vigilance: read the name twice, pin your versions, hope. Vigilance does not scale to a dependency graph with hundreds of transitive packages, and it scales even worse when the thing typing pip install is a script or an agent that never gets tired and never reads the name twice at all.
A firewall does scale, because it turns a judgment call into a check with an exit code. depwal will not make your supply chain safe. Nothing will. What it does is put a bouncer on the one door that runs code before you get to look, and ask every package the same quiet question: does your story hold together? Point it at your requirements.txt, start in warn, and see what walks up.
The code is at github.com/msoedov/depwal. If you are thinking about the same trust boundaries for agents that install their own tools, the argument for a plain CLI over MCP and the loop guards that keep agents from running away are the neighboring problems.



