Shifting the Trap
Shifting the Trap
Fri, 24 Apr 2026
Back in 2009, Richard Stallman warned us about The<br>JavaScript Trap. The pitch was simple, and it's aged<br>horrifyingly well. Every time you visit a modern website,<br>your browser silently downloads and executes a pile of<br>non-free JavaScript on your computer. You didn't choose this<br>software. It runs anyway, on your hardware, in your name, and<br>the moment your tab closes, it pretends nothing happened.<br>It's the perfect non-free program: invisible, ephemeral, and<br>authored by someone who would very much like you not to think<br>about it at all.
The response from the free software community has been to<br>push back at the browser layer - LibreJS, NoScript,<br>Trisquel's defaults, gentle public reminders that yes, that<br>"web app" is a program, and yes, you are running it.
Fine. Good. Necessary.
Stallman's warning was directed at the browser because<br>that's where, in 2009, the trap was being sprung. It's still<br>being sprung there.
But here's the part nobody seems to want to say out loud:<br>the trap's been quietly migrating out of the browser. It's<br>also leaking into our command-line utilities. To understand,<br>let's examine the widely used program yt-dlp.
I want to be careful here, because the tone of this piece<br>matters.
yt-dlp is GPL'd. The project's healthy. The work's hard,<br>and the adversary - Google's YouTube player, which mutates<br>daily for the explicit purpose of breaking interoperability -<br>is acting in bad faith on a scale that few free software<br>projects have to deal with. The codebase is, by the standards<br>of programs that have to keep up with a hostile,<br>shape-shifting adversary like YouTube, remarkably clean.<br>yt-dlp isn't Google. yt-dlp isn't Apple. yt-dlp isn't<br>"OmniCorp."
None of that's in dispute, and none of that's the problem.<br>Nothing in this post should be read as accusing yt-dlp of<br>malice. yt-dlp is a friend, and friends can be told the<br>truth.
The problem's what yt-dlp does for you when you ask it to<br>download a video.
The friend, in this case, has confused two things. yt-dlp<br>has confused being free software with only causing free<br>software to be run on the user's machine. Those aren't the<br>same proposition, and the difference is the entire subject of<br>Stallman's essay.
YouTube no longer hands out media URLs. It hands out URLs<br>that have been deliberately broken - the so-called<br>signatureCipher and the n parameter - and it ships, every<br>single day, a fresh blob of obfuscated, minified, non-free<br>JavaScript called base.js whose entire purpose is to un-break<br>those URLs. To make the download work, somebody has to<br>execute Google's JavaScript. In the browser, that "somebody"<br>is your browser, and Stallman already named that trap. In<br>yt-dlp, that "somebody" used to be - and in many code paths<br>still is - a 970-line file called yt_dlp/jsinterp.py.
"It's not really an interpreter, though." Yes, it<br>is.
The first move in any honest examination of this issue has<br>to be a definitional one, because the project's drift has<br>been protected, in part, by a quiet linguistic hedge: the<br>suggestion that yt_dlp/jsinterp.py is something less than a<br>JavaScript interpreter.
This is the part where a certain kind of computer science<br>purist tries to wave the problem away. They look at<br>jsinterp.py, notice that it skips half of the textbook<br>interpreter pipeline, and decides that what yt-dlp is doing<br>isn't really running JavaScript. It's just... string<br>manipulation. Pattern matching. A clever hack. Not an<br>interpreter, surely. Therefore, not The JavaScript Trap.<br>Therefore, nothing to worry about.
I want to take that argument seriously because it deserves<br>to be taken seriously and then dismantled.
Yes, jsinterp.py is unusual. A canonical interpreter<br>follows a textbook pipeline: Lexer → Parser → Abstract Syntax<br>Tree → Evaluator. It's true, and it should be conceded up<br>front, that jsinterp.py skips the middle two stages. There's<br>no tokenizer. There's no AST. The file walks raw source-code<br>substrings, peels constructs off the front with regular<br>expressions and _separate_at_paren, and recurses on what's<br>left. Parsing and evaluation are fused into a single pass. By<br>the standards of V8, SpiderMonkey, or even small embeddable<br>engines like QuickJS, this is unusual.
It's also irrelevant to the question. An interpreter's a<br>program that executes a source language by walking its<br>constructs and producing the language's observable effects.<br>By that definition - the only definition that matters here -<br>jsinterp.py is, indisputably, an interpreter for a subset of<br>JavaScript. The evidence is in the file itself:
It maintains a real lexical environment.<br>LocalNameSpace, a ChainMap with set_local and get_local,<br>provides proper scoping and shadowing. A string-replacement<br>engine doesn't need scopes. An interpreter does.
It dispatches recursively on language forms.<br>interpret_statement and interpret_expression know about<br>var/let/const, return, throw, try/catch/finally, blocks,<br>regex literals, string literals, new...