Thu, 20 Nov 2025 09:53:49 +0000 Fullscreen Open in Tab
Pluralistic: The long game (20 Nov 2025)


Today's links



The classic Puck Magazine editorial cartoon entitled 'The King of All Commodities,' depicting John D Rockefeller as a man with grotesquely tiny body and a gigantic head, wearing a crown emblazoned with the names of the industrial concerns he owned. Rockefeller's head has been replaced with that of Mark Zuckerberg's metaverse avatar. The names of the industrial concerns have been replaced with the wordmarks for Scale AI, Instagram, Oculus and Whatsapp. The dollar-sign at the crown's pinnacle has been replaced with the Facebook 'f' logo. The chain around Rockefeller's neck sports the charm that Mark Zuckerberg now wears around his neck.

The long game (permalink)

Well, this fucking sucks. A federal judge has decided that Meta is not a monopolist, and that its acquisitions of Instagram and Whatsapp were not an illegal bid to secure and maintain a monopoly:

https://gizmodo.com/meta-learns-that-nothing-is-a-monopoly-if-you-just-wait-long-enough-2000687691

This is particularly galling because Mark Zuckerberg repeatedly, explicitly declared that these mergers were undertaken to reduce competition, which is the only circumstance in which pro-monopoly economists and lawyers say that mergers should be blocked.

Let me take a step back here. During the Reagan years, a new economic orthodoxy took hold, a weird combination of economic theory and conspiracy theory that held that:

a) It was bad economic policy to try and prevent monopolization, since monopolies are "efficient" and arise because companies are so totally amazing that we all voluntarily buy their products and pay for their services and;

b) The anti-monopoly laws on the books are actually pro-monopoly laws, and if you look at them just right, you'll find that what Congress really intended was for monopolies to be nurtured and protected:

https://pluralistic.net/2022/02/20/we-should-not-endure-a-king/

The one exception these monsters of history were willing to make to their pro-monopoly posture was this: if a corporation undertakes a merger because they are seeking a monopoly, then the government should step in and stop them. This is a great standard to come up with if what you really want to do is nothing, because how can you know why a company truly wants to buy another company? Who can ever claim to know what is in another person's heart?

This is a great wheeze if you want to allow as many monopolies as possible, unless the guy who's trying to get that monopoly is Mark Zuckerberg, because Zuck is a man who has never had a criminal intention he did not immediately put to writing and email to someone else.

This is the guy who put in writing the immortal words, "It is better to buy than to compete," and "what we’re really buying is time," and who described his plans to clone a competitor's features as intended to get there "before anyone can get close to their scale again":

https://www.theverge.com/2020/7/29/21345723/facebook-instagram-documents-emails-mark-zuckerberg-kevin-systrom-hearing

Basically, Zuck is the guy who works until 2:30 every night, and then, before turning in, sends some key executive a fully discoverable, immortally backed-up digital message that reads, "Hey Bob, you know that guy we were thinking about killing? Well, I've decided we should do it. And for avoidance of doubt, it's 100% a murder, and right now, at this moment, I am premeditating it."

And despite this wealth of evidence as to Zuckerberg's intention at the time, US regulators at the FTC and EU regulators at the Commission both waved through those mergers, as well as many other before and since. Because it turns out that in the pro-monopoly world, there are no bright lines, no mergers so nakedly corrupt that they should be prevented. All that stuff about using state power to prevent deliberate monopolization was always and forever just bullshit. In the pro-monopoly camp, all monopolies are warmly welcome.

It wasn't always this way. In the trustbusting era, enforcers joined with organized labor and activists fighting for all kinds of human rights, from universal sufferage to ending Jim Crow, to smash corporate power. Foundational to this fight was the understanding that concentrated corporate power presented a serious danger: first, because of the way that it could corrupt our political process, and second, because of the difficulty of dislodging corporate power once it had been established.

In other words, trustbusters sought to prevent monopolies, not merely to break up monopolies once they were formed. They understood that a company that was too big to fail would also be too big to jail, and that impunity rotted societies from within.

Then came the project to dismantle antitrust and revive the monopolies. Corporatists from the University of Chicago School of Economics and their ultra-wealthy backers launched a multipronged attack on economics, law, and precedent. It was a successful bid to bring back oligarchy and establish a new class of modern aristocracy, whose dynastic fortunes would ensure their rule and the rule of their descendants for generations to come.

A key part of this was an attack on the judiciary. Like other professionals, federal judges are expected to undergo regular "ongoing education" to ensure they're current on the best practices in their field. Wealthy pro-monopolists bankrolled a series of junkets for judges called the "Manne Seminars," all-expenses-paid family trips to luxury resorts, where judges could be indoctrinated with the theory of "efficient monopolies":

https://pluralistic.net/2021/08/13/post-bork-era/#manne-down

40% of all federal judges attended a Manne Seminar, and empirical studies show that after graduating, these judges changed the way they ruled, to favor monopolies:

https://academic.oup.com/qje/advance-article/doi/10.1093/qje/qjaf042/8241352?login=false

The terrible beauty of this strategy is that you don't need to get all the judges into a Manne Seminar – you just need to get enough judges to attend that they will create a wall of precedent that every other judge will feel hemmed in by when they rule on antitrust cases. Those judgments further shore-up the pro-monopoly precedent, setting the stage for the next pro-monopoly judgment, and the next, and the next.

So here we are, a couple generations into the project to brainwash judges, monopolize the economy and establish a new aristocracy, and a judge just ruled that Meta isn't an illegal monopoly, even though Mark Zuckerberg literally put his explicit criminal intent in writing.

What are we to do? Should we despair? Does this mean it's all over?

Not hardly. Reversing 40+ years of pro-monopoly policy was always going to be a slog, with many setbacks on the way. That's why antitrust has historically sought to prevent monopolies. Once monopolies have conquered your economy, getting rid of them is far harder, or, as the joke from eastern Canada goes, "If you wanted to get there, I wouldn't start from here."

But you have to play the ball where it lies. The fact that Meta can deliberately set out to create a monopoly and still evade judgment is more reason to fight monopolies, not less – it's (more) evidence of just how corrupted and illegitimate our judicial system has become.

We've been here before. The first antitrust laws were passed to do the hard work of smashing existing monopolies, not the relatively easy task of preventing monopolization. Of course: before there is a law, there has to be a crime. Antitrust law was passed because of a monopoly problem, not as a pro-active measure to prevent the problem from arising.

Our forbears smashed monopolies that were, if anything, far more ferocious than Big Tech. They vanquished oligarchs whose perfidy and ruthlessness put today's ketamine-addled zuckermuskian mediocrities in the shade. How they did it is not a mystery: they just put in the hard yards of building coalitions and winning public sentiment.

They did it before and we can do it again. We know how it's done. We remember their names and what they did. Take Ida Tarbell, the slayer of John D Rockefeller and Standard Oil. Tarbell was a brilliant, fierce writer and orator, fearless and brilliant. She was the first woman in America to get a science degree, and a key driver of the movement for universal suffrage. But in addition to all that, she was an anti-monopolist.

Tarbell's father was a Pennsylvania oil man who'd been ruined by Rockefeller and Standard Oil. Determined to see him avenged, Tarbell researched the many tendrils of Rockefeller's empire and his devious tactics, and laid them bare in a pair of wildly successful serialized books, The History of the Standard Oil Company, Volumes I & II (published first in the popular national magazine Collier's):

https://pluralistic.net/2021/06/13/a-monopoly-isnt-the-same-as-legitimate-greatness/

Tarbell's History changed the way the country saw Rockefeller. She punctured his myth of brilliance and competence, and showed how he owed his fortune to swindling and cheating. She cut him down to size. She was a key figure in the American trustbusting movement, a catalyst for the revolution that saw Rockefeller and his fellow oligarchs overthrown.

This took a hell of a long time. The Sherman Act (which was used to break up Standard Oil) was passed in 1890, but Standard Oil wasn't broken up until 1912. It took perseverance through setback after setback, it took the compounding tragedies that drove people to question the order and demand change, and it took unglamorous organizing and dramatic street-fights to escape from oligarchy's powerful gravity well.

Today, we are back at square one, but we have advantages that Tarbell and the other trustbusters lacked. For one thing, we have them, the lessons of their fight and the inspiration of their victory. For another, we have the political wind at our back. All over the world, from China to Canada, from the EU to the USA, politicians have felt emboldened (or forced) to launch anti-monopoly efforts the likes of which have not been seen since the Carter administration:

https://pluralistic.net/2025/08/09/elite-disunity/#awoken-giants

What's more, these enforcers aren't alone – they can and do collaborate. Because these tech companies run the same swindles in every country in the world, enforcers can collaborate on building cases against them. After all the facts of Big Tech's crimes are virtually identical, whether you're in the UK, Singapore, South Korea, Canada or Germany:

https://pluralistic.net/2025/01/22/autocrats-of-trade/#dingo-babysitter

This is an advantage that the trustbusters who took down Rockefeller could only dream of. Like Big Tech, Rockefeller had a global empire, but unlike Big Tech, Rockefeller abused each of the nations of the world in distinct ways. In America, Rockefeller ran the refineries and pipelines; in Germany, he had a stranglehold on the ports.

Even if the Rockefeller-era trustbusters wanted to collaborate, sending memos back and forth across the Atlantic by zeppelin, all they could offer each other was warm wishes. US pipeline investigations had nothing to add to German port investigations.

Today's tech monopolists may be bigger than any one government, but they're not bigger than all the governments whose people they're abusing.

The trustbusters who brought down Rockefeller did something knowable and repeatable. Their work did not arise out of the lost arts of a fallen civilization. The work of taking down today's monopolists requires only that we recover our ancestors' moral fire and perseverance. No one needs to figure out how to build a pyramid without power tools or embalm a Pharaoh.

We merely have to build and sustain a global movement to destroy oligarchy.

(Merely!)

Yes, that's a hell of a big lift. But we're not alone. There are billions of people who suffer under oligarchy and an infinite variety of ways to erode its power, as a prelude to smashing that power. Our allies in antitrust include the voters who put Zohran Mamdani into office, going from less than 1% in the polls to a commanding majority in a three-way race, running on an anti-oligarch platform:

https://pluralistic.net/2025/06/28/mamdani/#trustbusting

(No coincidence that one of our most effective fighters is now co-leading Mamdani's transition team):

https://pluralistic.net/2025/11/15/unconscionability/#standalone-authority

Trustbusting alone will not end oligarchy and trustbusters alone cannot break up the monopolies. As with the original trustbusters, the modern trustbusting movement is but a part of a coalition that wants a world organized around the needs of the many, not the few.


Hey look at this (permalink)



A shelf of leatherbound history books with a gilt-stamped series title, 'The World's Famous Events.'

Object permanence (permalink)

#20yrsago Brit backpackers take Indian call-centre jobs https://web.archive.org/web/20051210103452/http://wiredblogs.tripod.com/sterling/index.blog?entry_id=1284171

#20yrsago Laser etching doesn’t necessarily void your warranty https://web.archive.org/web/20051126194823/http://www.makezine.com/blog/archive/2005/11/will_laser_etching_apple_gear.html

#20yrsago UCLA to MPAA shill: ARRRRRRR! https://www.latimes.com/archives/la-xpm-2005-nov-18-fi-glickman18-story.html

#20yrsago RIAA prez: Lots of companies secretly install rootkits! It’s no biggie! https://web.archive.org/web/20051125041201/http://www.malbela.com/blog/archives/000375.html

#20yrsago Sony offers MP3s in replacement for rootkit CDs https://web.archive.org/web/20051124233458/https://www.upsrow.com/sonybmg/

#15yrsago TSA forces cancer survivor to remove prosthetic breast https://web.archive.org/web/20101120213044/http://www.wbtv.com/Global/story.asp?S=13534628

#15yrsago How the Victorians wiped their bums https://web.archive.org/web/20101123191021/http://wellcomelibrary.blogspot.com/2010/11/item-of-month-november-2010-victorian.html

#15yrsago Understanding the “microcredit crisis” in Andhra Pradesh https://web.archive.org/web/20101119012652/https://blogs.reuters.com/felix-salmon/2010/11/18/the-lessons-of-andhra-pradesh/

#15yrsago Canadian Heritage Minister inadvertently damns his own copyright bill https://web.archive.org/web/20101121054805/https://www.michaelgeist.ca/content/view/5456/125/

#15yrsago TSA confiscates heavily-armed soldiers’ nail-clippers https://redstate.com/erick/2010/11/18/another-tsa-outrage-n37064

#15yrsago Chris McKitterick pirates his own book https://mckitterick.livejournal.com/653743.html

#15yrsago Chinese woman kidnapped to labor camp on her wedding day over sarcastic re-Tweet https://web.archive.org/web/20120609051421/http://voices.washingtonpost.com/blog-post/2010/11/chinese_twitter_sentence_a_yea.html

#15yrsago RuneScape devs refuse to cave in to patent trolls https://web.archive.org/web/20101119012943/http://www.gamasutra.com/view/news/31597/UKBased_RuneScape_Dev_Jagex_Wins_Patent_Infringement_Lawsuit.php

#10yrsago Manhattan DA calls for backdoors in all mobile operating systems https://web.archive.org/web/20151120003032/https://manhattanda.org/sites/default/files/11.18.15

#10yrsago Watching paint dry: epic crowfunded troll of the UK film censorship board https://www.kickstarter.com/projects/charlielyne/make-the-censors-watch-paint-drying?ref=video

#10yrsago CEOs are lucky, tall men https://hbr.org/2015/11/are-successful-ceos-just-lucky

#10yrsago America’s CEOs and hedge funds are starving the nation’s corporations to death https://www.reuters.com/investigates/special-report/usa-buybacks-cannibalized/

#10yrsago EU official: all identified Paris attackers were from the EU https://web.archive.org/web/20151116223023/https://thinkprogress.org/world/2015/11/16/3722838/all-paris-attackers-identified-so-far-are-european-nationals-according-to-top-eu-official/

#10yrsago The Web is pretty great with Javascript turned off https://www.wired.com/2015/11/i-turned-off-javascript-for-a-whole-week-and-it-was-glorious/

#10yrsago If the Paris attackers weren’t using cryptography, the next ones will, and so should you https://insidesources.com/new-york-times-article-blaming-encryption-paris-attacks/

#10yrsago Zero: the number of security experts Ted Koppel consulted for hysterical cyberwar book https://www.techdirt.com/2015/11/19/ted-koppel-writes-entire-book-about-how-hackers-will-take-down-our-electric-grid-never-spoke-to-any-experts/

#10yrsago How a paid FBI informant created a terror plot that sent an activist to jail for 9 years https://theintercept.com/2015/11/19/an-fbi-informant-seduced-eric-mcdavid-into-a-bomb-plot-then-the-government-lied-about-it/

#10yrsago Google steps up to defend fair use, will fund Youtubers’ legal defenses https://publicpolicy.googleblog.com/2015/11/a-step-toward-protecting-fair-use-on.html?m=1

#10yrsago Alan Moore’s advice to unpublished authors https://www.youtube.com/watch?v=CuaWu2uhmRQ

#10yrsago Private funding of public services is bankrupting the UK https://www.telegraph.co.uk/news/nhs/11748960/The-PFI-hospitals-costing-NHS-2bn-every-year.html

#10yrsago The US government turned down Anne Frank’s visa application https://www.reuters.com/article/2007/02/14/us-annefrank-letters-idUSN1430569220070214/#HmyajvjLmsX2tVYf.97

#10yrsago Seriously, try “view source” on google.com https://xkcd.com/1605/#10yrsago

#5yrsago Tyson execs bet on covid spread in unsafe plant https://pluralistic.net/2020/11/19/disneymustpay/#you-bet-your-life

#5yrsago Disney stiffs writer https://pluralistic.net/2020/11/19/disneymustpay/#disneymustpay

#5yrsago Cyberpunk and Post-Cyberpunk https://pluralistic.net/2020/11/19/disneymustpay/#asl

#5yrsago Canada's GDPR https://pluralistic.net/2020/11/18/always-get-their-rationalisation/#consent

#5yrsago Telehealth chickenizes docs https://pluralistic.net/2020/11/18/always-get-their-rationalisation/#telehealth

#5yrsago The Mounties lied about social surveillance https://pluralistic.net/2020/11/18/always-get-their-rationalisation/#rcmp

#5yrsago Race, surveillance and tech https://pluralistic.net/2020/11/18/always-get-their-rationalisation/#asl

#1yrago Harpercollins wants authors to sign away AI training rights https://pluralistic.net/2024/11/18/rights-without-power/#careful-what-you-wish-for

#1yrago Forcing Google to spin off Chrome (and Android?) https://pluralistic.net/2024/11/19/breaking-up-is-hard-to-do/#shiny-and-chrome


Upcoming appearances (permalink)

A photo of me onstage, giving a speech, pounding the podium.



A screenshot of me at my desk, doing a livecast.

Recent appearances (permalink)



A grid of my books with Will Stahle covers..

Latest books (permalink)



A cardboard book box with the Macmillan logo.

Upcoming books (permalink)

  • "Unauthorized Bread": a middle-grades graphic novel adapted from my novella about refugees, toasters and DRM, FirstSecond, 2026

  • "Enshittification, Why Everything Suddenly Got Worse and What to Do About It" (the graphic novel), Firstsecond, 2026

  • "The Memex Method," Farrar, Straus, Giroux, 2026

  • "The Reverse-Centaur's Guide to AI," a short book about being a better AI critic, Farrar, Straus and Giroux, 2026



Colophon (permalink)

Today's top sources:

Currently writing:

  • "The Reverse Centaur's Guide to AI," a short book for Farrar, Straus and Giroux about being an effective AI critic. FIRST DRAFT COMPLETE AND SUBMITTED.

  • A Little Brother short story about DIY insulin PLANNING


This work – excluding any serialized fiction – is licensed under a Creative Commons Attribution 4.0 license. That means you can use it any way you like, including commercially, provided that you attribute it to me, Cory Doctorow, and include a link to pluralistic.net.

https://creativecommons.org/licenses/by/4.0/

Quotations and images are not included in this license; they are included either under a limitation or exception to copyright, or on the basis of a separate license. Please exercise caution.


How to get Pluralistic:

Blog (no ads, tracking, or data-collection):

Pluralistic.net

Newsletter (no ads, tracking, or data-collection):

https://pluralistic.net/plura-list

Mastodon (no ads, tracking, or data-collection):

https://mamot.fr/@pluralistic

Medium (no ads, paywalled):

https://doctorow.medium.com/

Twitter (mass-scale, unrestricted, third-party surveillance and advertising):

https://twitter.com/doctorow

Tumblr (mass-scale, unrestricted, third-party surveillance and advertising):

https://mostlysignssomeportents.tumblr.com/tagged/pluralistic

"When life gives you SARS, you make sarsaparilla" -Joey "Accordion Guy" DeVilla

READ CAREFULLY: By reading this, you agree, on behalf of your employer, to release me from all obligations and waivers arising from any and all NON-NEGOTIATED agreements, licenses, terms-of-service, shrinkwrap, clickwrap, browsewrap, confidentiality, non-disclosure, non-compete and acceptable use policies ("BOGUS AGREEMENTS") that I have entered into with your employer, its partners, licensors, agents and assigns, in perpetuity, without prejudice to my ongoing rights and privileges. You further represent that you have the authority to release me from any BOGUS AGREEMENTS on behalf of your employer.

ISSN: 3066-764X

2025-11-19T22:46:01+00:00 Fullscreen Open in Tab
Read "The Charlie Kirk purge: How 600 Americans were punished in a pro-Trump crackdown"
Read:
Two months after Charlie Kirk's assassination, a government-backed campaign has led to firings, suspensions, investigations and other action against more than 600 people. Republican officials have endorsed the punishments, saying that those who glorify violence should be removed from positions of trust.
Illustration of Molly White sitting and typing on a laptop, on a purple background with 'Molly White' in white serif.
Illustration of Molly White sitting and typing on a laptop, on a purple background with 'Molly White' in white serif.
2025-11-20T00:51:04+00:00 Fullscreen Open in Tab
Published on Citation Needed: "Issue 97 – This is hardship"
2025-11-18T20:31:13+00:00 Fullscreen Open in Tab
Note published on November 18, 2025 at 8:31 PM UTC
Tue, 18 Nov 2025 13:54:06 +0000 Fullscreen Open in Tab
Pluralistic: Disney lost Roger Rabbit (18 Nov 2025)


Today's links



The final scene of Disneyland's 'Roger Rabbit's Toontown Spin,' in which Roger Rabbit is deploying a pair of portable holes; the holes have been replaced with copyright symbols, which are partially cropped.

Disney has lost Roger Rabbit (permalink)

Gary K Wolf is the author of a fantastic 1981 novel called Who Censored Roger Rabbit? which Disney licensed and turned into an equally fantastic 1988 live action/animated hybrid movie called Who Framed Roger Rabbit? But despite the commercial and critical acclaim of the movie, Disney hasn't made any feature-length sequels.

This is a nightmare scenario for a creator: you make a piece of work that turns out to be incredibly popular, but you've licensed it to a kind of absentee landlord who owns the rights but refuses to exercise them. Luckily, the copyright system contains a provision designed to rescue creative workers who fall into this trap: "Termination of Transfer."

"Termination of Transfer" was introduced via the 1976 Copyright Act. It allows creators to unilaterally cancel the copyright licenses they have signed over to others, by waiting 35 years and then filing some paperwork with the US Copyright Office.

Termination is a powerful copyright policy, and unlike most copyright, it solely benefits creative workers and not our bosses. Copyright is a very weak tool for protecting creators' interests, because copyright only gives us something to bargain with, without giving us any bargaining power, which means that copyright becomes something we bargain away.

Think of it this way: for the past 50 years, copyright has only expanded in every direction. Copyright now lasts longer, covers more kinds of works, prohibits more uses without permission, and carries stiffer penalties. The media industry is now larger and more profitable than at any time in history. But at the same time, the amount of money being earned by creative workers has only fallen over this period, both in real terms (how much money an average creative worker brings home) and as a share of the total (what percentage of the revenues from a creator's work the creator gets to keep). How to explain this seeming paradox?

The answer lies in the structure of creative labor markets, which are brutally concentrated. Creative workers bargain with one of five publishers, one of four studios, one of three music labels, one of two app marketplaces, or just one company that controls all the ebooks and audiobooks.

The media industry isn't just a monopoly, in other words – it's also a monopsony, which is to say, a collection of powerful buyers. The middlemen who control access to our audiences have all the power, so when Congress gives creators new copyrights to bargain with, the Big Five (or Four, or Three, or Two, or One) just amend their standard, non-negotiable contract to require creators to sign those new rights over as a condition of doing business.

In other words, giving creative workers more rights without addressing their market power is like giving your bullied kid more lunch money. There isn't an amount of lunch money you can give that kid that will buy them lunch – you're just enriching the bullies. Do this for long enough and you'll make the bullies so rich they can buy off the school principal. Keep it up even longer and the bullies will hire an ad agency to run a global campaign bemoaning the plight of the hungry schoolkids and demanding that they be given more lunch money:

https://pluralistic.net/2022/08/21/what-is-chokepoint-capitalism/

This is an argument that Rebecca Giblin and I develop in our 2022 book Chokepoint Capitalism: How Big Tech and Big Content Captured Creative Labor Markets and How We'll Win Them Back:

https://www.beacon.org/Chokepoint-Capitalism-P1856.aspx

Rebecca is a law professor who is, among other things, one of the world's leading experts on Termination of Transfer, who co-authored the definitive study on the use of Termination since the 1976 Copyright Act, and the many ways this has benefited creators at the expense of media companies:

https://pluralistic.net/2021/09/26/take-it-back/

Remember, Termination is one of the only copyright policies that solely benefits creative workers. Under Termination, a media company can force you to sign away your rights in perpetuity, but you can still claim those rights back after 35 years. Termination isn't just something to bargain away, it's a new power to bargain with.

The history of how Termination got into the 1976 Copyright Act is pretty gnarly. The original text of the Termination clause made Termination automatic, after 25 years. That would have meant that every quarter century, every media company would have to go hat in hand to every creative worker whose work was still selling and beg them to sign a new contract. If your original contract stank (say, because you were just starting your career), you could demand back-payment to make up for the shitty deal you'd been forced into, and if your publisher/label/studio wouldn't cough up, you could take your work somewhere else and bargain from a position of strength, because you'd be selling a sure thing – a work that was still commercially viable after 25 years!

Automatic termination would also solve the absentee landlord problem, where a media company was squatting on your rights, keeping your book or album in print (or these days, online), but doing nothing to promote them and refusing to return the rights to you so you could sell them to some who saw the potential in your old works.

Naturally, the media industry hated this, so they watered down Termination. Instead of applying after 25 years, it now applies after 35 years. Instead of being automatic, it now requires requires creators to go through red tape at the Copyright Office.

But that wasn't enough for the media companies. In 1999, an obscure Congressional staffer named Mitch Glazier slipped a rider into the Satellite Home Viewer Improvement Act that ended Termination of Transfer for musicians. Musicians really need Termination, since record deals were and are so unconscionable and one-sided. The bill passed without anyone noticing:

https://www.wired.com/2000/08/rule-reversal-blame-it-on-riaa/

Musicians got really pissed about this, and so did Congress, who'd been hoodwinked by this despicable pismire. Congress actually convened a special session just to delete Glazier's amendment, and Glazier left his government job under a cloud.

But Glazier wasn't unemployed for long. Within three months, he'd been installed as the CEO of the Recording Industry Association of America, a job he has held ever since, where he makes over $1.3 million/year:

https://projects.propublica.org/nonprofits/organizations/131669037

I recently got a press release signed by Glazier, supporting Disney and Universal's copyright suit against Midjourney, in which begins, "There is a clear path forward through partnerships":

https://www.riaa.com/riaa-statement-on-midjourney-ai-litigation/

In other words, Glazier doesn't want these lawsuits to get rid of Midjourney and protect creative workers from the threat of AI – he just wants the AI companies to pay the media companies to make the products that his clients will use to destroy creators' livelihoods. He wants there to be a new copyright that allows creators to decide whether their work can be used to train AI models, and then he wants that right transferred to media companies who will sell it to AI companies in a bid to stop paying artists:

https://pluralistic.net/2024/10/19/gander-sauce/#just-because-youre-on-their-side-it-doesnt-mean-theyre-on-your-side

US Copyright has always acknowledged the tension between creators' rights and the rights of publishers, studios, labels and other media companies that buy creators' works. The original US copyright lasted for 14 years, and could be renewed for another 14 years, but only by the creator (not by the publisher). This meant that if a work was still selling after 14 years, the publisher would have to convince the writer to renew the copyright, or the work would go into the public domain.

This was in an era in which writers were typically paid a flat fee for their work, so from a writer's perspective, it didn't matter if the publisher made any money from subsequent sales of their books, or whether the book entered the public domain so that anyone could sell it. The writer made the same amount either way: zero.

Copyright's original 14 year renewal was a way for creative labor markets to look back and address historic injustices. If your publisher underpaid you 14 years ago, you could demand that they make good on their moral obligation to you, and if they refused, you could punish them by putting the work into the public domain.

Termination has been a huge boon to artists of all description from Stephen King to Ann M Martin, creator of The Babysitters' Club. One of my favorite examples is funk legend George Clinton, whose shitweasel manager forged his signature on a contract and stole his royalties for decades (the reason Clinton is still touring isn't merely that he's an unstoppable funk god, but because he's broke). Clinton eventually gave up on suing his ex-manager and instead just filed for Termination of Transfer:

https://www.billboard.com/pro/george-clinton-lawsuit-ex-agent-music-rights/

If that sounds familiar, it may be because I used it as the basis for a subplot in my novel The Bezzle:

https://us.macmillan.com/books/9781250865878/thebezzle/

Back to Roger Rabbit. Author Gary K Wolf has successfully filed for Termination of Transfer, meaning he's recovered the rights to Roger Rabbit and the other characters from his novel:

https://www.imnotbad.com/2025/11/roger-rabbit-copyright-reverts-to.html

He discusses his plans for a sequel starring Jessica Rabbit in this interview with "I'm Not Bad TV":

https://www.youtube.com/watch?v=L_0lUiplxZk

Writing about the termination for Boing Boing, Ruben Bolling wonders what this means for things like the Roger Rabbit ride at Disneyland, and the ongoing distribution of the film:

https://boingboing.net/2025/11/17/disney-loses-the-rights-to-roger-rabbit-characters-as-they-revert-to-original-author-of-novel.html

It's not clear to me what the answer is but my guess is that Disney will have to offer Wolf enough money that he agrees to keep the film in distribution and the ride running. Which is the point: when you sell your work for film adaptation, no one know if it's going to be a dud or a classic. Termination is copyright's lookback, a way to renegotiate the deal once you've gotten the leverage that comes from success.

If you have a work you signed away the copyright for 35 years or more ago, here is a tool from Creative Commons and the Authors Alliance for terminating the transfer and getting your rights back (disclosure: I am an unpaid member of the Authors Alliance advisory board):

https://rightsback.org/

(Image: Ken Lund, CC BY-SA 2.0, modified)


Hey look at this (permalink)



A shelf of leatherbound history books with a gilt-stamped series title, 'The World's Famous Events.'

Object permanence (permalink)

#20yrsago Amazon offers refunds for all Sony rootkit CDs https://craphound.com/amznxmpsonycd.txt

#20yrsago Uninstaller for Sony’s other malware screws up your PC https://blog.citp.princeton.edu/2005/11/17/not-again-uninstaller-iotheri-sony-drm-also-opens-huge-security-hole/

#20yrsago Schneier: Why didn’t anti-virus apps defend us against Sony’s rootkit? https://web.archive.org/web/20051124121434/https://www.wired.com/news/print/0,1294,69601,00.html

#20yrsago Sony still advising public to install rootkits https://web.archive.org/web/20051124053020/https://cp.sonybmg.com/xcp/english/howtouse.html

#15yrsago Hilarious story of disastrous cross-country move with dogs https://hyperboleandahalf.blogspot.com/2010/11/dogs-dont-understand-basic-concepts.html

#15yrsago UK gov’t promises to allow telcos to hold Brits hostage on “two-speed” Internet https://www.bbc.co.uk/news/uk-politics-11773574

#15yrsago Sexually assaulted by a TSA groper https://web.archive.org/web/20101116004124/https://www.ourlittlechatterboxes.com/2010/11/tsa-sexual-assault.html

#10yrsago Former ISIS hostage: they want us to retaliate https://www.theguardian.com/commentisfree/2015/nov/16/isis-bombs-hostage-syria-islamic-state-paris-attacks?CMP=share_btn_tw

#10yrsago There is no record of US mass surveillance ever preventing a large terror attack https://theintercept.com/2015/11/17/u-s-mass-surveillance-has-no-record-of-thwarting-large-terror-attacks-regardless-of-snowden-leaks/

#10yrsago The final Pratchett: The Shepherd’s Crown https://memex.craphound.com/2015/11/17/the-final-pratchett-the-shepherds-crown/

#10yrsago DRM in TIG welders https://www.youtube.com/watch?v=b6mlr_MX2VI

#10yrsago We treat terrorism as more costly than it truly is https://timharford.com/2015/11/nothing-to-fear-but-fear-itself/

#10yrsago David Cameron capitulates to terror, proposes Britain’s USA Patriot Act https://web.archive.org/web/20151117154831/https://thestack.com/security/2015/11/16/cameron-draft-investigatory-powers-bill-timetable-paris/

#5yrsago Storage Wars https://pluralistic.net/2020/11/17/u-stor-it/#nyc

#5yrsago Cross-Media Sci-Fi with Amber Benson and John Rogers https://pluralistic.net/2020/11/17/u-stor-it/#asl


Upcoming appearances (permalink)

A photo of me onstage, giving a speech, pounding the podium.



A screenshot of me at my desk, doing a livecast.

Recent appearances (permalink)



A grid of my books with Will Stahle covers..

Latest books (permalink)



A cardboard book box with the Macmillan logo.

Upcoming books (permalink)

  • "Unauthorized Bread": a middle-grades graphic novel adapted from my novella about refugees, toasters and DRM, FirstSecond, 2026

  • "Enshittification, Why Everything Suddenly Got Worse and What to Do About It" (the graphic novel), Firstsecond, 2026

  • "The Memex Method," Farrar, Straus, Giroux, 2026

  • "The Reverse-Centaur's Guide to AI," a short book about being a better AI critic, Farrar, Straus and Giroux, 2026



Colophon (permalink)

Today's top sources:

Currently writing:

  • "The Reverse Centaur's Guide to AI," a short book for Farrar, Straus and Giroux about being an effective AI critic. FIRST DRAFT COMPLETE AND SUBMITTED.

  • A Little Brother short story about DIY insulin PLANNING


This work – excluding any serialized fiction – is licensed under a Creative Commons Attribution 4.0 license. That means you can use it any way you like, including commercially, provided that you attribute it to me, Cory Doctorow, and include a link to pluralistic.net.

https://creativecommons.org/licenses/by/4.0/

Quotations and images are not included in this license; they are included either under a limitation or exception to copyright, or on the basis of a separate license. Please exercise caution.


How to get Pluralistic:

Blog (no ads, tracking, or data-collection):

Pluralistic.net

Newsletter (no ads, tracking, or data-collection):

https://pluralistic.net/plura-list

Mastodon (no ads, tracking, or data-collection):

https://mamot.fr/@pluralistic

Medium (no ads, paywalled):

https://doctorow.medium.com/

Twitter (mass-scale, unrestricted, third-party surveillance and advertising):

https://twitter.com/doctorow

Tumblr (mass-scale, unrestricted, third-party surveillance and advertising):

https://mostlysignssomeportents.tumblr.com/tagged/pluralistic

"When life gives you SARS, you make sarsaparilla" -Joey "Accordion Guy" DeVilla

READ CAREFULLY: By reading this, you agree, on behalf of your employer, to release me from all obligations and waivers arising from any and all NON-NEGOTIATED agreements, licenses, terms-of-service, shrinkwrap, clickwrap, browsewrap, confidentiality, non-disclosure, non-compete and acceptable use policies ("BOGUS AGREEMENTS") that I have entered into with your employer, its partners, licensors, agents and assigns, in perpetuity, without prejudice to my ongoing rights and privileges. You further represent that you have the authority to release me from any BOGUS AGREEMENTS on behalf of your employer.

ISSN: 3066-764X

Mon, 17 Nov 2025 16:54:35 +0000 Fullscreen Open in Tab
Pluralistic: The games industry's self-induced traumatic brain injury (17 Nov 2025)


Today's links



A street-pole trashcan. In it are a collection of old-school video-game console controllers.

The games industry's self-induced traumatic brain injury (permalink)

Words have power. In 1991, I read "The Wonderful Power of Storytelling," the transcript of Bruce Sterling's keynote speech for that year's Game Developers Conference in San Jose, CA, and within a year, I'd dropped out of university to become a programmer:

https://bruces.medium.com/the-wonderful-power-of-storytelling-by-bruce-sterling-1991-9d2846c2c5df

Bruce's speech wasn't the only reason I dropped out, but it's certainly been the most durable, and I frequently return to it in my mind as I navigate the difficult and turbulent waters of art and technology. In particular, I've had much cause to ponder Sterling's ideas about the very weird way that game developers relate to their art-form's history:

My art, science fiction writing, is pretty new as literary arts go, but it labors under the curse of three thousand years of literacy. In some weird sense I’m in direct competition with Homer and Euripides. I mean, these guys aren’t in the SFWA, but their product is still taking up valuable rack-space. You guys on the other hand get to reinvent everything every time a new platform takes over the field. This is your advantage and your glory. This is also your curse. It’s a terrible kind of curse really…

…A lot of our art aspires to the condition of software, our art today wants to be digital… But our riches of information are in some deep and perverse sense a terrible burden to us. They’re like a cognitive load. As a digitized information-rich culture nowadays, we have to artificially invent ways to forget stuff. I think this is the real explanation for the triumph of compact disks…

…The real advantage of CDs is that they allow you to forget all your vinyl records. You think you love this record collection that you’ve amassed over the years. But really the sheer choice, the volume, the load of memory there is secretly weighing you down…

…By dumping the platform you dump everything attached to the platform and my god what a blessed secret relief. What a relief not to remember it, not to think about it, not to have it take up disk-space in your head…

…I’ve noticed though that computer game designers don’t look much to the past. All their idealized classics tend to be in reverse, they’re projected into the future. When you’re a game designer and you’re waxing very creative and arty, you tend to measure your work by stuff that doesn’t exist yet…

… I can see that it’s very seductive, but at the same time I can’t help but see that the ground is crumbling under your feet. Every time a platform vanishes it’s like a little cultural apocalypse…

…I can imagine a time when all the current platforms might vanish, and then what the hell becomes of your entire mode of expression?

Even by the high standards of a Bruce Sterling keynote, this is a very good one, and Sterling does that amazing thing where he's iterating different ways of making this point, examining it from every angle, and it makes it hard to excerpt it for an article like this. I mean, you should just go and read the whole thing and then come back, honestly:

https://bruces.medium.com/the-wonderful-power-of-storytelling-by-bruce-sterling-1991-9d2846c2c5df

But the reason I quote those specific excerpts above is because of what they say about the strange terror and exhilaration of working without history, of inhabiting a world shorn of all object permanence. This was a very live question in those days. In 1993, Wired's Jargon Watch column ran a definition for "Pickling":

Archiving a working model of a computer to read data stored in that computer's format. Apple Computer has pickled a shrink-wrapped Apple II in a vault so that it can read Apple II software, perhaps in the not-too-distant future.

https://www.wired.com/1993/05/jargon-watch-12/

In 1996, Brewster Kahle founded the Internet Archive, with the mission to save every version of every web-page, ever, forever. Today, the Archive holds more than a trillion pages:

https://blog.archive.org/trillion/

Digital media are paradoxical: on the one hand, nothing is easier to copy than bits. That's all a computer does, after all: copy things. What's more mass storage gets cheaper and faster and smaller every year, on a curve that puts Moore's Law to shame.

After dropping out of university, I got a job programming multimedia CD ROMs for The Voyager Company, and they sent me my first 1GB drive, which was the size of a toaster, weighed 3lbs and cost $4,000.

30 years later, I've just upgraded my laptop's SDD from 2TB to 4TB: it cost less than $300, and is both the size and weight of a stick of gum. It's 4,000 times larger, at least 10,000 times faster, is 98% lighter, and cost 97% less.

We can store a hell of a lot of data for not very much money. And at that price, we can back it up to hell and back: I rotate two backup drives at home, keeping one off-site and swapping them weekly; I also have another drive I travel with and do a daily backup on. Parts of my data are also backed up online to various cloud systems that are, themselves, also backed up.

And while drives do fail, drives that are attached to computers that people use every day tend to fail gracefully in that their material defects typically make themselves felt over time, giving ample warning (at least for attentive users) that it's time to replace them.

Given the spectacular improvements in mass storage, there's also no problem migrating data from one system to the next. Back in the 1990s, I stored a ton of my data offline and near-line, on fragile media like floppies, Zip cartridges and DAT cassettes. I pretty much never conducted a full inventory of these disks, checking to see if they were working, much less transferring them to new media. That meant that at every turn, there was the possibility that the media would have rotted; and with every generation, there was the possibility that I wouldn't be able to source a working drive that was capable of reading the old media.

But somewhere in there, storage got too cheap to meter. I transferred all those floppies – including some Apple ][+ formatted 5.25" disks I'd had since the early 1980s – to a hard drive, which was subsequently transferred to a bigger hard drive (which, paradoxically, was much smaller!) and thence to another bigger (and smaller) drive and so on, up to the 4TB drive that's presently about 7mm beneath my fingers as I type these words.

This data may not be immortal, but it's certainly a lot more loss-resistant than any comparable tranche of data in human history.

Data isn't the whole story, of course. To use the data, you have to be able to open it in a program. There, too, the problems of yesteryear have all but vanished. First came the interoperable programs, which reverse-engineered these file formats so they could be read and written with increasing fidelity to the programs they were created in:

https://www.eff.org/deeplinks/2019/06/adversarial-interoperability-reviving-elegant-weapon-more-civilized-age-slay

But then came the emulators and APIs that could simply run the old programs on new hardware. After all, computers are always getting much faster, which means that simulating a computer that's just a few years old on modern hardware is pretty trivial. Indeed, you can simulate multiple instances of the computer I wrote CD ROMs for Voyager on inside a browser window…on your phone:

https://infinitemac.org/1996/System%207.5.3

Which meant that, for quite some time, Bruce's prophecy of games living in an eternal ahistorical now, an art form whose earlier works are all but inaccessible, was dead wrong. Between emulators (MAME) and API reimplementations (WINE), a gigantic amount of gaming history has been brought back and preserved.

What's more, there's a market for this stuff. Companies like Good Old Games have gone into business licensing and reviving the games people love. But it keeps getting harder, because of a mix of "Digital Rights Management" (the "copy-protection" that games companies pursue with a virulence that borders on mania) and the difficulty of tracking down rightsholders:

https://www.pcgamer.com/games/just-in-case-you-thought-reviving-dead-games-seemed-easy-enough-gog-had-to-hire-a-private-investigator-to-find-an-ip-holder-living-off-the-grid-for-its-preservation-program/

And doing this stuff without permission is a fraught business, because the big games companies hate games preservation and wage vicious war on their own biggest fans to stamp it out:

https://pluralistic.net/2020/11/21/wrecking-ball/#ssbm

Which means that the games preservation effort is coming full circle, back to Bruce Sterling's 1991 description of "the ground crumbling under your feet"; of an endless series of "little cultural apocalypses."

It doesn't have to be this way. The decades since Bruce's talk proved that games can and should be preserved, that artists and their audiences need to continue to access these works even if the companies that make them would rather "reinvent everything every time a new platform takes over the field" and not have to be "in direct competition with Homer and Euripides."

The "Stop Killing Games" consumer movement is trying to save the library that games publishers have been trying to burn down since the 1990s:

https://www.stopkillinggames.com/

They're currently hoping to get games preservation built into the new EU "Digital Fairness" Act:

https://ec.europa.eu/info/law/better-regulation/have-your-say/initiatives/14622-Digital-Fairness-Act

It's a good tactical goal. After all, it's manifestly "unfair" to charge you money for a game and then take the game away later, whether that's because you don't want to pay to keep the servers on (or let someone else run them), or because you don't want the old game to exist in order to coerce your customers into buying a new one.

Or both.

No matter the reason, there is nothing good about the games industry's decades-long project of erasing its own past. It's bad for gamers, it's bad for game developers, and it's bad for games. No art form can exist in a permanent, atemporal now, with its history erased as quickly as it's created.

(Image: Erica Fischer, CC BY 2.0, modified)


Hey look at this (permalink)



A shelf of leatherbound history books with a gilt-stamped series title, 'The World's Famous Events.'

Object permanence (permalink)

#20yrsago 5000 music cylinders digitized and posted https://cylinders.library.ucsb.edu/

#20yrsago Girl who didn’t do homework put on street with WILL WORK FOR FOOD sign https://www.washingtonpost.com/wp-dyn/content/article/2005/11/16/AR2005111601926.html

#20yrsago Sony rootkit roundup, part II https://memex.craphound.com/2005/11/16/sony-rootkit-roundup-part-ii/

#20yrsago Sony CDs banned in the workplace https://memex.craphound.com/2005/11/16/sony-cds-banned-in-the-workplace/

#20yrsago Sony waits 3 DAYS to withdraw dangerous “uninstaller” for its rootkit https://web.archive.org/web/20051124053710/https://cp.sonybmg.com/xcp/english/uninstall.html

#20yrsago Student folds paper 12 times! https://web.archive.org/web/20051102085038/https://www.pomonahistorical.org/12times.htm

#20yrsago Barenaked Ladies release album on USB stick https://web.archive.org/web/20051124234734/http://www.bnlmusic.com/news/default.asp

#20yrsago Latest Sony news: 100% of CDs with rootkits, mainstream condemnation, retailers angry https://memex.craphound.com/2005/11/15/latest-sony-news-100-of-cds-with-rootkits-mainstream-condemnation-retailers-angry/

#20yrsago Sony disavows lockware patent https://web.archive.org/web/20051126133522/https://www.playfuls.com/news_3827.html

#20yrsago Sony infects more than 500k networks, including military and govt https://web.archive.org/web/20051231222014/http://www.doxpara.com/?q=/node/1129

#20yrsago Sony’s spyware “remover” creates huge security hole https://blog.citp.princeton.edu/2005/11/15/sonys-web-based-uninstaller-opens-big-security-hole-sony-recall-discs/

#20yrsago Sony issues non-apology for compromising your PC https://web.archive.org/web/20051124053248/http://cp.sonybmg.com/xcp/

#20yrsago Sory Electronics: Will Sony make amends for infecting our computers? https://web.archive.org/web/20051124203930/http://soryelectronics.com/

#15yrsago UK gov’t wants to legalize racial profiling https://www.theguardian.com/uk/2010/nov/15/stop-and-search-equality-commission

#15yrsago Canadian writers’ group issues FUD warnings about new copyright bill https://web.archive.org/web/20101117004549/http://www.michaelgeist.ca/content/view/5445/125/

#15yrsago Misprinted prefab houses https://zeitguised.wordpress.com/2010/11/08/concrete-misplots/

#15yrsago WWI-era photos of people pretending to be patriotic pixels https://web.archive.org/web/20101124060200/https://www.hammergallery.com/images/peoplepictures/people

#15yrsago Steampunk bandwidth gauge https://web.archive.org/web/20101118071250/https://blog.skytee.com/2010/11/torrentmeter-a-steampunk-bandwidth-meter/

#15yrsago UK gov’t apologizes for decades of secret nuclear power industry corpse-mutilation https://web.archive.org/web/20101119171708/http://uk.reuters.com/article/idUKTRE6AF4CT20101116

#15yrsago Understanding COICA, America’s horrific proposed net-censorship bill https://www.eff.org/deeplinks/2010/11/case-against-coica

#15yrsago London cops shut down anti-police website; mirrors spring up all over the net https://www.theguardian.com/education/2010/nov/16/web-advice-students-avoid-arrest

#15yrsago TSA tee: “We get to touch your junk” https://web.archive.org/web/20101119090103/http://skreened.com/oped/junk-search

#15yrsago Indie Band Survival Guide: soup-to-nuts, no-BS manual for 21st century artistic life https://memex.craphound.com/2010/11/16/indie-band-survival-guide-soup-to-nuts-no-bs-manual-for-21st-century-artistic-life/

#15yrsago New aviation risk: pleats https://web.archive.org/web/20101118015618/http://www.thelocal.de/sci-tech/20101116-31209.html

#10yrsago How scientists trick themselves (and how they can prevent it) https://www.nature.com/articles/526182a

#10yrsago Is Batman’s evidence admissible in court? https://lawandthemultiverse.com/2015/11/16/batman-constitution-how-gotham-da-convict-criminals/

#10yrsago Hello From the Magic Tavern: hilarious, addictive improv podcast https://memex.craphound.com/2015/11/16/hello-from-the-magic-tavern-hilarious-addictive-improv-podcast/

#10yrsago The Internet will always suck https://locusmag.com/feature/cory-doctorow-the-internet-will-always-suck/

#10yrsago How terrorists trick Western governments into doing their work for them https://web.archive.org/web/20151119044939/http://gawker.com/terrorism-works-1678049997

#5yrsago Youtube-dl is back https://pluralistic.net/2020/11/16/pill-mills/#yt-dl

#5yrsago HHS to pharma: stop bribing writing docs https://pluralistic.net/2020/11/16/pill-mills/#oig

#5yrsago The Attack Surface Lectures https://pluralistic.net/2020/11/16/pill-mills/#asl

#1yrago Canada's ground-breaking, hamstrung repair and interop laws https://pluralistic.net/2024/11/15/radical-extremists/#sex-pest


Upcoming appearances (permalink)

A photo of me onstage, giving a speech, pounding the podium.



A screenshot of me at my desk, doing a livecast.

Recent appearances (permalink)



A grid of my books with Will Stahle covers..

Latest books (permalink)



A cardboard book box with the Macmillan logo.

Upcoming books (permalink)

  • "Unauthorized Bread": a middle-grades graphic novel adapted from my novella about refugees, toasters and DRM, FirstSecond, 2026

  • "Enshittification, Why Everything Suddenly Got Worse and What to Do About It" (the graphic novel), Firstsecond, 2026

  • "The Memex Method," Farrar, Straus, Giroux, 2026

  • "The Reverse-Centaur's Guide to AI," a short book about being a better AI critic, Farrar, Straus and Giroux, 2026



Colophon (permalink)

Today's top sources:

Currently writing:

  • "The Reverse Centaur's Guide to AI," a short book for Farrar, Straus and Giroux about being an effective AI critic. FIRST DRAFT COMPLETE AND SUBMITTED.

  • A Little Brother short story about DIY insulin PLANNING


This work – excluding any serialized fiction – is licensed under a Creative Commons Attribution 4.0 license. That means you can use it any way you like, including commercially, provided that you attribute it to me, Cory Doctorow, and include a link to pluralistic.net.

https://creativecommons.org/licenses/by/4.0/

Quotations and images are not included in this license; they are included either under a limitation or exception to copyright, or on the basis of a separate license. Please exercise caution.


How to get Pluralistic:

Blog (no ads, tracking, or data-collection):

Pluralistic.net

Newsletter (no ads, tracking, or data-collection):

https://pluralistic.net/plura-list

Mastodon (no ads, tracking, or data-collection):

https://mamot.fr/@pluralistic

Medium (no ads, paywalled):

https://doctorow.medium.com/

Twitter (mass-scale, unrestricted, third-party surveillance and advertising):

https://twitter.com/doctorow

Tumblr (mass-scale, unrestricted, third-party surveillance and advertising):

https://mostlysignssomeportents.tumblr.com/tagged/pluralistic

"When life gives you SARS, you make sarsaparilla" -Joey "Accordion Guy" DeVilla

READ CAREFULLY: By reading this, you agree, on behalf of your employer, to release me from all obligations and waivers arising from any and all NON-NEGOTIATED agreements, licenses, terms-of-service, shrinkwrap, clickwrap, browsewrap, confidentiality, non-disclosure, non-compete and acceptable use policies ("BOGUS AGREEMENTS") that I have entered into with your employer, its partners, licensors, agents and assigns, in perpetuity, without prejudice to my ongoing rights and privileges. You further represent that you have the authority to release me from any BOGUS AGREEMENTS on behalf of your employer.

ISSN: 3066-764X

Sat, 15 Nov 2025 11:28:22 +0000 Fullscreen Open in Tab
Pluralistic: Zohran Mamdani's world-class photocopier-kicker (15 Nov 2025)


Today's links



A nighttime scene in Times Square in the 1960s, with the Camel ad replaced with a Zohran Mamdani ad. In the foreground the Statue of Liberty is kicking a photocopier.

Zohran Mamdani's world-class photocopier-kicker (permalink)

The most exciting thing about Biden's antitrust enforcers was how good they were at their jobs. They were dead-on chapter-and-verse on every authority and statute available to the administrative branch, and they set about in earnest figuring out how to use those powers to help the American people:

https://www.eff.org/de/deeplinks/2021/08/party-its-1979-og-antitrust-back-baby

It was a remarkable contrast from the default Democratic Party line, which is to insist that being elected gives you no power at all, because of filibusters or Republicans or pollsters or decorum or billionaire donors or Mercury in retrograde. It's also a remarkable contrast from Republicans, whose approach to politics is "fuck you, we said so, and our billionaires have showered the Supreme Court in enough money to make that stick."

But under Biden, the trustbusters that had been chosen and fought for by the Warren-Sanders wing of the party proved themselves to be both a) incredibly principled; and b) incredibly skilled. They memorized the rulebook(s) and then figured out what they needed to do to mobilize those rules to makes Americans' lives better by shielding them from swindlers, predators and billionaires (often the same person, obvs).

They epitomized the joke about the photocopier repair tech, who comes into the office, delivers a swift kick to the xerox machine, and hands you a bill for $75.

"$75 for kicking the photocopier?"

"No, it's $5 to kick the photocopier, and $70 for knowing where to kick it."

https://pluralistic.net/2022/10/18/administrative-competence/#i-know-stuff

One of Biden's best photocopier kickers was and is Lina Khan. She embodies the incredible potential of a fully operational battle-station, which is to say that she embodies the awesome power of a skilled technocrat who is also deeply ethical and genuinely interested in helping the public. Technocrats get a bad name, because they tend to be empty suits like Pete Buttigieg, who either didn't know what powers he had, or lacked the courage (or desire) to wield them:

https://pluralistic.net/2023/01/10/the-courage-to-govern/#whos-in-charge

But another way of saying "technocrat" is "someone who is very good at their job." And that's Khan.

You'll never guess what Khan is doing now: she's co-chairing Zohran Mamdani's transition team!

https://www.theguardian.com/commentisfree/2025/nov/12/yes-new-york-will-soon-be-under-new-management-but-zohran-mamdani-is-just-the-start

Khan's role in the Mamdani administration will be familiar to those of us who cheered her on at the Federal Trade Commission: she is metabolizing the rules that define the actions that mayors are allowed to take, figuring out how to use those actions to improve the lives of working New Yorkers, and making a plan to combine the former with the latter to make a real difference:

https://www.semafor.com/article/11/12/2025/lina-khans-populist-plan-for-new-york-cheaper-hot-dogs-and-other-things

Front and center is the New York City Consumer Protection Law of 1969, which contains a broad prohibition on "unconscionable" commercial practices:

https://repository.law.umich.edu/cgi/viewcontent.cgi?article=2404&context=mjlr

There are many statute books that contain a law like this. For example, Section 5 of the Federal Trade Commission Act bans "unfair and deceptive" practices, and this rule is so useful that it was transposed, almost verbatim, into the statute that defines the Department of Transportation's powers:

https://pluralistic.net/2023/01/16/for-petes-sake/#unfair-and-deceptive

Now, this isn't carte blanche for enforcers to simply point at anything they don't like and declare it to be "unconscionable" or "unfair" or "deceptive" and shut it down. To use these powers, enforcers must first "develop a record" by getting feedback from the public about the problem. The normal way to do this is through "notice and comment," where you collect comments from anyone who wants to weigh in on the issue. Practically speaking, though, "anyone" turns out to be "lawyers and lobbyists working for industry," who are the only people who pay attention to this kind of thing and know how to navigate it.

When Khan was running the FTC, she launched plenty of notice and comment efforts, but she went much further, doing "listening tours" in which she and her officials and staff went to the people, traveling the country convening well-attended public meetings where everyday people got to weigh in on these issues. This is an incredibly powerful approach, because enforcers can only act to address the issues in the record, and if you only hear from lawyers and lobbyists, you can only act to address their concerns.

Remember when Mamdani was on the campaign trail and he went out and talked to street vendors about why halal cart food had gotten so expensive? It turns out that halal cart vendors each have to pay tens of thousands of dollars to economic parasites who've cornered the market on food cart licenses, which they rent out at exorbitant markups to vendors, who pass those costs on to New Yorkers every lunchtime:

https://documentedny.com/2025/11/04/halal-food-trucks-back-mamdani/

That's the kind of thing Khan did when she was running the FTC, identifying serious problems, then seeking out the everyday people best suited to describing how the underlying scams hurt, and how they harmed everyday people:

https://pluralistic.net/2024/07/24/gouging-the-all-seeing-eye/#i-spy

Khan's already picked out some "unconscionable" practices that the mayor has "standalone authority" to address: everything from hospitals that price gouge on over-the-counter pain meds to sports stadiums that gouge fans on hot dogs and beer. She's taking aim at "algorithmic pricing" (when companies use commercial surveillance data to determine whether you're desperate and raise prices to take advantage of that fact) and junk fees (where the price you pay goes way up at checkout time to pay for a bunch of vague "services" that you can't opt out of).

This is already making all the right people lose their minds, with screaming headlines about how this will "deliver a socialist agenda":

https://web.archive.org/web/20251114230206/https://nypost.com/2025/11/14/us-news/zohran-mamdanis-transition-leader-lina-khan-seeks-more-power-for-him/

In a long-form interview with Jon Stewart, Khan goes deep on her regulatory philosophy and the way she's going to bring the same fire she brought to the most effective FTC since the Carter administration to Mamdani's historic administration of New York City, a municipality with a population and economy that's larger than many US states and foreign nations:

https://www.youtube.com/watch?v=vRJWM_3OW2Y

One important aspect of Khan's work that she is always at pains to stress is deterrence. When an enforcer acts against a company that is scamming and preying upon the public, their private finances and internal communications become a matter of public record. Employees and executives have to be painstakingly instructed and monitored so that they don't say anything that will prejudice their cases. All this happens irrespective of the eventual outcome of the case.

Remember: we're at the tail end of a 40-year experiment in official tolerance and encouragement for monopolies and corporate predation. Those lost generations saw the construction of a massive edifice of bad case-law and judicial intuition. Smashing that wall won't happen overnight. There will be a lot of losses. But when the process is (part of) the punishment, the mere existence of someone like Khan in a position of power can terrify companies into being on their best behavior.

As MLK put it, "The law can't make a man love me, but it can stop him from lynching me, and that's pretty important."

The oligarchs that acquired their wealth and power by ripping off New Yorkers will never truly believe that working people deserve a fair shake – but if they're sufficiently afraid of the likes of Khan, they'll damned well act like they do.

(Image: lee, CC BY-SA 4.0, modified)


Hey look at this (permalink)



A shelf of leatherbound history books with a gilt-stamped series title, 'The World's Famous Events.'

Object permanence (permalink)

#20yrsago Sony begins to recall some infected CDs https://web.archive.org/web/20051127235441/http://www.usatoday.com/money/industries/technology/2005-11-14-sony-cds_x.htm

#20yrsago Sony’s rootkit uninstaller is really dangerous https://blog.citp.princeton.edu/2005/11/14/dont-use-sonys-web-based-xcp-uninstaller/

#20yrsago Table made from ancient, giant hard-drive platter https://web.archive.org/web/20050929185244/https://grandideastudio.com/portfolio/index.php?id=1&prod=20

#20yrsago EFF to Sony: you broke it, you oughta fix it https://web.archive.org/web/20051126084944/http://www.eff.org/IP/DRM/Sony-BMG/?f=open-letter-2005-11-14.html

#20yrsago Sony anti-customer technology roundup and time-line https://memex.craphound.com/2005/11/14/sony-anti-customer-technology-roundup-and-time-line/

#20yrsago Visa’s “free” laptop costs at least $60 more than retail in fees https://web.archive.org/web/20051125053825/http://debt-consolidation.strategy-blogs.com/2005/10/free-laptop-from-visa.html

#20yrsago Sony’s rootkit infringes on software copyrights https://web.archive.org/web/20061108150242/https://dewinter.com/modules.php?name=News&file=article&sid=215

#20yrsago Gizmodo flamed by crazy inventor; turns out he’s a crook https://web.archive.org/web/20051126101341/https://us.gizmodo.com/gadgets/portable-media/iload-inventor-vents-is-out-on-bail-136934.php

#20yrsago Fox counsels viewers to share videos of shows https://memex.craphound.com/2005/11/13/fox-counsels-viewers-to-share-videos-of-shows/

#20yrsago Sony’s malware uninstaller leaves your computer vulnerable https://www.hack.fi/~muzzy/sony-drm/

#15yrsago Tim Wu on the new monopolists: a “last chapter” for The Master Switch https://web.archive.org/web/20151214010555/https://www.wsj.com/articles/SB10001424052748704635704575604993311538482

#15yrsago Man at San Diego airport opts out of porno scanner and grope, told he’ll be fined $10K unless he submits to fondling https://johnnyedge.blogspot.com/2010/11/these-events-took-place-roughly-between.html

#10yrsago 100 useful tips from a bygone era https://digitalcollections.nypl.org/search/index?q=gallaher++how+to+do+it#/?scroll=18

#10yrsago Copyfraud: Anne Frank Foundation claims father was “co-author,” extends copyright by decades https://www.nytimes.com/2015/11/14/books/anne-frank-has-a-co-as-diary-gains-co-author-in-legal-move.html

#10yrsago Startup uses ultrasound chirps to covertly link and track all your devices https://arstechnica.com/tech-policy/2015/11/beware-of-ads-that-use-inaudible-sound-to-link-your-phone-tv-tablet-and-pc/

#10yrsago Cop who unplugged his cam before killing a 19-year-old girl is rehired https://arstechnica.com/tech-policy/2015/11/cop-fired-for-having-lapel-cam-turned-off-a-lot-reinstated-to-force/

#10yrsago Hospitals are patient zero for the Internet of Things infosec epidemic https://web.archive.org/web/20151113050443/https://www.bloomberg.com/features/2015-hospital-hack/

#10yrsago Ol’ Dirty Bastard’s FBI files https://www.muckrock.com/news/archives/2015/nov/13/ol-dirty-bastard-fbi-files/

#10yrsago I-Spy Surveillance Books: a child’s first Snoopers Charter https://scarfolk.blogspot.com/2015/11/i-spy-surveillance-books.html

#10yrsago China routinely tortures human rights lawyers https://www.businessinsider.com/amnesty-international-report-on-torture-2015-11

#10yrsago Fordite: a rare mineral only found in old Detroit auto-painting facilities https://miningeology.blogspot.com/2015/07/the-most-amazing-rocks.html

#10yrsago Facebook won’t remove photo of children tricked into posing for neo-fascist group https://www.bbc.com/news/uk-england-nottinghamshire-34797757

#5yrsago Big Car wants to pump the brakes on Right to Repair https://pluralistic.net/2020/11/13/said-no-one-ever/#r2r

#1yrago America's richest Medicare fraudsters are untouchable https://pluralistic.net/2024/11/13/last-gasp/#i-cant-breathe


Upcoming appearances (permalink)

A photo of me onstage, giving a speech, pounding the podium.



A screenshot of me at my desk, doing a livecast.

Recent appearances (permalink)



A grid of my books with Will Stahle covers..

Latest books (permalink)



A cardboard book box with the Macmillan logo.

Upcoming books (permalink)

  • "Unauthorized Bread": a middle-grades graphic novel adapted from my novella about refugees, toasters and DRM, FirstSecond, 2026

  • "Enshittification, Why Everything Suddenly Got Worse and What to Do About It" (the graphic novel), Firstsecond, 2026

  • "The Memex Method," Farrar, Straus, Giroux, 2026

  • "The Reverse-Centaur's Guide to AI," a short book about being a better AI critic, Farrar, Straus and Giroux, 2026



Colophon (permalink)

Today's top sources:

Currently writing:

  • "The Reverse Centaur's Guide to AI," a short book for Farrar, Straus and Giroux about being an effective AI critic. FIRST DRAFT COMPLETE AND SUBMITTED.

  • A Little Brother short story about DIY insulin PLANNING


This work – excluding any serialized fiction – is licensed under a Creative Commons Attribution 4.0 license. That means you can use it any way you like, including commercially, provided that you attribute it to me, Cory Doctorow, and include a link to pluralistic.net.

https://creativecommons.org/licenses/by/4.0/

Quotations and images are not included in this license; they are included either under a limitation or exception to copyright, or on the basis of a separate license. Please exercise caution.


How to get Pluralistic:

Blog (no ads, tracking, or data-collection):

Pluralistic.net

Newsletter (no ads, tracking, or data-collection):

https://pluralistic.net/plura-list

Mastodon (no ads, tracking, or data-collection):

https://mamot.fr/@pluralistic

Medium (no ads, paywalled):

https://doctorow.medium.com/

Twitter (mass-scale, unrestricted, third-party surveillance and advertising):

https://twitter.com/doctorow

Tumblr (mass-scale, unrestricted, third-party surveillance and advertising):

https://mostlysignssomeportents.tumblr.com/tagged/pluralistic

"When life gives you SARS, you make sarsaparilla" -Joey "Accordion Guy" DeVilla

READ CAREFULLY: By reading this, you agree, on behalf of your employer, to release me from all obligations and waivers arising from any and all NON-NEGOTIATED agreements, licenses, terms-of-service, shrinkwrap, clickwrap, browsewrap, confidentiality, non-disclosure, non-compete and acceptable use policies ("BOGUS AGREEMENTS") that I have entered into with your employer, its partners, licensors, agents and assigns, in perpetuity, without prejudice to my ongoing rights and privileges. You further represent that you have the authority to release me from any BOGUS AGREEMENTS on behalf of your employer.

ISSN: 3066-764X

Thu, 13 Nov 2025 08:55:19 +0000 Fullscreen Open in Tab
Pluralistic: For-profit healthcare is the problem, not (just) private equity (13 Nov 2025)


Today's links



A black and white photo of an old hospital ward. A bright red river of blood courses between the beds. Dancing in the blood is Monopoly's 'Rich Uncle Pennybags.' He has removed his face to reveal a grinning skull.

For-profit healthcare is the problem, not (just) private equity (permalink)

When you are at the library, you are a patron, not a customer. When you are at school, you're a student, not a customer. When you get health care, you are a patient, not a customer.

Property rights are America's state religion, and so market-oriented language is the holy catechism. But the things we value most highly aren't property, they cannot be bought or sold in markets, and describing them as property grossly devalues them. Think of human beings: murder isn't "theft of life" and kidnapping isn't "theft of children":

https://www.theguardian.com/technology/2008/feb/21/intellectual.property

When we use markets and property relations to organize these non-market matters, horrors abound. Just look at the private equity takeover of American healthcare. PE bosses have spent more than a trillion dollars cornering regional markets on various parts of the health system:

https://pluralistic.net/2024/02/28/5000-bats/#charnel-house

The PE playbook is plunder. After PE buys a business, it borrows heavily against it (with the loan going straight into the PE investors' pockets), and then, to service that debt, the new owners cut, and cut, and cut. PE-owned hospitals are literally filled with bats because the owners stiff the exterminators:

https://prospect.org/health/2024-02-27-scenes-from-bat-cave-steward-health-florida/

Needless to say, a hospital that is full of bats has other problems. All of the high-tech medical devices are broken and no one will fix them because the PE bosses have stiffed all the repair companies and contractors. There are blood shortages, saline shortages, PPE shortages. Doctors and nurses go weeks or months without pay. The elevators don't work. Black mold climbs the walls.

When PE rolls up all the dialysis clinics in your neighborhood, the new owners fire all the skilled staff and hire untrained replacements. They dispense with expensive fripperies like sterilizing their needles:

https://www.thebignewsletter.com/p/the-dirty-business-of-clean-blood

When PE rolls up your regional nursing homes, they turn into slaughterhouses. To date, PE-owned nursing homes have stolen at least 160,000 lost life years:

https://pluralistic.net/2021/02/23/acceptable-losses/#disposable-olds

Then there's hospices, the last medical care you will ever receive. Once your doctor declares that you have less than six months or less to live, Medicare will pay a hospice $243-$1,462/day to take care of you as you die. At the top end of that rate, hospices have to satisfy a lot of conditions, but if the hospice is willing to take $243/day, they effectively have no duties to you – they don't even have to continue providing you with your regular medication or painkillers for your final days:

https://prospect.org/health/2023-04-26-born-to-die-hospice-care/

Setting up a hospice is cheap as hell. Pay a $3,000 filing fee, fill in some paperwork (which no one ever checks) and hang out a shingle. Nominally, a doctor has to oversee the operation, but PE-backed hospices save money here by having a single doctor "oversee" dozens of hospices:

https://auditor.ca.gov/reports/2021-123/index.html#pg34A

Once you rope a patient into this system, you can keep billing the government for them up to a total of $32,000, then you have to kick them out. Why would a patient with only six months to live survive to be kicked out? Because PE companies pay bounties to doctors to refer patients who aren't dying to hospices. 51% of patients in the PE-cornered hospices of Van Nuys are "live discharged":

https://pluralistic.net/2023/04/26/death-panels/#what-the-heck-is-going-on-with-CMS

However, once you're admitted to a hospice, Medicare expects you to die – so "live discharged" patients face a thick bureaucratic process to get back into the system so they can start seeing a doctor again.

So all of this is obviously very bad, a stark example of what happens when you mix the most rapacious form of capitalist plunder with the most vulnerable kind of patient. But, as Elle Rothermich writes for LPE Journal, the PE model of hospice is merely a more extreme and visible version of the ghastly outcomes that arise out of all for-profit hospice care:

https://lpeproject.org/blog/hospice-commodification-and-the-limits-of-antitrust/

The problems of PE-owned hospices are not merely a problem of the lack of competition, and applying antitrust to PE rollups of hospices won't stop the carnage, though it would certainly improve things somewhat. While once American hospices were run by nonprofits and charities, that changed in 1983 with the introduction of Medicare's hospice benefit. Today, three quarters of US hospices are private.

It's not just PE-backed hospices; the entire for-profit hospice sector is worse than the nonprofit alternative. For-profit hospices deliver worse care and worse outcomes at higher prices. They are the worst-performing hospices in the country.

This is because (as Rothermich writes) "The actual provision of care—the act of healing or attempting to heal—is broadly understood to be something more than a purely economic transaction." In other words, patients are not customers. In the hierarchy of institutional obligations, "patients" rank higher than customers. To be transformed from a "patient" into a "customer" is to be severely demoted.

Hospice care is a complex, multidisciplinary, highly individualized practice, and pain treatment spans many dimensions: "psychological, social, emotional, and spiritual as well as physical." A cash-for-service model inevitably flattens this into "a standardized list of discrete services that can each be given a monetary value: pain medication, durable medical equipment, skilled nursing visits, access to a chaplain."

As Rothermich writes, while there are benefits to blocking PE rollups and monopolization of hospices, to do so at all tacitly concedes that health care should be treated as a business, that "corporate involvement in care delivery is an inevitable, irreversible development."

Rothermich's point is that health care isn't a commodity, and to treat it as such always worsens care. It dooms patients to choosing between different kinds of horrors, and subjects health care workers to the moral injury of failing their duty to their patients in order to serve them as customers.


Hey look at this (permalink)



A shelf of leatherbound history books with a gilt-stamped series title, 'The World's Famous Events.'

Object permanence (permalink)

#20yrsago New Sony lockware prevents selling or loaning of games https://memex.craphound.com/2005/11/12/new-sony-lockware-prevents-selling-or-loaning-of-games/

#20yrsago Dr Seuss meets Star Trek https://web.archive.org/web/20051126025052/http://www.seuss.org/seuss/seuss.sttng.html

#20yrsago Sony’s other malicious audio CD trojan https://memex.craphound.com/2005/11/12/sonys-other-malicious-audio-cd-trojan/

#15yrsago Will TSA genital grope/full frontal nudity “security” make you fly less? https://web.archive.org/web/20101115011017/https://blogs.reuters.com/ask/2010/11/12/are-new-security-screenings-affecting-your-decision-to-fly/

#15yrsago Make inner-tube laces, turn your shoes into slip-ons https://www.instructables.com/Make-normal-shoes-into-slip-ons-with-inner-tubes/

#15yrsago Tractor sale gone bad ends with man eating own beard https://web.archive.org/web/20101113200759/http://www.msnbc.msn.com/id/40136299

#10yrsago San Francisco Airport security screeners charged with complicity in drug-smuggling https://www.justice.gov/usao-ndca/pr/three-san-francisco-international-airport-security-screeners-charged-fraud-and

#10yrsago Female New Zealand MPs ejected from Parliament for talking about their sexual assault https://www.theguardian.com/world/2015/nov/11/new-zealand-female-mps-mass-walkout-pm-rapists-comment

#10yrsago Councillor who voted to close all public toilets gets a ticket for public urination https://uk.news.yahoo.com/councillor-cut-public-toilets-fined-094432429.html#1snIQOG

#10yrsago Edward Snowden’s operational security advice for normal humans https://theintercept.com/2015/11/12/edward-snowden-explains-how-to-reclaim-your-privacy/

#10yrsago Not (just) the War on Drugs: the difficult, complicated truth about American prisons https://jacobin.com/2015/03/mass-incarceration-war-on-drugs/

#10yrsago Britons’ Internet access bills will soar to pay for Snoopers Charter https://www.theguardian.com/technology/2015/nov/11/broadband-bills-increase-snoopers-charter-investigatory-powers-bill-mps-warned

#10yrsago How big offshoring companies pwned the H-1B process, screwing workers and businesses https://www.nytimes.com/2015/11/11/us/large-companies-game-h-1b-visa-program-leaving-smaller-ones-in-the-cold.html?_r=0

#5yrsago Anti-bear robo-wolves https://pluralistic.net/2020/11/12/thats-what-xi-said/#robo-lobo

#5yrsago Xi on interop and lock-in https://pluralistic.net/2020/11/12/thats-what-xi-said/#with-chinese-characteristics

#5yrsago Constantly Wrong https://pluralistic.net/2020/11/12/thats-what-xi-said/#conspiratorialism


Upcoming appearances (permalink)

A photo of me onstage, giving a speech, pounding the podium.



A screenshot of me at my desk, doing a livecast.

Recent appearances (permalink)



A grid of my books with Will Stahle covers..

Latest books (permalink)



A cardboard book box with the Macmillan logo.

Upcoming books (permalink)

  • "Unauthorized Bread": a middle-grades graphic novel adapted from my novella about refugees, toasters and DRM, FirstSecond, 2026

  • "Enshittification, Why Everything Suddenly Got Worse and What to Do About It" (the graphic novel), Firstsecond, 2026

  • "The Memex Method," Farrar, Straus, Giroux, 2026

  • "The Reverse-Centaur's Guide to AI," a short book about being a better AI critic, Farrar, Straus and Giroux, 2026



Colophon (permalink)

Today's top sources:

Currently writing:

  • "The Reverse Centaur's Guide to AI," a short book for Farrar, Straus and Giroux about being an effective AI critic. FIRST DRAFT COMPLETE AND SUBMITTED.

  • A Little Brother short story about DIY insulin PLANNING


This work – excluding any serialized fiction – is licensed under a Creative Commons Attribution 4.0 license. That means you can use it any way you like, including commercially, provided that you attribute it to me, Cory Doctorow, and include a link to pluralistic.net.

https://creativecommons.org/licenses/by/4.0/

Quotations and images are not included in this license; they are included either under a limitation or exception to copyright, or on the basis of a separate license. Please exercise caution.


How to get Pluralistic:

Blog (no ads, tracking, or data-collection):

Pluralistic.net

Newsletter (no ads, tracking, or data-collection):

https://pluralistic.net/plura-list

Mastodon (no ads, tracking, or data-collection):

https://mamot.fr/@pluralistic

Medium (no ads, paywalled):

https://doctorow.medium.com/

Twitter (mass-scale, unrestricted, third-party surveillance and advertising):

https://twitter.com/doctorow

Tumblr (mass-scale, unrestricted, third-party surveillance and advertising):

https://mostlysignssomeportents.tumblr.com/tagged/pluralistic

"When life gives you SARS, you make sarsaparilla" -Joey "Accordion Guy" DeVilla

READ CAREFULLY: By reading this, you agree, on behalf of your employer, to release me from all obligations and waivers arising from any and all NON-NEGOTIATED agreements, licenses, terms-of-service, shrinkwrap, clickwrap, browsewrap, confidentiality, non-disclosure, non-compete and acceptable use policies ("BOGUS AGREEMENTS") that I have entered into with your employer, its partners, licensors, agents and assigns, in perpetuity, without prejudice to my ongoing rights and privileges. You further represent that you have the authority to release me from any BOGUS AGREEMENTS on behalf of your employer.

ISSN: 3066-764X

Wed, 12 Nov 2025 06:03:26 +0000 Fullscreen Open in Tab
Pluralistic: A tale of three customer service chatbots (12 Nov 2025)


Today's links



A queue of people in 1950s garb, colorized in garish tones, waiting for a wicket with a sign labeled SERVICE. Behind the counter stands a male figure in a suit whose head has been replaced with a set of chattering teeth.

A tale of three customer service chatbots (permalink)

AI can't do your job, but an AI salesman can convince your boss to fire you and replace you with an AI that can't do your job. Nowhere is that more true than in customer service:

https://pluralistic.net/2025/08/06/unmerchantable-substitute-goods/#customer-disservice

Customer service is a pure cost center for companies, and the best way to reduce customer service costs is to make customer service so terrible that people simply give up. For decades, companies have outsourced their customer service to overseas call centers with just that outcome in mind. Workers in overseas call centers are given a very narrow slice of authority to solve your problem, and are also punished if they solve too many problems or pass too many callers onto a higher tier of support that can solve the problem. They aren't there to solve the problem – they're there to take the blame for the problem. They're "accountability sinks":

https://en.wikipedia.org/wiki/The_Unaccountability_Machine

It's worse than that, though. Call centers cheap out on long distance service, trading off call quality and reliability to save a few pennies. The fact that you can't hear the person on the other end of the line clearly, and that your call is randomly disconnected, sending you to the back of the hold queue? That's a feature, not a bug.

In a recent article for The Atlantic about his year-long quest to get Ford to honor its warranty on his brand-new car, Chris Colin describes the suite of tactics that companies engage in to exhaust your patience so that you just go away and stop trying to get your refund, warranty exchange or credit, branding them "sludge":

https://www.theatlantic.com/ideas/archive/2025/06/customer-service-sludge/683340/

Colin explores the historical antecedants for this malicious, sludgy compliance, including (hilariously) the notorious Simple Sabotage Field Manual, a US military guide designed for citizens in Nazi-occupied territories, detailing ways that they can seem to do their jobs while actually slowing everything down and ensuring nothing gets done:

https://www.cia.gov/static/5c875f3ec660e092cf893f60b4a288df/SimpleSabotage.pdf

In an interview with the 99 Percent Invisible podcast's Roman Mars, Colin talks about the factors that emboldened companies to switch from these maddening, useless, frustrating outsource call centers to chatbots:

https://99percentinvisible.org/episode/644-your-call-is-important-to-us/

Colin says that during the covid lockdowns, companies that had to shut down their call centers switched to chatbots of various types. After the lockdowns lifted, companies surveyed their customers to see how they felt about this switch and received a resounding, unambiguous, FUCK THAT NOISE. Colin says that companies' response was, "What I hear you saying is that you hate this, but you'll tolerate it."

This is so clearly what has happened. No one likes to interact with a chatbot for customer service. I personally find it loathsome. I've had three notable recent experiences where I had to interact with a chatbot, and in two of them, the chatbot performed as a perfect accountability sink, a literal "Computer says no" machine. In the third case, the chatbot actually turned on its master.

The first case: I pre-booked a taxi for a bookstore event on my tour. 40 minutes before the car was due to arrive, I checked Google Maps' estimate of the drive time and saw that it had gone up by 45 minutes (Trump was visiting the city and they'd shut down many of the streets, creating a brutal gridlock). I hastily canceled the taxi and rebooked it for an immediate pickup, and I got an email telling me I was being charged a $10 cancellation fee, because I hadn't given an hour's notice of the change.

Naturally, the email came from a noreply@ address, but it had a customer service URL, which – after a multi-stage login that involved yet another email verification step – dumped me into a chatbot window. An instant after I sent my typed-out complaint, the chatbot replied that I had violated company policy and would therefore have to pay a $10 fine, and that was that. When I asked to be transferred to a human, the chatbot told me that wasn't possible.

So I logged into the app and used the customer support link there, and had the identical experience, only this time when I asked the chatbot to transfer me to a human, I was put in a hold queue. An hour later, I was still in it. I powered down my phone and went onstage and, well, that's $10 I won't see again. Score one for sludge. Score one for enshittification. All hail the accountability sink.

The second case: I'm on a book-tour and here's a thing they won't tell you about suitcases: they do not survive. I don't care if the case has a 10-year warranty, it will not survive more than 20-30 flights. The trick of the 10-year suitcase warranty is that 95% of the people who buy that suitcase take two or fewer flights per year, and if the suitcase disintegrates in nine years instead of a decade, most people won't even think to apply for a warranty replacement. They'll just write it off.

But if you're a very frequent flier – if you get on (at least) one plane every day for a month and check a bag every time☨, that bag will absolutely disintegrate within a couple months.

☨ If you fly that often, you get your bag-check for free. In my experience, I only have a delayed or lost bag every 18 months or so (add a tracker and you can double that interval) and the convenience of having all your stuff with you when you land is absolutely worth the inconvenience of waiting a day or two every couple years to be reunited with your bag.

My big Solgaard case has had its wheels replaced twice, and the current set are already shot. But then the interior and one hinge disintegrated, so I contacted the company for a warranty swap, hoping to pick it up on a 36-hour swing through LA between Miami and Lisbon. They sent me a Fedex tracking code and I added it to my daily-load tab-group so I could check in on the bag's progress:

https://pluralistic.net/2024/01/25/today-in-tabs/#unfucked-rota

After 5 days, it was clear that something was wrong: there was a Fedex waybill, but the replacement suitcase hadn't been handed over to the courier. I emailed the Solgaard customer service address and a cheerful AI informed me that there was sometimes a short delay between the parcel being handed to the courier and it showing up in the tracker, but they still anticipated delivering it the next day. I wrote back and pointed out that this bag hadn't been shipped yet, and it was 3,000 miles from me, so there was no way they were going to deliver it in less than 24h. This got me escalated to a human, who admitted that I was right and promised to "flag the order with the warehouse." I'm en route to Lisbon now, and I don't have my suitcase. Score two for sludge!

The third case: Our kid started university this year! As a graduation present, we sent her on a "voluntourism" trip over the summer, doing some semi-skilled labor at a turtle sanctuary in Southeast Asia. That's far from LA and it was the first time she'd gone such a long way on her own. Delays in the first leg of her trip – to Hong Kong – meant that she missed her connection, which, in turn, meant getting re-routed through Singapore, with the result that she arrived more than 14 hours later than originally planned.

We tried contacting the people who ran the project, but they were offline. Earlier, we'd been told that there was no way to directly message the in-country team who'd be picking up our kid, just a Whatsapp group for all the participants. It quickly became clear that there was no one monitoring this group. It was getting close to when our kid would touch down, and we were getting worried, so my wife tried the chatbot on the organization's website.

After sternly warning us that it was not allowed to give us the contact number for the in-country lead who would be picking up our daughter, it then cheerfully spat out that forbidden phone number. This was the easiest AI jailbreak in history. We literally just said, "Aw, c'mon, please?" and it gave us that private info. A couple text messages later, we had it all sorted out.

This is a very funny outcome: the support chatbot sucked, but in a way that turned out to be advantageous to us. It did that thing that outsource call centers were invented to prevent: it actually helped us.

But this one is clearly an outlier. It was a broken bot. I'm sure future iterations will be much more careful not to help…if they can help it.

(Image: Cryteria, CC BY 3.0, modified)


Hey look at this (permalink)



A shelf of leatherbound history books with a gilt-stamped series title, 'The World's Famous Events.'

Object permanence (permalink)

#20yrsago Sony will stop shipping infectious CDs — too little, too late https://www.vaildaily.com/news/sony-halts-production-of-music-cds-with-copy-protection-scheme/

#15yrsago We Won’t Fly: national aviation opt-out day in protest of TSA porno scanner/genital grope “security” https://web.archive.org/web/20101111201035/https://wewontfly.com/

#10yrsago Green tea doesn’t promote weight loss https://www.cochrane.org/evidence/CD008650_green-tea-weight-loss-and-weight-maintenance-overweight-or-obese-adults

#10yrsago The DoJ won’t let anyone in the Executive Branch read the CIA Torture Report https://www.techdirt.com/2015/11/11/doj-has-blocked-everyone-executive-branch-reading-senates-torture-report/

#10yrsago House GOP defends the right of racist car-dealers to overcharge people of color https://mathbabe.org/2015/11/11/republicans-would-let-car-dealers-continue-racist-practices-undeterred/

#10yrsago UK Snooper’s Charter “would put an invisible landmine under every security researcher” https://arstechnica.com/tech-policy/2015/11/the-snoopers-charter-would-devastate-computer-security-research-in-the-uk/

#5yrsago Interactive UK covid omnishambles explorer https://pluralistic.net/2020/11/11/omnishambles/#serco

#1yrago General Strike 2028 https://pluralistic.net/2024/11/11/rip-jane-mcalevey/#organize


Upcoming appearances (permalink)

A photo of me onstage, giving a speech, pounding the podium.



A screenshot of me at my desk, doing a livecast.

Recent appearances (permalink)



A grid of my books with Will Stahle covers..

Latest books (permalink)



A cardboard book box with the Macmillan logo.

Upcoming books (permalink)

  • "Unauthorized Bread": a middle-grades graphic novel adapted from my novella about refugees, toasters and DRM, FirstSecond, 2026

  • "Enshittification, Why Everything Suddenly Got Worse and What to Do About It" (the graphic novel), Firstsecond, 2026

  • "The Memex Method," Farrar, Straus, Giroux, 2026

  • "The Reverse-Centaur's Guide to AI," a short book about being a better AI critic, Farrar, Straus and Giroux, 2026



Colophon (permalink)

Today's top sources:

Currently writing:

  • "The Reverse Centaur's Guide to AI," a short book for Farrar, Straus and Giroux about being an effective AI critic. FIRST DRAFT COMPLETE AND SUBMITTED.

  • A Little Brother short story about DIY insulin PLANNING


This work – excluding any serialized fiction – is licensed under a Creative Commons Attribution 4.0 license. That means you can use it any way you like, including commercially, provided that you attribute it to me, Cory Doctorow, and include a link to pluralistic.net.

https://creativecommons.org/licenses/by/4.0/

Quotations and images are not included in this license; they are included either under a limitation or exception to copyright, or on the basis of a separate license. Please exercise caution.


How to get Pluralistic:

Blog (no ads, tracking, or data-collection):

Pluralistic.net

Newsletter (no ads, tracking, or data-collection):

https://pluralistic.net/plura-list

Mastodon (no ads, tracking, or data-collection):

https://mamot.fr/@pluralistic

Medium (no ads, paywalled):

https://doctorow.medium.com/

Twitter (mass-scale, unrestricted, third-party surveillance and advertising):

https://twitter.com/doctorow

Tumblr (mass-scale, unrestricted, third-party surveillance and advertising):

https://mostlysignssomeportents.tumblr.com/tagged/pluralistic

"When life gives you SARS, you make sarsaparilla" -Joey "Accordion Guy" DeVilla

READ CAREFULLY: By reading this, you agree, on behalf of your employer, to release me from all obligations and waivers arising from any and all NON-NEGOTIATED agreements, licenses, terms-of-service, shrinkwrap, clickwrap, browsewrap, confidentiality, non-disclosure, non-compete and acceptable use policies ("BOGUS AGREEMENTS") that I have entered into with your employer, its partners, licensors, agents and assigns, in perpetuity, without prejudice to my ongoing rights and privileges. You further represent that you have the authority to release me from any BOGUS AGREEMENTS on behalf of your employer.

ISSN: 3066-764X

Tue, 11 Nov 2025 10:45:03 +0000 Fullscreen Open in Tab
Pluralistic: Theodora Goss's 'Letters From an Imaginary Country' (11 Nov 2025)


Today's links



The Tachyon Books cover for Theodora Goss's 'Letters From an Imaginary Country.

Theodora Goss's 'Letters From an Imaginary Country' (permalink)

Theodora Goss's latest book of short stories is 'Letters From an Imaginary Country,' and it manages to be one of the most voraciously, delightfully readable books I've ever read and it's one of the most writerly books, too. What a treat!

https://tachyonpublications.com/product/letters-from-an-imaginary-country/

The brilliant writer and critic Jo Walton (who wrote the introduction to this book) coined an extremely useful term of art to describe something science fiction and fantasy writers do: "incluing":

Incluing is the process of scattering information seamlessly through the text, as opposed to stopping the story to impart the information.

https://web.archive.org/web/20111119145140/http:/papersky.livejournal.com/324603.html

You see, in science fiction and fantasy, everything is up for grabs – the dog snoozing by the hearth can be a robot, a hologram, a simulation, a changeling, a shapeshifter, a cursed knight, a god…or just a dog. We talk a lot about how genre writers invent a scenario ("worldbuilding"), but thinking up a cool imaginary milieu is much easier than gracefully imparting it.

Sure, you can just flat-out tell the reader what's going on, and there's times when that works well. Exposition gets a bad rap, mostly because it's really hard to do well, and when it's not done well, it's either incredibly dull, or incredibly cringe, or both at once:

https://maryrobinettekowal.com/journal/my-favorite-bit/my-favorite-bit-cory-doctorow-talks-about-the-bezzle/

That's where incluing comes in. By "scattering information seamlessly through the text," the writer plays a kind of intellectual game with the reader in which the reader is clued in by little droplets of scene-setting, pieces of a puzzle that the reader collects and assembles in their head even as they're sinking into the story's characters and their problems.

This is a very fun game at the best, but it's also work. It's one of the reasons that sf/f short story collections can feel difficult: every 10 or 20 pages, you're solving a new puzzle, just so you can understand the stakes and setting of the story. Sure, you're also getting a new (potentially) super-cool conceit every few pages, and ideally, the novelty, the intellectual challenge and the cognitive load of grasping the situation all balance out.

One way to reduce the cognitive load on the reader is to build a world that hews more closely to the mundane one in which we live, where the conceit is simpler and thus easier to convey. But another way to do it is to just be REALLY! FUCKING! GOOD! at incluing.

That's Goss: really fucking good at incluing. Goss spins extremely weird, delightful and fun scenarios in these stories and she slides you into them like they were a warm bath. Before you know it, you're up to your nostrils in story, the water filling your ears, and you don't even remember getting in the tub. They're that good.

Goss has got a pretty erudite and varied life-history to draw on here. She's a Harvard-trained lawyer who was born in Soviet Hungary, raised across Europe and the UK and now lives in the USA. She's got a PhD in English Lit specializing in gothic literature and monsters and was the research assistant on a definitive academic edition of Dracula. Unsurprisingly, she often writes herself into her stories as a character.

With all that erudition, you won't be surprised to learn that formally, structurally, these are very daring stories. They take many narrative forms – correspondence, academic articles, footnotes, diary entries. But not in a straightforward way – for example, the title story, "Letters From an Imaginary Country," consists entirely of letters to the protagonist, with no replies. The protagonist doesn't appear a single time in this story, You have to infer everything about her and what's happening to her by means of the incluing in these letters (some of which are written by people whom the protagonist believes to be fictional characters!).

All of this should add to that cognitive loading – it's flashy, it's writerly, it's clever as hell. But it doesn't! Somehow, Goss is setting up these incredibly imaginative MacGuffins, using these weird-ass narrative building blocks, and unless you deliberately stop yourself, pull your face out of the pages and think about how these stories are being told, you won't even notice. She's got you by the ankles, grasping ever so gently, and she's pulling you in a smooth glide into that warm bath.

It's incredible.

And that's just the style and structure of these stories. They're also incredibly imaginative and emotionally intelligent. Many of these stories are metatextual, intertextual remixes of popular literature – the first story in the collection, "The Mad Scientist's Daughter," is a sweet little tale of the daughters of Drs Frankenstein, Moreau, Jekyll, Hyde, Rappaccini, and Meyrink, all living together in a kind of gothic commune:

http://strangehorizons.com/wordpress/fiction/the-mad-scientists-daughter-part-1-of-2/

It's such a great premise, but it's also got all this gorgeous character-driven stuff going on in it, with an emotional payoff that's a proper gut-punch.

Other stories in the collection concern the field of "Imaginary Anthropology," a Borges-inspired riff on the idea that if you write a detailed enough backstory for an imaginary land, it can spring into existence. Again, it's a great gaff, but Goss attacks it in both serious mode with "Cimmeria: From the Journal of Imaginary Anthropology":

https://www.lightspeedmagazine.com/fiction/cimmeria-journal-imaginary-anthropology/

and as a more lighthearted romp with "Pellargonia: A Letter to the Journal of Imaginary Anthropology," which is extremely funny, as many of these stories are (and in truth, even the most serious ones have laugh-aloud moments in them).

Goss grounds much of this fiction in her experience as an immigrant to the USA and in her Hungarian heritage. Fans of Lisa Goldstein's Red Magician and Steven Brust's Vlad Taltos novels will find much to love in Goss's use of Hungarian folklore and mythos, and Brust fans will be especially pleased (and famished) by her incredibly evocative descriptions of Hungarian food.

This is a book bursting with monsters – the final novella, "The Secret Diary of Mina Harker," is a beautiful vampire tale that remixes Dracula to great effect – food, humor, subtle emotions and beautiful premises. It's got Oz, and Burroughs' Barsoom, and King Arthur, and so much else besides.

This was my introduction to Goss's work – I'd heard great things, but the TBR pile is always 50 times bigger than I can possibly tackle. I'm off to read a lot more of it.


Hey look at this (permalink)



A shelf of leatherbound history books with a gilt-stamped series title, 'The World's Famous Events.'

Object permanence (permalink)

#20yrsago Agent to the Stars: comic sf about an alien race’s Hollywood agent https://memex.craphound.com/2005/11/10/agent-to-the-stars-comic-sf-about-an-alien-races-hollywood-agent/

#15yrsago ACTA will force your ISP to censor your work if someone lodges an unsupported trademark claim https://www.itnews.com.au/news/acta-isps-could-be-liable-for-trademark-infringements-238141

#15yrsago Free Kinect drivers released; Adafruit pays $3k bounty to hacker, $2k more to EFF https://blog.adafruit.com/2010/11/10/we-have-a-winner-open-kinect-drivers-released-winner-will-use-3k-for-more-hacking-plus-an-additional-2k-goes-to-the-eff/

#15yrsago White paper on 3D printing and the law: the coming copyfight https://publicknowledge.org/it-will-be-awesome-if-they-dont-screw-it-up-3d-printing/

#15yrsago UK security chief upset at being asked to surrender banned items at Heathrow https://www.mirror.co.uk/news/uk-news/terror-chief-tries-to-board-plane-260577

#10yrsago All smart TVs are watching you back, but Vizio’s spyware never blinks https://www.propublica.org/article/own-a-vizio-smart-tv-its-watching-you

#10yrsago Gallery of the Soviet Union’s most desirable personal computers https://www.rbth.com/multimedia/pictures/2014/04/07/before_the_internet_top_11_soviet_pcs_35711

#10yrsago Future Forms: beautifully curated collection of space-age electronics https://www.future-forms.com/

#10yrsago The Four Horsemen of Gentrification: Brine, Snark, Brunch, Whole Foods https://www.mcsweeneys.net/articles/the-four-horsemen-of-gentrification

#10yrsago America’s airlines send planes to El Salvador, China for service by undertrained technicians https://www.vanityfair.com/news/2015/11/airplane-maintenance-disturbing-truth

#10yrsago Google releases critical AI program under a free/open license https://www.wired.com/2015/11/google-open-sources-its-artificial-intelligence-engine/

#10yrsago UK law will allow secret backdoor orders for software, imprison you for disclosing them https://arstechnica.com/tech-policy/2015/11/snoopers-charter-uk-govt-can-demand-backdoors-give-prison-sentences-for-disclosing-them/

#5yrsago Broadband wins the 2020 election https://pluralistic.net/2020/11/10/dark-matter/#munifiber

#5yrsago Rights of Nature and legal standing https://pluralistic.net/2020/11/10/dark-matter/#rights-of-nature

#5yrsago Microchip "dark matter" https://pluralistic.net/2020/11/10/dark-matter/#precursor


Upcoming appearances (permalink)

A photo of me onstage, giving a speech, pounding the podium.



A screenshot of me at my desk, doing a livecast.

Recent appearances (permalink)



A grid of my books with Will Stahle covers..

Latest books (permalink)



A cardboard book box with the Macmillan logo.

Upcoming books (permalink)

  • "Unauthorized Bread": a middle-grades graphic novel adapted from my novella about refugees, toasters and DRM, FirstSecond, 2026

  • "Enshittification, Why Everything Suddenly Got Worse and What to Do About It" (the graphic novel), Firstsecond, 2026

  • "The Memex Method," Farrar, Straus, Giroux, 2026

  • "The Reverse-Centaur's Guide to AI," a short book about being a better AI critic, Farrar, Straus and Giroux, 2026



Colophon (permalink)

Today's top sources:

Currently writing:

  • "The Reverse Centaur's Guide to AI," a short book for Farrar, Straus and Giroux about being an effective AI critic. FIRST DRAFT COMPLETE AND SUBMITTED.

  • A Little Brother short story about DIY insulin PLANNING


This work – excluding any serialized fiction – is licensed under a Creative Commons Attribution 4.0 license. That means you can use it any way you like, including commercially, provided that you attribute it to me, Cory Doctorow, and include a link to pluralistic.net.

https://creativecommons.org/licenses/by/4.0/

Quotations and images are not included in this license; they are included either under a limitation or exception to copyright, or on the basis of a separate license. Please exercise caution.


How to get Pluralistic:

Blog (no ads, tracking, or data-collection):

Pluralistic.net

Newsletter (no ads, tracking, or data-collection):

https://pluralistic.net/plura-list

Mastodon (no ads, tracking, or data-collection):

https://mamot.fr/@pluralistic

Medium (no ads, paywalled):

https://doctorow.medium.com/

Twitter (mass-scale, unrestricted, third-party surveillance and advertising):

https://twitter.com/doctorow

Tumblr (mass-scale, unrestricted, third-party surveillance and advertising):

https://mostlysignssomeportents.tumblr.com/tagged/pluralistic

"When life gives you SARS, you make sarsaparilla" -Joey "Accordion Guy" DeVilla

READ CAREFULLY: By reading this, you agree, on behalf of your employer, to release me from all obligations and waivers arising from any and all NON-NEGOTIATED agreements, licenses, terms-of-service, shrinkwrap, clickwrap, browsewrap, confidentiality, non-disclosure, non-compete and acceptable use policies ("BOGUS AGREEMENTS") that I have entered into with your employer, its partners, licensors, agents and assigns, in perpetuity, without prejudice to my ongoing rights and privileges. You further represent that you have the authority to release me from any BOGUS AGREEMENTS on behalf of your employer.

ISSN: 3066-764X

Today's links



The Tachyon Books cover for Theodora Goss's 'Letters From an Imaginary Country.

Theodora Goss's 'Letters From an Imaginary Country' (permalink)

Theodora Goss's latest book of short stories is 'Letters From an Imaginary Country,' and it manages to be one of the most voraciously, delightfully readable books I've ever read and it's one of the most writerly books, too. What a treat!

https://tachyonpublications.com/product/letters-from-an-imaginary-country/

The brilliant writer and critic Jo Walton (who wrote the introduction to this book) coined an extremely useful term of art to describe something science fiction and fantasy writers do: "incluing":

Incluing is the process of scattering information seamlessly through the text, as opposed to stopping the story to impart the information.

https://web.archive.org/web/20111119145140/http:/papersky.livejournal.com/324603.html

You see, in science fiction and fantasy, everything is up for grabs – the dog snoozing by the hearth can be a robot, a hologram, a simulation, a changeling, a shapeshifter, a cursed knight, a god…or just a dog. We talk a lot about how genre writers invent a scenario ("worldbuilding"), but thinking up a cool imaginary milieu is much easier than gracefully imparting it.

Sure, you can just flat-out tell the reader what's going on, and there's times when that works well. Exposition gets a bad rap, mostly because it's really hard to do well, and when it's not done well, it's either incredibly dull, or incredibly cringe, or both at once:

https://maryrobinettekowal.com/journal/my-favorite-bit/my-favorite-bit-cory-doctorow-talks-about-the-bezzle/

That's where incluing comes in. By "scattering information seamlessly through the text," the writer plays a kind of intellectual game with the reader in which the reader is clued in by little droplets of scene-setting, pieces of a puzzle that the reader collects assembles in their head even as they're sinking into the story's characters and their problems.

This is a very fun game at the best, but it's also work. It's one of the reasons that sf/f short story collections can feel difficult: every 10 or 20 pages, you're solving a new puzzle, just so you can understand the stakes and setting of the story. Sure, you're also getting a new (potentially) super-cool conceit every few pages, and ideally, the novelty, the intellectual challenge and the cognitive load of grasping the situation all balance out.

One way to reduce the cognitive load on the reader is to build a world that hews more closely to the mundane one in which we live, where the conceit is simpler and thus easier to convey. But another way to do it is to just be REALLY! FUCKING! GOOD! at incluing.

That's Goss: really fucking good at incluing. Goss spins extremely weird, delightful and fun scenarios in these stories and she slides you into them like they were a warm bath. Before you know it, you're up to your nostrils in story, the water filling your ears, and you don't even remember getting in the tub. They're that good.

Goss has got a pretty erudite and varied life-history to draw on here. She's a Harvard-trained lawyer who was born in Soviet Hungary, raised across Europe and the UK and now lives in the USA. She's got a PhD in English Lit specializing in gothic literature and monsters and was the research assistant on a definitive academic edition of Dracula. Unsurprisingly, she often writes herself into her stories as a character.

With all that erudition, you won't be surprised to learn that formally, structurally, these are very daring stories. They take many narrative forms – correspondence, academic articles, footnotes, diary entries. But not in a straightforward way – for example, the title story, "Letters From an Imaginary Country," consists entirely of letters to the protagonist, with no replies. The protagonist doesn't appear a single time in this story, You have to infer everything about her and what's happening to her by means of the incluing in these letters (some of which are written by people whom the protagonist believes to be fictional characters!).

All of this should add to that cognitive loading – it's flashy, it's writerly, it's clever as hell. But it doesn't! Somehow, Goss is setting up these incredibly imaginative McGuffins, using these weird-ass narrative building blocks, and unless you deliberately stop yourself, pull your face out of the pages and think about how these stories are being told, you won't even notice. She's got you by the ankles, grasping ever so gently, and she'll pulling you in a smooth glide into that warm bath.

It's incredible.

And that's just the style and structure of these stories. They're also incredibly imaginative and emotionally intelligent. Many of these stories are metatextual, intertextual remixes of popular literature – the first story in the collection, "The Mad Scientist's Daughter," is a sweet little tale of the daughters of Drs Frankenstein, Moreau, Jekyll, Hyde, Rappaccini, and Meyrink, all living together in a kind of gothic commune:

http://strangehorizons.com/wordpress/fiction/the-mad-scientists-daughter-part-1-of-2/

It's such a great premise, but it's also got all this gorgeous character-driven stuff going on in it, with an emotional payoff that's a proper gut-punch.

Other stories in the collection concern the field of "Imaginary Anthropology," a Borges-inspired riff on the idea that if you write a detailed enough backstory for an imaginary land, it can spring into existence. Again, it's a great gaff, but Goss attacks it in both serious mode with "Cimmeria: From the Journal of Imaginary Anthropology":

https://www.lightspeedmagazine.com/fiction/cimmeria-journal-imaginary-anthropology/

And as a more lighthearted romp with "Pellargonia: A Letter to the Journal of Imaginary Anthropology," which is extremely funny, as many of these stories are (and in truth, even the most serious ones have laugh-aloud moments in them).

Goss grounds much of this fiction in her experience as an immigrant to the USA and in her Hungarian heritage. Fans of Lisa Goldstein's Red Magician and Steven Brust's Vlad Taltos novels will find much to love in Goss's use of Hungarian folklore and mythos, and Brust fans will be especially pleased (and famished) by her incredibly evocative descriptions of Hungarian food.

This is a book bursting with monsters – the final novella, "The Secret Diary of Mina Harker," is a beautiful vampire tale that remixes Dracula to great effect – food, humor, subtle emotions and beautiful premises. It's got Oz, and Burroughs's Barsoom, and King Arthur, and so much else besides.

This was my introduction to Goss's work – I'd heard great things, but the TBR pile is always 50 times bigger than I can possibly tackle. I'm off to read a lot more of it.


Hey look at this (permalink)



A shelf of leatherbound history books with a gilt-stamped series title, 'The World's Famous Events.'

Object permanence (permalink)

#20yrsago Agent to the Stars: comic sf about an alien race’s Hollywood agent https://memex.craphound.com/2005/11/10/agent-to-the-stars-comic-sf-about-an-alien-races-hollywood-agent/

#15yrsago ACTA will force your ISP to censor your work if someone lodges an unsupported trademark claim https://www.itnews.com.au/news/acta-isps-could-be-liable-for-trademark-infringements-238141

#15yrsago Free Kinect drivers released; Adafruit pays $3k bounty to hacker, $2k more to EFF https://blog.adafruit.com/2010/11/10/we-have-a-winner-open-kinect-drivers-released-winner-will-use-3k-for-more-hacking-plus-an-additional-2k-goes-to-the-eff/

#15yrsago White paper on 3D printing and the law: the coming copyfight https://publicknowledge.org/it-will-be-awesome-if-they-dont-screw-it-up-3d-printing/

#15yrsago UK security chief upset at being asked to surrender banned items at Heathrow https://www.mirror.co.uk/news/uk-news/terror-chief-tries-to-board-plane-260577

#10yrsago All smart TVs are watching you back, but Vizio’s spyware never blinks https://www.propublica.org/article/own-a-vizio-smart-tv-its-watching-you

#10yrsago Gallery of the Soviet Union’s most desirable personal computers https://www.rbth.com/multimedia/pictures/2014/04/07/before_the_internet_top_11_soviet_pcs_35711

#10yrsago Future Forms: beautifully curated collection of space-age electronics https://www.future-forms.com/

#10yrsago The Four Horsemen of Gentrification: Brine, Snark, Brunch, Whole Foods https://www.mcsweeneys.net/articles/the-four-horsemen-of-gentrification

#10yrsago America’s airlines send planes to El Salvador, China for service by undertrained technicians https://www.vanityfair.com/news/2015/11/airplane-maintenance-disturbing-truth

#10yrsago Google releases critical AI program under a free/open license https://www.wired.com/2015/11/google-open-sources-its-artificial-intelligence-engine/

#10yrsago UK law will allow secret backdoor orders for software, imprison you for disclosing them https://arstechnica.com/tech-policy/2015/11/snoopers-charter-uk-govt-can-demand-backdoors-give-prison-sentences-for-disclosing-them/

#5yrsago Broadband wins the 2020 election https://pluralistic.net/2020/11/10/dark-matter/#munifiber

#5yrsago Rights of Nature and legal standing https://pluralistic.net/2020/11/10/dark-matter/#rights-of-nature

#5yrsago Microchip "dark matter" https://pluralistic.net/2020/11/10/dark-matter/#precursor


Upcoming appearances (permalink)

A photo of me onstage, giving a speech, pounding the podium.



A screenshot of me at my desk, doing a livecast.

Recent appearances (permalink)



A grid of my books with Will Stahle covers..

Latest books (permalink)



A cardboard book box with the Macmillan logo.

Upcoming books (permalink)

  • "Unauthorized Bread": a middle-grades graphic novel adapted from my novella about refugees, toasters and DRM, FirstSecond, 2026

  • "Enshittification, Why Everything Suddenly Got Worse and What to Do About It" (the graphic novel), Firstsecond, 2026

  • "The Memex Method," Farrar, Straus, Giroux, 2026

  • "The Reverse-Centaur's Guide to AI," a short book about being a better AI critic, Farrar, Straus and Giroux, 2026



Colophon (permalink)

Today's top sources:

Currently writing:

  • "The Reverse Centaur's Guide to AI," a short book for Farrar, Straus and Giroux about being an effective AI critic. FIRST DRAFT COMPLETE AND SUBMITTED.

  • A Little Brother short story about DIY insulin PLANNING


This work – excluding any serialized fiction – is licensed under a Creative Commons Attribution 4.0 license. That means you can use it any way you like, including commercially, provided that you attribute it to me, Cory Doctorow, and include a link to pluralistic.net.

https://creativecommons.org/licenses/by/4.0/

Quotations and images are not included in this license; they are included either under a limitation or exception to copyright, or on the basis of a separate license. Please exercise caution.


How to get Pluralistic:

Blog (no ads, tracking, or data-collection):

Pluralistic.net

Newsletter (no ads, tracking, or data-collection):

https://pluralistic.net/plura-list

Mastodon (no ads, tracking, or data-collection):

https://mamot.fr/@pluralistic

Medium (no ads, paywalled):

https://doctorow.medium.com/

Twitter (mass-scale, unrestricted, third-party surveillance and advertising):

https://twitter.com/doctorow

Tumblr (mass-scale, unrestricted, third-party surveillance and advertising):

https://mostlysignssomeportents.tumblr.com/tagged/pluralistic

"When life gives you SARS, you make sarsaparilla" -Joey "Accordion Guy" DeVilla

READ CAREFULLY: By reading this, you agree, on behalf of your employer, to release me from all obligations and waivers arising from any and all NON-NEGOTIATED agreements, licenses, terms-of-service, shrinkwrap, clickwrap, browsewrap, confidentiality, non-disclosure, non-compete and acceptable use policies ("BOGUS AGREEMENTS") that I have entered into with your employer, its partners, licensors, agents and assigns, in perpetuity, without prejudice to my ongoing rights and privileges. You further represent that you have the authority to release me from any BOGUS AGREEMENTS on behalf of your employer.

ISSN: 3066-764X

2025-11-10T23:49:18+00:00 Fullscreen Open in Tab
Note published on November 10, 2025 at 11:49 PM UTC
2025-11-10T16:12:25+00:00 Fullscreen Open in Tab
Note published on November 10, 2025 at 4:12 PM UTC
Mon, 10 Nov 2025 12:15:28 +0000 Fullscreen Open in Tab
Pluralistic: "Flexible labor" is a euphemism for "derisking capital" (10 Nov 2025)


Today's links



Blind justice holding a set of scales aloft. Her head has been replaced with the hostile red eye of HAL 9000 from Stanley Kubrick's '2001: A Space Odyssey.' On the scales stand a worker and a millionaire, in belligerent postures. The millionaire's head has been replaced with the enshittification poop emoji, with angry eyes and a black, grawlix-scrawled bar over its mouth.

"Flexible labor" is a euphemism for "derisking capital" (permalink)

Corporations aren't people, but people and corporations do share some characteristics. Whether you're a human being or an immortal sinister colony organism that uses humans as gut flora (e.g. a corporation), most of us need to pay the rent and cover our other expenses.

"Earning a living" is a fact of life for humans and for corporations, and in both cases, the failure to do so can have dire consequences. For most humans, the path to earning a living is in selling your labor: that is, by finding a job, probably with a corporation. In taking that job, you assume some risk – for example, that your boss might be a jerk who makes your life a living hell, or that the company will go bust and leave you scrambling to make rent.

The corporation takes a risk, too: you might be an ineffectual or even counterproductive employee who fails to work its capital to produce a surplus from which a profit can be extracted. You might also fail to show up for work, or come in late, and lower the productivity of the firm (say, because another worker will have to cover for you and fall behind on their own work). You could even quit your job.

Both workers and corporations seek to "de-risk" their position. Workers can vote for politicians who will set minimum wages, punish unsafe working conditions and on-the-job harassment, and require health and disability insurance. They can also unionize and get some or all of these measures through collective bargaining (they might even get more protections, such as workplace tribunals to protect them from jobsite harassment). These are all examples of measures that shift risk from workers to capital. If a boss hires or promotes an abusive manager or cuts corners on shop-floor safety, the company – not the workers – will ultimately have to pay the price for its managers' poor judgment.

Bosses also strive to de-risk their position, by shifting the risk onto workers. For example, bosses love noncompete clauses in contracts, which let them harness the power of the government to punish their workers for changing jobs, and other bosses for hiring them. Given a tight noncompete, a boss can impose such high costs on workers who quit that they will elect to stay, even in the face of degraded working conditions, inadequate pay, and abusive management:

https://pluralistic.net/2022/02/02/its-the-economy-stupid/#neofeudal

If you have $250,000 worth of student debt and your boss has coerced you into signing a contract with a noncompete, that means that quitting your job will see you excluded for three years (or longer) from the field you paid all that money to get a degree in, but you will still be expected to pay your loans over that period. Missing the loan payments means sky-high penalties, which is how you get situations where you borrow $79k, pay back $190k, and still owe $236k:

https://pluralistic.net/2020/12/04/kawaski-trawick/#strike-debt

Bosses can also coerce workers into signing contracts with "training repayment agreement provisions" (TRAPs), which force workers to pay thousands of dollars for the privilege of quitting their job. Put this in stark economic terms: if your boss can fine you $5,000 for quitting your job, he can impose $4,999 worth of risk on you without risking your departure:

https://pluralistic.net/2022/08/04/its-a-trap/#a-little-on-the-nose

Bosses also enter into illegal, secret "no poach" agreements whereby they all agree not to hire one another's workers. One particularly pernicious version of this is the "bondage fee," where a staffing agency will demand that all its clients agree never to hire one of its contractors. In NYC, the majority of "doorman buildings" use a staffing agency called Planned Companies, a subsidiary of Toronto-based Firstservice, whose standard contract contains a bondage fee provision. The upshot is that pretty much every doorman building is legally on the hook for huge cash fines if they hire pretty much anyone who has worked as a doorman anywhere in the city:

https://pluralistic.net/2023/04/21/bondage-fees/#doorman-building

Again, this is a form of de-risking for capital. By creating barriers to workers quitting their jobs, bosses can reduce the risk that their workers will quit, even if the pay and working conditions are inadequate.

One of the most profound, effective and pervasive sites of de-risking is the gig economy, in which workers are not guaranteed any wages. By paying workers on a piecework basis – where you are only paid if a customer appears and consumes some of your labor – bosses can shift the risks associated with bad marketing, bad planning, and bad pricing onto their workers.

Think of an Uber driver: when an Uber driver clocks into the app, they make the whole system more valuable. Each additional Uber driver on the road shortens the average wait time for a taxi. What's more, Uber's algorithmic wage discrimination allows the company to pay lower wages when there are more workers available:

https://pluralistic.net/2023/04/12/algorithmic-wage-discrimination/#fishers-of-men

Lots of companies have hit on the strategy of increasing staffing levels in order to increase customer satisfaction. If you're a hardcore frequent flier, your chosen airline will give you a special number you can call to speak to a human in a matter of seconds, without ever being shunted to a chatbot. This is a gigantic perk – especially if you're flying at a time when air traffic controllers are quitting in droves because they haven't been paid in a month, and thousands of flights are being canceled, leaving travelers scrambling to get rebooked:

https://www.thedailybeast.com/air-traffic-controllers-start-resigning-as-shutdown-bites/

The airline that creates the secret, heavily staffed call center for its biggest customers is making a bet that those customers will spend enough money with the airline to cover the wage of those call-center employees. If the company bets wrong, it pays the penalty, taking a net loss on the call center.

But what if the airline could switch to a "gig economy" call center like Arise, a pyramid scheme that ropes in primarily Black women who have to pay for the privilege of answering phones, and pay for the privilege of quitting, but who can be fired at any time?

https://pluralistic.net/2020/10/02/chickenized-by-arise/#arise

Well, in that case the airline could tap an effectively limitless pool of call-center workers who could keep its best customers happy, but without taking the risk that the wages for those workers will exceed the new business brought in by those frequent fliers. Instead, that risk is borne by the workers, who have to pay for their own training, and whose pay can be doled out on a piecework basis, only paying them when someone calls in, but not paying them to simply be available in case someone calls in.

This isn't merely an employer de-risking its position: rather, the company is shifting its risk onto its workers. By deploying the legal fiction of worker misclassification in which an employee is classed as an "independent contractor," the boss can shift all the risk of misallocating labor onto workers.

In other words, risk-shifting isn't eliminating risk, it's just moving it around. Remember: both the corporation and the humans who work for it have to earn a living. They both need money for rent and other bills, and they both face dire consequences if they fail to pay those bills. When your boss misclassifies you as a contractor and only pays you when there's a customer demanding your labor, the boss is shifting the risk that they won't be able to pay the rent (because they hired too many workers or marketed their product badly) to you. If your boss screws up, they can still pay the rent – because you won't be able to pay yours.

That's what bosses mean by a "flexible workforce": a workforce that can coerced into assuming risk that properly belongs to its employers. After all, if you get into your car and clock onto the Uber app and fail to get a fare, whose fault is that? Uber bosses have all kinds of levers they can pull to increase ridership: they can reduce fares, they can advertise, they can even ping Uber riders directly through the app. What can an Uber driver do to increase the likelihood that they will get a fare? Absolutely, positively nothing. But who assumes the risk if a driver cruises the streets for hours, burning gas, not earning elsewhere, and not making a dime? The driver.

Uber alone determines the conditions for drivers, including how many drivers they will allow to be on the streets at the same time. Uber alone has the aggregated statistics with which to estimate likely ridership. Uber alone has the ability to entice more riders to hail cars. And yet it is Uber drivers who bear the responsibility if Uber fucks any of this up, and Uber does fuck this up, so badly that the true average driver wage (that is, the wage for hours in the car, not just when there's a passenger in there with you) is $2.50/hour:

https://pluralistic.net/2024/02/29/geometry-hates-uber/#toronto-the-gullible

This is what it means to shift risk. Uber doesn't have to be disciplined about its fares or its staffing levels or its marketing, because its workers can be made to pay the penalties for its mistakes. It's like this throughout the gig economy: the rise and rise of a massive "flexible workforce" is actually the rise and rise of a system in which labor assumes capital's risk.

Capital's story about a "flexible workforce" is that the risk is somehow magicked away when you can reclassify a worker as a contractor, but that's not true. A business that can only secure its sustained operations by shifting risk to its workers is a corporation that only exists because the workers who produce its profits assume the risks for its managers' blunders.

(Image: Cryteria, CC BY 3.0, modified)


Hey look at this (permalink)



A shelf of leatherbound history books with a gilt-stamped series title, 'The World's Famous Events.'

Object permanence (permalink)

#20yrsago Where do trolls come from? https://web.archive.org/web/20051124144047/http://www.barbelith.com/topic/22769

#20yrsago Lists of corrupted CDs https://web.archive.org/web/20051104092309/http://ukcdr.org/issues/cd/bad/

#20yrsago Wanna sue the pants off Sony? https://web.archive.org/web/20051113134057/https://www.eff.org/deeplinks/archives/004149.php

#20yrsago Katamari sushi https://www.flickr.com/photos/59199828@N00/61624921/

#20yrsago Sony’s EULA is worse than their rootkit https://web.archive.org/web/20051113134044/https://www.eff.org/deeplinks/archives/004145.php

#20yrsago List of CDs infected with Sony’s rootkit DRM https://web.archive.org/web/20051113134049/https://www.eff.org/deeplinks/archives/004144.php

#15yrsago TSA: checkpoint groping doesn’t exist https://web.archive.org/web/20101112010440/http://blog.tsa.gov/2010/11/white-house-blog-backscatter-back-story.html?showComment=1289329969182#c8438617926094279566

#15yrsago RIP, Robbins Barstow, godfather of the home movie revival https://amateurism.wordpress.com/2010/11/09/robbins-barstow-1919-2010/

#15yrsago Math papers with complicated, humbling titles https://web.archive.org/web/20101113223106/http://www.daddymodern.com/top-five-utterly-incomprehensible-mathematics-titles-at-arxiv-org/

#15yrsago Shirky: Times paywall is pretty much like all the other paywalls http://shirky.com/weblog/the-times-paywall-and-newsletter-economics/

#10yrsago 10,000 wax cylinders digitized and free to download https://cylinders.library.ucsb.edu/index.php

#10yrsago The Economist’s anti-ad-blocking tool was hacked and infected readers’ computers https://www.theverge.com/2015/11/6/9681124/pagefair-economist-malware-ad-blocker

#10yrsago EU wants to require permission to make a link on the Web https://felixreda.eu/2015/11/ancillary-copyright-2-0-the-european-commission-is-preparing-a-frontal-attack-on-the-hyperlink/

#10yrsago Federal judge orders NSA to stop collecting and searching plaintiffs’ phone records https://www.eff.org/deeplinks/2015/11/nsa-ordered-stop-collecting-querying-plaintiffs-phone-records

#10yrsago Here’s the kind of data the UK government will have about you, in realtime https://web.archive.org/web/20151112034545/https://icreacharound.xyz/

#10yrsago The CIA writes like Lovecraft, Bureau of Prisons is like Stephen King, & NSA is like… https://www.muckrock.com/news/archives/2015/nov/09/famous-writers-agency-foia-offices/

#10yrsago Unevenly distributed future: America’s online education systemhttps://medium.com/@cshirky/the-digital-revolution-in-higher-education-has-already-happened-no-one-noticed-78ec0fec16c7

#10yrsago Chelsea Manning’s statement for Aaron Swartz Day 2015 https://www.aaronswartzday.org/chelsea-manning-2015/

#10yrsago Unevenly distributed futures: Hong Kong’s amazing towershttps://www.peterstewartphotography.com/Portfolio/Stacked-Hong-Kong

#5yrsago UK corporate registrar bans code-injection https://pluralistic.net/2020/11/09/boundless-realm/#timmy-drop-tables

#5yrsago Student data breaches vastly underreported https://pluralistic.net/2020/11/09/boundless-realm/#leaky-edtech

#5yrsago Boundless Realms https://pluralistic.net/2020/11/09/boundless-realm/#fuxxfur


Upcoming appearances (permalink)

A photo of me onstage, giving a speech, pounding the podium.



A screenshot of me at my desk, doing a livecast.

Recent appearances (permalink)



A grid of my books with Will Stahle covers..

Latest books (permalink)



A cardboard book box with the Macmillan logo.

Upcoming books (permalink)

  • "Unauthorized Bread": a middle-grades graphic novel adapted from my novella about refugees, toasters and DRM, FirstSecond, 2026

  • "Enshittification, Why Everything Suddenly Got Worse and What to Do About It" (the graphic novel), Firstsecond, 2026

  • "The Memex Method," Farrar, Straus, Giroux, 2026

  • "The Reverse-Centaur's Guide to AI," a short book about being a better AI critic, Farrar, Straus and Giroux, 2026



Colophon (permalink)

Today's top sources:

Currently writing:

  • "The Reverse Centaur's Guide to AI," a short book for Farrar, Straus and Giroux about being an effective AI critic. FIRST DRAFT COMPLETE AND SUBMITTED.

  • A Little Brother short story about DIY insulin PLANNING


This work – excluding any serialized fiction – is licensed under a Creative Commons Attribution 4.0 license. That means you can use it any way you like, including commercially, provided that you attribute it to me, Cory Doctorow, and include a link to pluralistic.net.

https://creativecommons.org/licenses/by/4.0/

Quotations and images are not included in this license; they are included either under a limitation or exception to copyright, or on the basis of a separate license. Please exercise caution.


How to get Pluralistic:

Blog (no ads, tracking, or data-collection):

Pluralistic.net

Newsletter (no ads, tracking, or data-collection):

https://pluralistic.net/plura-list

Mastodon (no ads, tracking, or data-collection):

https://mamot.fr/@pluralistic

Medium (no ads, paywalled):

https://doctorow.medium.com/

Twitter (mass-scale, unrestricted, third-party surveillance and advertising):

https://twitter.com/doctorow

Tumblr (mass-scale, unrestricted, third-party surveillance and advertising):

https://mostlysignssomeportents.tumblr.com/tagged/pluralistic

"When life gives you SARS, you make sarsaparilla" -Joey "Accordion Guy" DeVilla

READ CAREFULLY: By reading this, you agree, on behalf of your employer, to release me from all obligations and waivers arising from any and all NON-NEGOTIATED agreements, licenses, terms-of-service, shrinkwrap, clickwrap, browsewrap, confidentiality, non-disclosure, non-compete and acceptable use policies ("BOGUS AGREEMENTS") that I have entered into with your employer, its partners, licensors, agents and assigns, in perpetuity, without prejudice to my ongoing rights and privileges. You further represent that you have the authority to release me from any BOGUS AGREEMENTS on behalf of your employer.

ISSN: 3066-764X

Sat, 08 Nov 2025 15:36:19 +0000 Fullscreen Open in Tab
Pluralistic: Facebook's fraud files (08 Nov 2025)


Today's links



A tuxedoed figure dramatically shoveing greenish pigs into a tube, from whose other end vomits forth a torrent of packaged goods. He has the head of Mark Zuckerberg's 'metaverse' avatar. He stands upon an endless field of gold coins. The background is the intaglioed upper face of the engraving of Benjamin Franklin on a US$100 bill, roughed up to a dark and sinister hue.

Facebook's fraud files (permalink)

A blockbuster Reuters report by Jeff Horwitz analyzes leaked internal documents that reveal that: 10% of Meta's gross revenue comes from ads for fraudulent goods and scams, and; the company knows it, and; they decided not to do anything about it, because; the fines for facilitating this life-destroying fraud are far less than the expected revenue from helping to destroy its users' lives:

https://www.reuters.com/investigations/meta-is-earning-fortune-deluge-fraudulent-ads-documents-show-2025-11-06/

The crux of the enshittification hypothesis is that companies deliberately degrade their products and services to benefit themselves at your expense because they can. An enshittogenic policy environment that rewards cheating, spying and monopolization will inevitably give rise to cheating, spying monopolists:

https://pluralistic.net/2025/09/10/say-their-names/#object-permanence

You couldn't ask for a better example than Reuters' Facebook Fraud Files. The topline description hardly does this scandal justice. Meta's depravity and greed in the face of truly horrifying fraud and scams on its platform is breathtaking.

Here's some details: first, the company's own figures estimate that they are delivering 15 billion scam ads every single day, which generate $7 billion in revenue every year. Despite its own automatic systems flagging the advertisers behind these scams, Meta does not terminate their account – rather, it charges them more money as a "disincentive." In other words, fraudulent ads are more profitable for Meta than non-scam ads.

Meta's own internal memos also acknowledge that they help scammers automatically target their most vulnerable users: if a user clicks on a scam, the automated ad-targeting system floods that user's feed with more scams. The company knows that the global fraud economy is totally dependent on Meta, with one third of all US scams going through Facebook (in the UK, the figure is 54% of all "payment-related scam losses"). Meta also concludes that it is uniquely hospitable to scammers, with one internal 2025 memo revealing the company's conclusion that "It is easier to advertise scams on Meta platforms than Google."

Internally, Meta has made plans to reduce the fraud on the platform, but the effort is being slow-walked because the company estimates that the most it will ultimately pay in fines worldwide ads up to $1 billion, while it currently books $7 billion/year in revenue from fraud. The memo announcing the anti-fraud effort concludes that scam revenue dwarfs "the cost of any regulatory settlement involving scam ads." Another memo concludes that the company will not take any pro-active measures to fight fraud, and will only fight fraud in response to regulatory action.

Meta's anti-fraud team operates under an internal quota system that limits how many scam ads they are allowed to fight. A Feb 2025 memo states that the anti-fraud team is only allowed to take measures that will reduce ad revenue by 0.15% ($135m) – even though Meta's own estimate is that scam ads generate $7 billion per year for the company. The manager in charge of the program warns their underlings that "We have specific revenue guardrails."

What does Meta fraud look like? One example cited by Reuters is the company's discovery of a "six-figure network of accounts" that impersonated US military personnel, who attempted to trick other Meta users sending them money. Reuters also describes "a torrent of fake accounts pretending to be celebrities or represent major consumer brands" in order to steal Meta users' money.

Another common form of fraud is "sextortion" scams. That's when someone acquires your nude images and threatens to publish them unless you pay them money and/or perform more sexual acts on camera for them. These scams disproportionately target teenagers and have led to children committing suicide:

https://www.usatoday.com/story/life/health-wellness/2025/02/25/teenage-boys-mental-health-suicide-sextortion-scams/78258882007/

In 2022, a Meta manager sent a memo complaining about a "lack of investment" in fraud-fighting systems. The company had classed this kind of fraud as a "low severity" problem and was deliberately starving enforcement efforts of resources.

This only got worse in the years that followed, when Meta engaged in mass layoffs from the anti-fraud side of the business in order to free up capital to work on perpetrating a different kind of scam – the mass investor frauds of metaverse and AI:

https://pluralistic.net/2025/05/07/rah-rah-rasputin/#credulous-dolts

These layoffs sometimes led to whole departments being shuttered. For example, in 2023, the entire team that handled "advertiser concerns about brand-rights issues" was fired. Meanwhile, Meta's metaverse and AI divisions were given priority over the company's resources, to the extent that safety teams were ordered to stop making any demanding use of company infrastructure, ordered instead to operate so minimally that they were merely "keeping the lights on."

Those safety teams, meanwhile, were receiving about 10,000 valid fraud reports from users every week, but were – by their own reckoning – ignoring or incorrectly rejecting 96% of them. The company responded to this revelation by vowing, in 2023, to reduce the share of valid fraud reports that it ignored to a mere 75%.

When Meta roundfiles and wontfixes valid fraud reports, Meta users lose everything. Reuters reports out the case of a Canadian air force recruiter whose account was taken over by fraudsters. Despite the victim repeatedly reporting the account takeover to Meta, the company didn't act on any of these reports. The scammers who controlled the account started to impersonate the victim to her trusted contacts, shilling crypto scams, claiming that she had bought land for a dream home with her crypto gains.

While Meta did nothing, the victim's friends lost everything. One colleague, Mike Lavery, was taken for CAD40,000 by the scammers. He told Reuters, "I thought I was talking to a trusted friend who has a really good reputation. Because of that, my guard was down." Four other colleagues were also scammed.

The person whose account had been stolen begged her friends to report the fraud to Meta. They sent hundreds of reports to the company, which ignored them all – even the ones she got the Royal Canadian Mounted Police to deliver to Meta's Canadian anti-fraud contact.

Meta calls this kind of scam, where scammers impersonate users, "organic," differentiating it from scam ads, where scammers pay to reach potential victims. Meta estimates that it hosts 22 billion "organic" scam pitches per day. These organic scams are actually often permitted by Meta's terms of service: when Singapore police complained to Meta about 146 scam posts, the company concluded that only 23% of these scams violated their Terms of Service. The others were all allowed.

These permissible frauds included "too good to be true" come-ons for 80% discounts on leading fashion brands, offers for fake concert tickets, and fake job listings – all permitted under Meta's own policies. The internal memos seen by Reuters show Meta's anti-fraud staffers growing quite upset to realize that these scams were not banned on the platform, with one Meta employee writing, "Current policies would not flag this account!"

But even if a fraudster does violate Meta's terms of service, the company will not act. Per Meta's own policies, a "High Value Account" (one that spends a lot on fraudulent ads) has to accrue more than 500 "strikes" (adjudicated violations of Meta policies) before the company will take down the account.

Meta's safety staff grew so frustrated by the company's de facto partnership with the fraudsters that preyed on its users that they created a weekly "Scammiest Scammer" award, given to the advertiser that generated the most complaints that week. But this didn't actually spark action – Reuters found that 40% of Scammiest Scammers were still operating on the platform six months after being flagged as the company's most prolific fraudster.

This callous disregard for Meta's users isn't the result of a new, sadistic streak in the company's top management. As the whistleblower Sarah Wynn-Williams' memoir Careless People comprehensively demonstrates, the company has always been helmed by awful people who would happily subject you to grotesque torments to make a buck:

https://pluralistic.net/2025/04/23/zuckerstreisand/#zdgaf

The thing that's changed over time is whether they can make a buck by screwing you over. The company's own internal calculus reveals how this works: they make more money from fraud – $7 billion/year – than they will ever have to pay in fines for exposing you to fraud. A fine is a price, and the price is right (for fraud).

The company could reduce fraud, but it's expensive. To lower the amount of fraud, they must spend money on fraud-fighting employees who review automated and user-generated fraud flags, and accept losses from "false positives" – overblocking ads that look fraudulent, but aren't. Note that these two outcomes are inversely correlated: the more the company spends on human review, the fewer dolphins they'll catch in their tuna nets.

Committing more resources to fraud fighting isn't the same thing as vowing to remove all fraud from the platform. That's likely impossible, and trying to do so would involve invasively intervening in users' personal interactions. But it's not necessary for Meta to sit inside every conversation among friends, trying to decide whether one of them is scamming the others, for the company to investigate and act on user complaints. It's not necessary for Meta to invade your conversations for it to remove prolific and profitable fraudsters without waiting for them to rack up 500 policy violations.

And of course, there is one way that Meta could dramatically reduce fraud: eliminate its privacy-invasive ad-targeting system. The top of the Meta ad-funnel starts with the nonconsensual dossiers Meta has assembled on more than 4 billion people around the world. Scammers pay to access these dossiers, targeting their pitches to users who are most vulnerable.

This is an absolutely foreseeable outcome of deeply, repeatedly violating billions of peoples' human rights by spying on them. Gathering and selling access to all this surveillance data is like amassing a mountain of oily rags so large that you can make billions by processing them into low-grade fuel. This is only profitable if you can get someone else to pay for the inevitable fires:

https://locusmag.com/feature/cory-doctorow-zucks-empire-of-oily-rags/

That's what Meta is doing here: privatizing the gains to be had from spying on us, and socializing the losses we all experience from the inevitable fallout. They are only able to do this, though, because of supine regulators. Here in the USA, Congress hasn't delivered a new consumer privacy law since 1988, when they made it a crime for video-store clerks to disclose your VHS rentals:

https://pluralistic.net/2023/12/06/privacy-first/#but-not-just-privacy

Meta spies on us and then allows predators to use that surveillance to destroy our lives for the same reason that your dog licks its balls: because they can. They are engaged in conduct that is virtually guaranteed by the enshittogenic policy environment, which allows Meta to spy on us without limit and which fines them $1b for making $7b on our misery.

Mark Zuckerberg has always been an awful person, but – as Sarah Wynn-Williams demonstrates in her book – he was once careful, worried about the harms he would suffer if he harmed us. Once we took those consequences away, Zuck did exactly what his nature dictated he must: destroyed our lives to increase his own fortune.


Hey look at this (permalink)



A shelf of leatherbound history books with a gilt-stamped series title, 'The World's Famous Events.'

Object permanence (permalink)

#20yrsago Singapore’s stocking-foot executioner https://web.archive.org/web/20051029103210/http://www.news.com.au/story/0,10117,17057851-2,00.html

#20yrsago Cinemas as police-states: why box-office revenue is in decline? https://web.archive.org/web/20051107024915/https://www.politechbot.com/2005/11/04/how-the-mpaa/

#20yrsago Westchester Co’s clueless WiFi lawmakers demonstrate cluelessness http://www.psychicfriends.net/blog/archives/2005/11/06/idiot_politicians_in_my_neighborhood.html

#20yrsago Katamari Damacy homemade models http://www.harveycartel.org/mare/pics/katamari.html

#15yrsago Cut-up artist alphabetizes the newspaper https://web.archive.org/web/20101109012930/http://derrenbrown.co.uk/blog/2010/11/kim-rugg-london-artists-knife-skills-knack-precision/

#15yrsago Colorado DA drops felony hit-and-run charges against billion-dollar financier because of “serious job implications” https://web.archive.org/web/20101108122254/http://www.vaildaily.com/article/20101104/NEWS/101109939/1078&ParentProfile=1062

#10yrsago A Freedom of Information request for UK Home Secretary Theresa May’s metadata https://www.techdirt.com/2015/11/06/uk-home-secretary-says-dont-worry-about-collection-metadata-foia-request-made-her-metadata/

#10yrsago Religious children more punitive, less likely to display altruism https://www.theguardian.com/world/2015/nov/06/religious-children-less-altruistic-secular-kids-study

#10yrsago Once again, the SFPD blames a cyclist for his own death without any investigation https://sfist.com/2015/11/04/sfpd_once_again_blames_cyclist_for/

#10yrsago Paid Patriotism: Pentagon spent millions bribing sports teams to recognize military service https://www.huffpost.com/entry/defense-military-tributes-professional-sports_n_5639a04ce4b0411d306eda5e

#10yrsago Spy at will! FCC won’t force companies to honor Do Not Track https://arstechnica.com/information-technology/2015/11/fcc-wont-force-websites-to-honor-do-not-track-requests/

#10yrsago TPP will let banks write their own regulations and stick taxpayers with the bill https://theintercept.com/2015/11/06/ttp-trade-pact-would-give-wall-street-a-trump-card-to-block-regulations/

#10yrsago Typewriter portraiture, the strange story of 1920s ASCII art https://web.archive.org/web/20151108220746/https://pictorial.jezebel.com/the-typewriter-ascii-portraits-of-classic-hollywood-and-1738094492

#5yrsago QE, inflation, slave labor and a People's Bailout https://pluralistic.net/2020/11/07/obamas-third-term/#peoplesbailout

#1yrago Antiusurpation and the road to disenshittification https://pluralistic.net/2024/11/07/usurpers-helpmeets/#disreintermediation


Upcoming appearances (permalink)

A photo of me onstage, giving a speech, pounding the podium.



A screenshot of me at my desk, doing a livecast.

Recent appearances (permalink)



A grid of my books with Will Stahle covers..

Latest books (permalink)



A cardboard book box with the Macmillan logo.

Upcoming books (permalink)

  • "Unauthorized Bread": a middle-grades graphic novel adapted from my novella about refugees, toasters and DRM, FirstSecond, 2026

  • "Enshittification, Why Everything Suddenly Got Worse and What to Do About It" (the graphic novel), Firstsecond, 2026

  • "The Memex Method," Farrar, Straus, Giroux, 2026

  • "The Reverse-Centaur's Guide to AI," a short book about being a better AI critic, Farrar, Straus and Giroux, 2026



Colophon (permalink)

Today's top sources:

Currently writing:

  • "The Reverse Centaur's Guide to AI," a short book for Farrar, Straus and Giroux about being an effective AI critic. FIRST DRAFT COMPLETE AND SUBMITTED.

  • A Little Brother short story about DIY insulin PLANNING


This work – excluding any serialized fiction – is licensed under a Creative Commons Attribution 4.0 license. That means you can use it any way you like, including commercially, provided that you attribute it to me, Cory Doctorow, and include a link to pluralistic.net.

https://creativecommons.org/licenses/by/4.0/

Quotations and images are not included in this license; they are included either under a limitation or exception to copyright, or on the basis of a separate license. Please exercise caution.


How to get Pluralistic:

Blog (no ads, tracking, or data-collection):

Pluralistic.net

Newsletter (no ads, tracking, or data-collection):

https://pluralistic.net/plura-list

Mastodon (no ads, tracking, or data-collection):

https://mamot.fr/@pluralistic

Medium (no ads, paywalled):

https://doctorow.medium.com/

Twitter (mass-scale, unrestricted, third-party surveillance and advertising):

https://twitter.com/doctorow

Tumblr (mass-scale, unrestricted, third-party surveillance and advertising):

https://mostlysignssomeportents.tumblr.com/tagged/pluralistic

"When life gives you SARS, you make sarsaparilla" -Joey "Accordion Guy" DeVilla

READ CAREFULLY: By reading this, you agree, on behalf of your employer, to release me from all obligations and waivers arising from any and all NON-NEGOTIATED agreements, licenses, terms-of-service, shrinkwrap, clickwrap, browsewrap, confidentiality, non-disclosure, non-compete and acceptable use policies ("BOGUS AGREEMENTS") that I have entered into with your employer, its partners, licensors, agents and assigns, in perpetuity, without prejudice to my ongoing rights and privileges. You further represent that you have the authority to release me from any BOGUS AGREEMENTS on behalf of your employer.

ISSN: 3066-764X

2025-11-07T23:50:21+00:00 Fullscreen Open in Tab
Note published on November 7, 2025 at 11:50 PM UTC
2025-11-07T19:30:52+00:00 Fullscreen Open in Tab
Published on Citation Needed: "Issue 96 – Redefining solvency"
2025-11-07T17:35:59+00:00 Fullscreen Open in Tab
Note published on November 7, 2025 at 5:35 PM UTC
Fri, 07 Nov 2025 15:32:00 +0000 Fullscreen Open in Tab
Pluralistic: The enshittification of labor (07 Nov 2025)


Today's links



A Gilded Age editorial cartoon depicting a muscular worker and a corpulent millionaire squaring off for a fight; the millionaire's head has been replaced with the poop emoji from the cover of 'Enshittification,' its mouth covered in a grawlix-scrawled black bar.

The enshittification of labor (permalink)

While I formulated the idea of enshittification to refer to digital platforms and their specific technical characteristics, economics and history, I am very excited to see other theorists extend the idea of enshittification beyond tech and into wider policy realms.

There's an easy, loose way to do this, which is using "enshittification" to refer to "things generally getting worse." To be clear, I am fine with this:

https://pluralistic.net/2024/10/14/pearl-clutching/#this-toilet-has-no-central-nervous-system

But there's a much more exciting way to broaden "enshittification," which starts with the foundation of the theory: that the things we rely on go bad when the system stops punishing this kind of deliberate degradation and starts rewarding it. In other words, the foundation of enshittification is the enshittogenic policy environment:

https://pluralistic.net/2025/09/10/say-their-names/#object-permanence

That's where Pavlina Tcherneva comes in. Tcherneva is an economist whose work focuses on the power of a "job guarantee," which is exactly what it sounds like: a guarantee from the government to employ anyone who wants a job, by either finding or creating a job that a) suits that person's talents and abilities and b) does something useful and good. If this sounds like a crazy pipe-dream to you, let me remind you that America had a job guarantee and it was wildly successful, and created (among other things), the system of national parks, a true jewel in America's crown:

https://pluralistic.net/2020/10/23/foxconned/#ccc

Tcherneva's latest paper is "The Death of the Social Contract and the Enshittification of Jobs," in which she draws a series of careful and compelling parallels between my work on enshittification and today's employment crisis, showing how a job guarantee is the ultimate disenshittifier of work:

https://www.levyinstitute.org/wp-content/uploads/2025/11/wp_1100.pdf

Tcherneva starts by proposing a simplified model of enshittification, mapping my three stages onto three of her own:

  1. Bait: Lure in users with a great, often subsidized, service.

  2. Trap: Use that captive audience to attract businesses (sellers, creators, advertisers).

  3. Switch: Exploit those groups by degrading the experience for everyone to extract maximum profit.

How do these map onto the current labor market and economy? For Tcherneva, the "bait" stage was "welfare state capitalism," which was "shaped by post–Great Depression government reforms and lasted through the 70s." This was the era in which the chaos of the Great Depression gave rise to fiscal and monetary policy that promoted macroeconomic stability. It was the era of economic safety nets and mass-scale federal investment in American businesses, through the Reconstruction Finance Corporation, a federal entity that expanded into directly funding large companies during WWII. After the war, the US Treasury continued to play a direct role in finance, through procurement, infrastructure spending and provision of social services.

As Tcherneva writes, this is widely considered the "Golden Age" of the US economy, a period of sustained growth and rising standard of living (she also points out that these benefits were very unevenly distributed, thanks to compromises made with southern white nationalists that exempted farm labor, and a pervasive climate of misogyny that carved out home work).

The welfare state capitalism stage was celebrated not merely for the benefits that it brought, but also for the system it supplanted. Before welfare state capitalism, we had 19th century "banker capitalism," in which cartels and trusts controlled every aspect of our lives and gave rise to a string of spectacular economic bubbles and busts. Before that, we had the "industrial capitalism" of the Industrial Revolution, where large corporations seized power. Before that, it was "merchant capitalism," and before that, feudalism – where workers were bound to a lord's land, unable to escape the economic and geographic destiny assigned to them at birth.

So welfare state capitalism was a welcome evolution, at least for the workers who got to reap its benefits. But welfare state capitalism was short-lived. To understand what came next, Tcherneva cites Hyman Minsky (whose "theory of capitalist development" provides this epochal nomenclature for the various stages of capitalism over the centuries).

Minsky calls the capitalism that supplanted welfare state capitalism "money manager capitalism," the system that reigned from the Reagan revolution until the Great Financial Crisis of 2008. This was an era of "deregulation, eroding worker power, rapid increase in inequality, and a rise of the money manager class." It's the period of financialization, which favored the creation of gigantic conglomerates that wrapped banking services (loans, credit cards, etc) around their core offerings, from GE to Amazon.

Then came the crash of 2008, which gave us our current era, the era of "international money manager capitalism," which is the system in which gigantic, transnational funds capture our economy pumping and dumping a series of scammy bubbles, like crypto, metaverse, blockchain, and (of course) AI:

https://pluralistic.net/2025/09/27/econopocalypse/#subprime-intelligence

Welfare state capitalism was the "bait" stage of the enshittification of labor. Public subsidies and regulation produced an environment in which (many) workers were able to command a large share of the fruits of their labor, securing both a living wage and old-age surety. This was the era of the "family wage," in which a single earner could supply all the necessities of life to a family: an owner-occupied home, material sufficiency, and enough left over for vacations, Christmas presents and other trappings of "the good life."

During this stage, the "social contract" meant the government training a skilled workforce (through universal education) and public goods like roads and utilities. Companies got big contracts, but only if they accepted collective bargaining from their unions. Governments and corporations collaborated to secure a comfortable requirement for workers.

But this arrangement lacked staying power, thanks to a key omission in the social contract: the guarantee of a good job. Rather than continuing the job guarantee that brought America out of the Depression, all the post-New Deal order could offer the unemployed was unemployment insurance. This wasn't so important while America was booming and employers were begging for workers, but when growth slowed, the lack of a job guarantee suddenly became the most important fact of many workers' lives.

This was foreseen by the architects of the New Deal. FDR's "Second Bill of (Economic) Rights" would have guaranteed every American "national healthcare, paid vacation, and a guaranteed job":

https://en.wikipedia.org/wiki/Second_Bill_of_Rights

These guarantees were never realized, and for Tcherneva, this failure doomed welfare state capitalism. Unions were powerful during an era of tight labor markets and able to wring concessions out of capital, but once demand for workers ebbed (thanks to slowing growth and, later, offshoring), bosses could threaten workers with unemployment, breaking union power.

The social contract was bait, promising "economic security and decent jobs" through cooperation between the government, corporations and unions.

The switch came from Reagan, with mass-scale deregulation, a hack-and-slash approach to social spending, and the enshrining of a permanently unemployed reserve army of workers whose "job" was fighting inflation (by not having a job). Trump has continued this, with massive cuts to the federal workforce. Today, "job insecurity is not an unfortunate consequence of shifting economic winds, it is the objective of public policy."

For money manager capitalism, unemployment is a feature, not a bug – literally. Neoliberal economists invented something called the NAIRU ("non-accelerating inflation rate of unemployment"), which deliberately sets out to keep a certain percentage of workers in a state of unemployment, in order to fight inflation.

Here's how that works: if the economy is at full employment (meaning everyone who wants a job has one), and prices go up (say, because bosses decide to increase their rate of profit), then workers will demand and receive a pay-rise, because bosses can't afford to fire those "greedy" workers – there are no unemployed workers to replace them.

This means that if bosses want to maintain their rate of profit, they will have to raise prices again to pay those higher wages for their workers. But after that, workers' pay no longer goes as far as it used to, so workers demand another raise and then bosses have to hike prices again (if they are determined not to allow the decline of their own profits). This is called "the wage-price spiral" and it's what happens when bosses refuse to accept lower profits and workers have the power to demand that their wages get adjusted to keep up with prices.

Of course, this only makes sense if you think that bosses should be guaranteed their profits, even if that means that workers' real take-home pay (measured by purchasing power) declines. You aren't supposed to notice this, though. That's why neoliberal economists made it a sin to ask about "distributional effects" (that is, asking about how the pie gets divided) – you're only supposed to care about how big the pie gets:

https://pluralistic.net/2023/03/28/imagine-a-horse/#perfectly-spherical-cows-of-uniform-density-on-a-frictionless-plane

With the adoption of NAIRU, joblessness "was now officially sanctioned as necessary for the health of the economy." You could not survive unless you had a job, not everyone could have a job, and the jobs were under control of a financialized, concentrated corporate sector. Companies merged and competition disappeared. If you refused to knuckle under to the boss at your (formerly) good factory job, there wasn't another factory that would put you on the line. The alternative to those decaying industrial jobs were "unemployment and low-wage service sector work."

That's where the final phase of the enshittification of labor comes in: the "trap." For Tcherneva, the trap is "the brutal fact of necessity itself." You cannot survive without a roof over your head, without electricity, without food and without healthcare. As these are not provided by the state, the only way to procure them (apart from inherited wealth) is through work, and access to work is entirely in the hands of the private sector.

Once corporations capture control of housing (through corporate landlords), healthcare (though corporate takeover of hospitals, pharma, etc), and power (through privatization of utilities), they can squeeze the people who depend on these things, because there is no competitor. You can't opt out of shelter, food, electricity and healthcare – at least, not without substantial hardship.

In my own theory of enshittification, platforms hunt relentlessly for sources of lock-in (e.g., the high switching costs of losing your social media community or your platform data) and, having achieved it, squeeze users and businesses, secure in the knowledge that users can't readily leave for a better service. This is compounded by monopolization (which reduces the likelihood that a better service even exists) and regulatory capture (which gives companies a free hand to squeeze with). Once a company can squeeze you, it will.

Here, Tcherneva is translating this to macroeconomic phenomena: control over the labor market and capture of the necessaries of life allows companies to squeeze, and so they do. A company rips you off for the same reason your dog licks its balls: because it can.

Tcherneva describes the era of money manager capitalism as "the slow, grinding enshittification of daily life." It's an era of corporate landlords raising the rent and skimping on maintenance, while hitting tenants with endless junk fees. It's an era of corporate hospitals gouging you on bills, skimping on care, and screwing healthcare workers. It's an era of utilities capturing their public overseers and engaging in endless above-inflation price hikes:

https://pluralistic.net/2025/02/24/surfa/#mark-ellis

This is the "trap" of Tcherneva's labor enshittification, and it kicked off "a decades-long enshittification of working life." Enshittified labor is "low-wage jobs with unpredictable schedules and no benefits." Half of American workers earn less than $25/hour. The federal minimum wage is stuck at $7.25/hour. Half of all renters are rent-burdened and a third of homeowners are mortgage-burdened. A quarter of renters are severely rent-burdened, with more than half their pay going to rent.

Money manager capitalism's answer to this is…more finance. Credit cards, payday loans, home equity loans, student loans. All this credit isn't nearly sufficient to keep up with rising health, housing, and educational prices. This locks workers into "a lifetime of servicing debt, incurred to simulate a standard of living the social contract had once promised but their wages could no longer deliver."

To manage this impossible situation, money manager capitalism spun up huge "securitized" debt markets, the CDOs and ABSes that led to the Great Financial Crisis (today, international money manager capitalism is spinning up even more forms of securitized debts).

In my theory of enshittification, there are four forces that keep tech platforms from going bad: competition, regulation, a strong workforce and interoperability. For Tcherneva, these forces all map onto the rise and fall of the American standard of living.

Competition: Welfare state capitalism was born in a time of tight labor markets. Workers could walk out of a bad job and into a good one, forcing bosses to compete for workers (including by dealing fairly with unions). This was how we got the "good job," one with medical, retirement, training and health care benefits.

Regulation: The New Deal established the 40-hour week, minimum wages, overtime, and the right to unionize. As with tech regulation, this was backstopped by competition – the existence of a tight labor market meant that companies had to concede to regulation. As with tech regulation, the capture of the state meant the end of the benefits of regulation. With the rise of NAIRU, regulation was captured by bosses, with the government now guaranteeing a pool of unemployed workers who could be used to terrorize uppity employees into meek acceptance.

Interoperability: In tech enshittification, the ability to take your data, relationships and devices with you when you switch to a competitor means that the companies you do business with have to treat you well, or risk your departure. In labor enshittification, bosses use noncompetes, arbitration, trade secrecy, and nondisparagement to keep workers from walking across the street and into a better job. Some workers are even encumbered with "training repayment agreement provisions (TRAPs) that force them to pay thousands of dollars if they quit their jobs:

https://pluralistic.net/2022/08/04/its-a-trap/#a-little-on-the-nose

Worker power: In tech enshittification, tech workers – empowered by the historically tight tech labor market – were able to hold the line, refusing to enshittify the products they develop, blocking their bosses' enshittifying impulses up with the constant threat that they can walk out the door and get a job elsewhere. In labor enshittification, NAIRU, combined with corporate capture of the necessaries of life and the retreat of unionization, means that workers have very little power to demand a better situation, which means their bosses can worsen the products and services they provide to their shriveled hearts' content.

As with my theory of enshittification, the erosion of worker power is an accelerant for labor enshittification. Weaker competition for workers means weaker labor power, which means weaker power to force the government to regulate. This sets the stage for more consolidation, weaker workers, and more state capture. This is the completion of the bait-trap-switch of the postwar social contract.

For Tcherneva, this enshittification arises out of the failure to create a job guarantee as part of the New Deal. And yet, a job guarantee remains very popular today:

https://www.jobguarantee.org/resources/public-support/

How would a job guarantee disenshittify the labor market? The job guarantee means a "permanent, publicly provided employment opportunity to anyone ready and willing to work, it establishes an effective floor for the entire labor market."

Under a job guarantee, any private employer wishing to hire a worker will have to beat the job guarantee's wages and benefits. No warehouse or fast-food chain could offer "poverty wages, unpredictable hours, and a hostile environment." It's an incentive to the private sector to compete for labor by restoring the benefits that characterized America's "golden age."

What's more, a job guarantee is administrable. A job guarantee means that workers can always access a safe, good job, even if the state fails to adequately police private-sector employers and their wages and working conditions. A job guarantee does much of the heavy lifting of enforcing a whole suite of regulations: "minimum wage laws, overtime rules, safety standards—that are constantly subject to political attack, corporate lobbying, and enforcement challenges."

A job guarantee also restores interoperability to the labor market. Rather than getting trapped in a deskilled, low-waged gig job, those at the bottom of the labor market will always have access to a job that comes with training and skills development, without noncompetes and other gotchas that trap workers in shitty jobs. For workers this means "career advancement and mobility." For society, "it delivers a pipeline of trained personnel to tackle our most pressing challenges."

And best of all, a job guarantee restores worker power. The fact that you can always access a decent job at a socially inclusive wage means that you don't have to eat shit when it comes to negotiating for your housing, health care and education. You can tell payday lenders, for-profit scam colleges (like Trump University), and slumlords to go fuck themselves.

Tcherneva concludes by pointing out that, as with tech enshittification, labor enshittification "is a political choice, not an economic inevitability." Labor enshittification is the foreseeable outcome of specific policies undertaken in living memory by named individuals. As with tech enshittification, we are under no obligation to preserve those enshittificatory policies. We can replace them with better ones.

If you want to learn more about the job guarantee, you can read my review of her book on the subject:

https://pluralistic.net/2020/06/22/jobs-guarantee/#job-guarantee

And the interview I did with her about it for the LA Times:

https://www.latimes.com/entertainment-arts/books/story/2020-06-24/forget-ubi-says-an-economist-its-time-for-universal-basic-jobs

Tcherneva and I are appearing onstage together next week in Lisbon at Web Summit to discuss this further:

https://websummit.com/sessions/lis25/2a479f57-a938-485a-acae-713ea9529292/working-it-out-job-security-in-the-ai-era/

And I assume that the video will thereafter be posted to Websummit's Youtube channel:

https://www.youtube.com/@websummit


Hey look at this (permalink)



A shelf of leatherbound history books with a gilt-stamped series title, 'The World's Famous Events.'

Object permanence (permalink)

#20yrsago PATRIOT Act secret-superwarrants use is up 10,000 percent https://www.washingtonpost.com/wp-dyn/content/article/2005/11/05/AR2005110501366_pf.html

#10yrsago Protopiper: tape-gun-based 3D printer extrudes full-size furniture prototypes https://www.youtube.com/watch?v=beRA4sIjxa8

#10yrsago EFF on TPP: all our worst fears confirmed https://www.eff.org/deeplinks/2015/11/release-full-tpp-text-after-five-years-secrecy-confirms-threats-users-rights

#10yrsago TPP will ban rules that require source-code disclosure https://www.keionline.org/39045

#10yrsago Publicity Rights could give celebrities a veto over creative works https://www.eff.org/deeplinks/2015/11/eff-asks-supreme-court-apply-first-amendment-speech-about-celebrities-0

#10yrsago How TPP will clobber Canada’s municipal archives and galleries of historical city photos https://www.geekman.ca/single-post/2015/11/the-tpp-vs-municipal-archives.html

#5yrsago HP ends its customers' lives https://pluralistic.net/2020/11/06/horrible-products/#inkwars

#1yrago Every internet fight is a speech fight https://pluralistic.net/2024/11/06/brazilian-blowout/#sovereignty-sure-but-human-rights-even-moreso


Upcoming appearances (permalink)

A photo of me onstage, giving a speech, pounding the podium.



A screenshot of me at my desk, doing a livecast.

Recent appearances (permalink)



A grid of my books with Will Stahle covers..

Latest books (permalink)



A cardboard book box with the Macmillan logo.

Upcoming books (permalink)

  • "Unauthorized Bread": a middle-grades graphic novel adapted from my novella about refugees, toasters and DRM, FirstSecond, 2026

  • "Enshittification, Why Everything Suddenly Got Worse and What to Do About It" (the graphic novel), Firstsecond, 2026

  • "The Memex Method," Farrar, Straus, Giroux, 2026

  • "The Reverse-Centaur's Guide to AI," a short book about being a better AI critic, Farrar, Straus and Giroux, 2026



Colophon (permalink)

Today's top sources:

Currently writing:

  • "The Reverse Centaur's Guide to AI," a short book for Farrar, Straus and Giroux about being an effective AI critic. FIRST DRAFT COMPLETE AND SUBMITTED.

  • A Little Brother short story about DIY insulin PLANNING


This work – excluding any serialized fiction – is licensed under a Creative Commons Attribution 4.0 license. That means you can use it any way you like, including commercially, provided that you attribute it to me, Cory Doctorow, and include a link to pluralistic.net.

https://creativecommons.org/licenses/by/4.0/

Quotations and images are not included in this license; they are included either under a limitation or exception to copyright, or on the basis of a separate license. Please exercise caution.


How to get Pluralistic:

Blog (no ads, tracking, or data-collection):

Pluralistic.net

Newsletter (no ads, tracking, or data-collection):

https://pluralistic.net/plura-list

Mastodon (no ads, tracking, or data-collection):

https://mamot.fr/@pluralistic

Medium (no ads, paywalled):

https://doctorow.medium.com/

Twitter (mass-scale, unrestricted, third-party surveillance and advertising):

https://twitter.com/doctorow

Tumblr (mass-scale, unrestricted, third-party surveillance and advertising):

https://mostlysignssomeportents.tumblr.com/tagged/pluralistic

"When life gives you SARS, you make sarsaparilla" -Joey "Accordion Guy" DeVilla

READ CAREFULLY: By reading this, you agree, on behalf of your employer, to release me from all obligations and waivers arising from any and all NON-NEGOTIATED agreements, licenses, terms-of-service, shrinkwrap, clickwrap, browsewrap, confidentiality, non-disclosure, non-compete and acceptable use policies ("BOGUS AGREEMENTS") that I have entered into with your employer, its partners, licensors, agents and assigns, in perpetuity, without prejudice to my ongoing rights and privileges. You further represent that you have the authority to release me from any BOGUS AGREEMENTS on behalf of your employer.

ISSN: 3066-764X

2025-11-06T17:52:51+00:00 Fullscreen Open in Tab
Finished reading Mage Tank 2
Finished reading:
Cover image of Mage Tank 2
Mage Tank series, book 2.
Published . 694 pages.
Started ; completed November 5, 2025.
Illustration of Molly White sitting and typing on a laptop, on a purple background with 'Molly White' in white serif.
2025-11-03T22:52:08+00:00 Fullscreen Open in Tab
Published on Citation Needed: "Trump says he has “no idea” who he just pardoned"
2025-11-03T19:09:58+00:00 Fullscreen Open in Tab
Note published on November 3, 2025 at 7:09 PM UTC

The full CBS interview with Trump about the pardon of Binance's Changpeng Zhao is shocking. "Why did you pardon him?" "I have no idea who he is. I was told that he was a victim ... They sent him to jail and they really set him up. That's my opinion. I was told about it."

NORAH O'DONNELL: Looked at this, the government at the time said that C.Z. had caused

"I know nothing about it because I'm too busy." He talks about how his sons are in the crypto industry, and how his son and wife published bestselling books. "I'm proud of them for doing that. I'm focused on this."

NORAH O'DONNELL: The government had accused him of

"[You're] not concerned about the appearance of corruption with this?"

"I'd rather not have you ask the question."

NORAH O'DONNELL: So not concerned about the appearance of corruption with this?  PRESIDENT DONALD TRUMP: I can't say, because-- I can't say-- I'm not concerned. I don't-- I'd rather not have you ask the question. But I let you ask it. You just came to me and you said,
2025-11-02T19:41:06+00:00 Fullscreen Open in Tab
Reviewing the 13 books I read in September and October

Reviewing the 13 books I read in September and October


Missed my reading wrap-up for September and have been too busy to read as much as usual, so here’s a combined September/October wrap-up. Lots of litRPG, and James S. A. Corey’s Caliban’s War (The Expanse #2) was definitely a highlight!

@molly0xfff September and October reading wrap-up, reviewing the 13 books I read those months (no spoilers) #readingwrapup #octoberreadingwrapup #booktok #litrpg #bookrecommendations ♬ original sound - Molly White
Storygraph September 2025 wrap-up page. Books: 10; pages: 4,124; av. rating 3.94. Highest rated reads: Demon World Boba Shop Vol. 2 (4.5 stars), Discount Dan (4 stars), Demon World Boba Shop Vol. 4 (4 stars). Average book length: 400 pages; average time to finish: 4 days. 100% fiction. 60% digital, 40% audio.
Storygraph October 2025 wrap-up page. Books: 3; pages: 2,426; av. rating 4.0. Highest rated reads: Caliban's War (4.5 stars), Cul-de-sac Carnage (4 stars), Mage Tank (4 stars). Average book length: 653 pages; average time to finish: 11 days. 100% fiction. 67% digital, 33% audio.
September 2025 reads: The League of Frightened Men, Rex Stout (4 stars)
The Concrete Blonde, Michael Connelly (4 stars)
The Last Coyote, Michael Connelly (4 stars)
Trunk Music, Michael Connelly (4 stars)
The Rubber Band, Rex Stout (3 stars)
Angels Flight, Michael Connelly (4 stars)
Demon World Boba Shop #2, R.C. Joshua (4.5 stars)
Demon World Boba Shop #3, R.C. Joshua (4 stars)
Demon World Boba Shop #4, R.C. Joshua (4 stars)
Discount Dan, James A. Hunter (4 stars)
Caliban's War, James S. A. Corey (4.5 stars)
Cul-de-sac Carnage, James A. Hunter (4 stars)
Mage Tank, Cornman (3.5 stars)
2025-11-02T14:54:53+00:00 Fullscreen Open in Tab
Read "Some People Can't See Mental Images. The Consequences Are Profound"
Read:
Larissa MacFarquhar writes about the recent research into the neurodiverse syndromes known as aphantasia and hyperphantasia, their effects on our experience of trauma and memory, and the sense of identity that has grown up around them.
Illustration of Molly White sitting and typing on a laptop, on a purple background with 'Molly White' in white serif.
Illustration of Molly White sitting and typing on a laptop, on a purple background with 'Molly White' in white serif.
2025-10-30T15:12:32+00:00 Fullscreen Open in Tab
Note published on October 30, 2025 at 3:12 PM UTC

“I have a great and considerable fear that people will freeze to death in their homes this winter if we do not turn this around quickly.”

The federal government shutdown means heating aid will not be released Nov. 1, leading to stark worries from those who manage the program.
Illustration of Molly White sitting and typing on a laptop, on a purple background with 'Molly White' in white serif.
2025-10-29T16:03:25+00:00 Fullscreen Open in Tab
Note published on October 29, 2025 at 4:03 PM UTC
2025-10-28T21:58:35+00:00 Fullscreen Open in Tab
Published on Citation Needed: "Issue 95 – The pardon was the payoff"
2025-10-28T16:33:42+00:00 Fullscreen Open in Tab
Note published on October 28, 2025 at 4:33 PM UTC
2025-10-23T17:33:54+00:00 Fullscreen Open in Tab
Note published on October 23, 2025 at 5:33 PM UTC
2025-10-23T15:36:35+00:00 Fullscreen Open in Tab
Note published on October 23, 2025 at 3:36 PM UTC

Donald Trump has pardoned Binance founder Changpeng Zhao. Binance has been a major supporter of Trump's crypto projects, and Trump has already made millions after Binance accepted a $2 billion investment from an Emirati fund denominated in the Trump family's USD1 stablecoin.

WSJ headline: Trump Pardons Convicted Binance Founder

One of the people CZ hired to lobby for the pardon is  Teresa Goody Guillén, a lawyer who has simultaneously represented the Trump World Liberty Financial project. She's also lobbied on behalf of Binance on crypto-related topics.

2025-10-22T19:49:40+00:00 Fullscreen Open in Tab
Note published on October 22, 2025 at 7:49 PM UTC
2025-10-11T09:49:59-07:00 Fullscreen Open in Tab
Adding Support for BlueSky to IndieLogin.com

Today I just launched support for BlueSky as a new authentication option in IndieLogin.com!

IndieLogin.com is a developer service that allows users to log in to a website with their domain. It delegates the actual user authentication out to various external services, whether that is an IndieAuth server, GitHub, GitLab, Codeberg, or just an email confirmation code, and now also BlueSky.

This means if you have a custom domain as your BlueSky handle, you can now use it to log in to websites like indieweb.org directly!

bluesky login

Alternatively, you can add a link to your BlueSky handle from your website with a rel="me atproto" attribute, similar to how you would link to your GitHub profile from your website.

<a href="https://example.bsky.social" rel="me atproto">example.bsky.social</a>

Full setup instructions here

This is made possible thanks to BlueSky's support of the new OAuth Client ID Metadata Document specification, which was recently adopted by the OAuth Working Group. This means as the developer of the IndieLogin.com service, I didn't have to register for any BlueSky API keys in order to use the OAuth server! The IndieLogin.com website publishes its own metadata which the BlueSky OAuth server can use to fetch the metadata from. This is the same client metadata that an IndieAuth server will parse as well! Aren't standards fun!

The hardest part about the whole process was probably adding DPoP support. Actually creating the DPoP JWT wasn't that bad but the tricky part was handling the DPoP server nonces sent back. I do wish we had a better solution for that mechanism in DPoP, but I remember the reasoning for doing it this way and I guess we just have to live with it now.

This was a fun exercise in implementing a bunch of the specs I've been working on recently!

Here's the link to the full ATProto OAuth docs for reference.

2025-10-10T00:00:00+00:00 Fullscreen Open in Tab
Notes on switching to Helix from vim

Hello! Earlier this summer I was talking to a friend about how much I love using fish, and how I love that I don’t have to configure it. They said that they feel the same way about the helix text editor, and so I decided to give it a try.

I’ve been using it for 3 months now and here are a few notes.

why helix: language servers

I think what motivated me to try Helix is that I’ve been trying to get a working language server setup (so I can do things like “go to definition”) and getting a setup that feels good in Vim or Neovim just felt like too much work.

After using Vim/Neovim for 20 years, I’ve tried both “build my own custom configuration from scratch” and “use someone else’s pre-buld configuration system” and even though I love Vim I was excited about having things just work without having to work on my configuration at all.

Helix comes with built in language server support, and it feels nice to be able to do things like “rename this symbol” in any language.

the search is great

One of my favourite things about Helix is the search! If I’m searching all the files in my repository for a string, it lets me scroll through the potential matching files and see the full context of the match, like this:

For comparison, here’s what the vim ripgrep plugin I’ve been using looks like:

There’s no context for what else is around that line.

the quick reference is nice

One thing I like about Helix is that when I press g, I get a little help popup telling me places I can go. I really appreciate this because I don’t often use the “go to definition” or “go to reference” feature and I often forget the keyboard shortcut.

some vim -> helix translations

  • Helix doesn’t have marks like ma, 'a, instead I’ve been using Ctrl+O and Ctrl+I to go back (or forward) to the last cursor location
  • I think Helix does have macros, but I’ve been using multiple cursors in every case that I would have previously used a macro. I like multiple cursors a lot more than writing macros all the time. If I want to batch change something in the document, my workflow is to press % (to highlight everything), then s to select (with a regex) the things I want to change, then I can just edit all of them as needed.
  • Helix doesn’t have neovim-style tabs, instead it has a nice buffer switcher (<space>b) I can use to switch to the buffer I want. There’s a pull request here to implement neovim-style tabs. There’s also a setting bufferline="multiple" which can act a bit like tabs with gp, gn for prev/next “tab” and :bc to close a “tab”.

some helix annoyances

Here’s everything that’s annoyed me about Helix so far.

  • I like the way Helix’s :reflow works much less than how vim reflows text with gq. It doesn’t work as well with lists. (github issue)
  • If I’m making a Markdown list, pressing “enter” at the end of a list item won’t continue the list. There’s a partial workaround for bulleted lists but I don’t know one for numbered lists.
  • No persistent undo yet: in vim I could use an undofile so that I could undo changes even after quitting. Helix doesn’t have that feature yet. (github PR)
  • Helix doesn’t autoreload files after they change on disk, I have to run :reload-all (:ra<tab>) to manually reload them. Not a big deal.
  • Sometimes it crashes, maybe every week or so. I think it might be this issue.

The “markdown list” and reflowing issues come up a lot for me because I spend a lot of time editing Markdown lists, but I keep using Helix anyway so I guess they can’t be making me that mad.

switching was easier than I thought

I was worried that relearning 20 years of Vim muscle memory would be really hard.

It turned out to be easier than I expected, I started using Helix on a vacation for a little low-stakes coding project I was doing on the side and after a week or two it didn’t feel so disorienting anymore. I think it might be hard to switch back and forth between Vim and Helix, but I haven’t needed to use Vim recently so I don’t know if that’ll ever become an issue for me.

The first time I tried Helix I tried to force it to use keybindings that were more similar to Vim and that did not work for me. Just learning the “Helix way” was a lot easier.

There are still some things that throw me off: for example w in vim and w in Helix don’t have the same idea of what a “word” is (the Helix one includes the space after the word, the Vim one doesn’t).

using a terminal-based text editor

For many years I’d mostly been using a GUI version of vim/neovim, so switching to actually using an editor in the terminal was a bit of an adjustment.

I ended up deciding on:

  1. Every project gets its own terminal window, and all of the tabs in that window (mostly) have the same working directory
  2. I make my Helix tab the first tab in the terminal window

It works pretty well, I might actually like it better than my previous workflow.

my configuration

I appreciate that my configuration is really simple, compared to my neovim configuration which is hundreds of lines. It’s mostly just 4 keyboard shortcuts.

theme = "solarized_light"
[editor]
# Sync clipboard with system clipboard
default-yank-register = "+"

[keys.normal]
# I didn't like that Ctrl+C was the default "toggle comments" shortcut
"#" = "toggle_comments"

# I didn't feel like learning a different way
# to go to the beginning/end of a line so
# I remapped ^ and $
"^" = "goto_first_nonwhitespace"
"$" = "goto_line_end"

[keys.select]
"^" = "goto_first_nonwhitespace"
"$" = "goto_line_end"

[keys.normal.space]
# I write a lot of text so I need to constantly reflow,
# and missed vim's `gq` shortcut
l = ":reflow"

There’s a separate languages.toml configuration where I set some language preferences, like turning off autoformatting. For example, here’s my Python configuration:

[[language]]
name = "python"
formatter = { command = "black", args = ["--stdin-filename", "%{buffer_name}", "-"] }
language-servers = ["pyright"]
auto-format = false

we’ll see how it goes

Three months is not that long, and it’s possible that I’ll decide to go back to Vim at some point. For example, I wrote a post about switching to nix a while back but after maybe 8 months I switched back to Homebrew (though I’m still using NixOS to manage one little server, and I’m still satisfied with that).

2025-10-08T12:14:38-07:00 Fullscreen Open in Tab
Client ID Metadata Document Adopted by the OAuth Working Group

The IETF OAuth Working Group has adopted the Client ID Metadata Document specification!

This specification defines a mechanism through which an OAuth client can identify itself to authorization servers, without prior dynamic client registration or other existing registration.

Clients identify themselves with their own URL, and host their metadata (name, logo, redirect URL) in a JSON document at that URL. They then use that URL as the client_id to introduce themselves to an authorization server for the first time.

The mechanism of clients identifying themselves as a URL has been in use in IndieAuth for over a decade, and more recently has been adopted by BlueSky for their OAuth API. The recent surge in interest in MCP has further demonstrated the need for this to be a standardized mechanism, and was the main driver in the latest round of discussion for the document! This could replace Dynamic Client Registration in MCP, dramatically simplifying management of clients, as well as enabling servers to limit access to specific clients if they want.

The folks at Stytch put together a really nice explainer website about it too! cimd.dev

Thanks to everyone for your contributions and feedback so far! And thanks to my co-author Emilia Smith for her work on the document!

2025-10-04T07:32:57-07:00 Fullscreen Open in Tab
Meetable Release Notes - October 2025

I just released some updates for Meetable, my open source event listing website.

The major new feature is the ability to let users log in with a Discord account. A Meetable instance can be linked to a Discord server to enable any member of the server to log in to the site. You can also restrict who can log in based on Discord "roles", so you can limit who can edit events to only certain Discord members.

One of the first questions I get about Meetable is whether recurring events are supported. My answer has always been "no". In general, it's too easy for recurring events on community calendars go get stale. If an organizer forgets to cancel or just stops showing up, that isn't visible unless someone takes the time to clean up the recurrence. Instead, it's healthier to require each event be created manually. There is a "clone event" feature that makes it easy to copy all the details from a previous event to be able to quickly manually create these sorts of recurring events. In this update, I just added a feature to streamline this even further. The next recurrence is now predicted based on the past interval of the event.

For example, for a biweekly cadence, the following steps happen now:

  • You would create the first instance manually, say for October 1
  • You click "Clone Event" and change the date of the new event to October 15
  • Now when you click "Clone Event" on the October 15 event, it will pre-fill October 29 based on the fact that the October 15 event was created 2 weeks after the event it was cloned from

Currently this only works by counting days, so wouldn't work for things like "first Tuesday of the month" or "the 1st of the month", but I hope this saves some time in the future regardless. If "first Tuesday" or specific days of the month are an important use case for you, let me know and I can try to come up with a solution.

Minor changes/fixes below:

  • Added "Create New Event" to the "Add Event" dropdown menu because it wasn't obvious "Add Event" was clickable.
  • Meeting link no longer appears for cancelled events. (Actually the meeting link only appears for "confirmed" events.)
  • If you add a meeting link but don't set a timezone, a warning message appears on the event.
  • Added a setting to show a message when uploading a photo, you can use this to describe a photo license policy for example.
  • Added a "user profile" page, and if users are configured to fetch profile info from their website, a button to re-fetch the profile info will appear.
2025-08-06T17:00:00-07:00 Fullscreen Open in Tab
San Francisco Billboards - August 2025

Every time I take a Lyft from the San Francisco airport to downtown going up 101, I notice the billboards. The billboards on 101 are always such a good snapshot in time of the current peak of the Silicon Valley hype cycle. I've decided to capture photos of the billboards every time I am there, to see how this changes over time. 

Here's a photo dump from the 101 billboards from August 2025. The theme is clearly AI. Apologies for the slightly blurry photos, these were taken while driving 60mph down the highway, some of them at night.

2025-06-26T00:00:00+00:00 Fullscreen Open in Tab
New zine: The Secret Rules of the Terminal

Hello! After many months of writing deep dive blog posts about the terminal, on Tuesday I released a new zine called “The Secret Rules of the Terminal”!

You can get it for $12 here: https://wizardzines.com/zines/terminal, or get an 15-pack of all my zines here.

Here’s the cover:

the table of contents

Here’s the table of contents:

why the terminal?

I’ve been using the terminal every day for 20 years but even though I’m very confident in the terminal, I’ve always had a bit of an uneasy feeling about it. Usually things work fine, but sometimes something goes wrong and it just feels like investigating it is impossible, or at least like it would open up a huge can of worms.

So I started trying to write down a list of weird problems I’ve run into in terminal and I realized that the terminal has a lot of tiny inconsistencies like:

  • sometimes you can use the arrow keys to move around, but sometimes pressing the arrow keys just prints ^[[D
  • sometimes you can use the mouse to select text, but sometimes you can’t
  • sometimes your commands get saved to a history when you run them, and sometimes they don’t
  • some shells let you use the up arrow to see the previous command, and some don’t

If you use the terminal daily for 10 or 20 years, even if you don’t understand exactly why these things happen, you’ll probably build an intuition for them.

But having an intuition for them isn’t the same as understanding why they happen. When writing this zine I actually had to do a lot of work to figure out exactly what was happening in the terminal to be able to talk about how to reason about it.

the rules aren’t written down anywhere

It turns out that the “rules” for how the terminal works (how do you edit a command you type in? how do you quit a program? how do you fix your colours?) are extremely hard to fully understand, because “the terminal” is actually made of many different pieces of software (your terminal emulator, your operating system, your shell, the core utilities like grep, and every other random terminal program you’ve installed) which are written by different people with different ideas about how things should work.

So I wanted to write something that would explain:

  • how the 4 pieces of the terminal (your shell, terminal emulator, programs, and TTY driver) fit together to make everything work
  • some of the core conventions for how you can expect things in your terminal to work
  • lots of tips and tricks for how to use terminal programs

this zine explains the most useful parts of terminal internals

Terminal internals are a mess. A lot of it is just the way it is because someone made a decision in the 80s and now it’s impossible to change, and honestly I don’t think learning everything about terminal internals is worth it.

But some parts are not that hard to understand and can really make your experience in the terminal better, like:

  • if you understand what your shell is responsible for, you can configure your shell (or use a different one!) to access your history more easily, get great tab completion, and so much more
  • if you understand escape codes, it’s much less scary when cating a binary to stdout messes up your terminal, you can just type reset and move on
  • if you understand how colour works, you can get rid of bad colour contrast in your terminal so you can actually read the text

I learned a surprising amount writing this zine

When I wrote How Git Works, I thought I knew how Git worked, and I was right. But the terminal is different. Even though I feel totally confident in the terminal and even though I’ve used it every day for 20 years, I had a lot of misunderstandings about how the terminal works and (unless you’re the author of tmux or something) I think there’s a good chance you do too.

A few things I learned that are actually useful to me:

  • I understand the structure of the terminal better and so I feel more confident debugging weird terminal stuff that happens to me (I was even able to suggest a small improvement to fish!). Identifying exactly which piece of software is causing a weird thing to happen in my terminal still isn’t easy but I’m a lot better at it now.
  • you can write a shell script to copy to your clipboard over SSH
  • how reset works under the hood (it does the equivalent of stty sane; sleep 1; tput reset) – basically I learned that I don’t ever need to worry about remembering stty sane or tput reset and I can just run reset instead
  • how to look at the invisible escape codes that a program is printing out (run unbuffer program > out; less out)
  • why the builtin REPLs on my Mac like sqlite3 are so annoying to use (they use libedit instead of readline)

blog posts I wrote along the way

As usual these days I wrote a bunch of blog posts about various side quests:

people who helped with this zine

A long time ago I used to write zines mostly by myself but with every project I get more and more help. I met with Marie Claire LeBlanc Flanagan every weekday from September to June to work on this one.

The cover is by Vladimir Kašiković, Lesley Trites did copy editing, Simon Tatham (who wrote PuTTY) did technical review, our Operations Manager Lee did the transcription as well as a million other things, and Jesse Luehrs (who is one of the very few people I know who actually understands the terminal’s cursed inner workings) had so many incredibly helpful conversations with me about what is going on in the terminal.

get the zine

Here are some links to get the zine again:

As always, you can get either a PDF version to print at home or a print version shipped to your house. The only caveat is print orders will ship in August – I need to wait for orders to come in to get an idea of how many I should print before sending it to the printer.

2025-06-10T00:00:00+00:00 Fullscreen Open in Tab
Using `make` to compile C programs (for non-C-programmers)

I have never been a C programmer but every so often I need to compile a C/C++ program from source. This has been kind of a struggle for me: for a long time, my approach was basically “install the dependencies, run make, if it doesn’t work, either try to find a binary someone has compiled or give up”.

“Hope someone else has compiled it” worked pretty well when I was running Linux but since I’ve been using a Mac for the last couple of years I’ve been running into more situations where I have to actually compile programs myself.

So let’s talk about what you might have to do to compile a C program! I’ll use a couple of examples of specific C programs I’ve compiled and talk about a few things that can go wrong. Here are three programs we’ll be talking about compiling:

  • paperjam
  • sqlite
  • qf (a pager you can run to quickly open files from a search with rg -n THING | qf)

step 1: install a C compiler

This is pretty simple: on an Ubuntu system if I don’t already have a C compiler I’ll install one with:

sudo apt-get install build-essential

This installs gcc, g++, and make. The situation on a Mac is more confusing but it’s something like “install xcode command line tools”.

step 2: install the program’s dependencies

Unlike some newer programming languages, C doesn’t have a dependency manager. So if a program has any dependencies, you need to hunt them down yourself. Thankfully because of this, C programmers usually keep their dependencies very minimal and often the dependencies will be available in whatever package manager you’re using.

There’s almost always a section explaining how to get the dependencies in the README, for example in paperjam’s README, it says:

To compile PaperJam, you need the headers for the libqpdf and libpaper libraries (usually available as libqpdf-dev and libpaper-dev packages).

You may need a2x (found in AsciiDoc) for building manual pages.

So on a Debian-based system you can install the dependencies like this.

sudo apt install -y libqpdf-dev libpaper-dev

If a README gives a name for a package (like libqpdf-dev), I’d basically always assume that they mean “in a Debian-based Linux distro”: if you’re on a Mac brew install libqpdf-dev will not work. I still have not 100% gotten the hang of developing on a Mac yet so I don’t have many tips there yet. I guess in this case it would be brew install qpdf if you’re using Homebrew.

step 3: run ./configure (if needed)

Some C programs come with a Makefile and some instead come with a script called ./configure. For example, if you download sqlite’s source code, it has a ./configure script in it instead of a Makefile.

My understanding of this ./configure script is:

  1. You run it, it prints out a lot of somewhat inscrutable output, and then it either generates a Makefile or fails because you’re missing some dependency
  2. The ./configure script is part of a system called autotools that I have never needed to learn anything about beyond “run it to generate a Makefile”.

I think there might be some options you can pass to get the ./configure script to produce a different Makefile but I have never done that.

step 4: run make

The next step is to run make to try to build a program. Some notes about make:

  • Sometimes you can run make -j8 to parallelize the build and make it go faster
  • It usually prints out a million compiler warnings when compiling the program. I always just ignore them. I didn’t write the software! The compiler warnings are not my problem.

compiler errors are often dependency problems

Here’s an error I got while compiling paperjam on my Mac:

/opt/homebrew/Cellar/qpdf/12.0.0/include/qpdf/InputSource.hh:85:19: error: function definition does not declare parameters
   85 |     qpdf_offset_t last_offset{0};
      |                   ^

Over the years I’ve learned it’s usually best not to overthink problems like this: if it’s talking about qpdf, there’s a good change it just means that I’ve done something wrong with how I’m including the qpdf dependency.

Now let’s talk about some ways to get the qpdf dependency included in the right way.

the world’s shortest introduction to the compiler and linker

Before we talk about how to fix dependency problems: building C programs is split into 2 steps:

  1. Compiling the code into object files (with gcc or clang)
  2. Linking those object files into a final binary (with ld)

It’s important to know this when building a C program because sometimes you need to pass the right flags to the compiler and linker to tell them where to find the dependencies for the program you’re compiling.

make uses environment variables to configure the compiler and linker

If I run make on my Mac to install paperjam, I get this error:

c++ -o paperjam paperjam.o pdf-tools.o parse.o cmds.o pdf.o -lqpdf -lpaper
ld: library 'qpdf' not found

This is not because qpdf is not installed on my system (it actually is!). But the compiler and linker don’t know how to find the qpdf library. To fix this, we need to:

  • pass "-I/opt/homebrew/include" to the compiler (to tell it where to find the header files)
  • pass "-L/opt/homebrew/lib -liconv" to the linker (to tell it where to find library files and to link in iconv)

And we can get make to pass those extra parameters to the compiler and linker using environment variables! To see how this works: inside paperjam’s Makefile you can see a bunch of environment variables, like LDLIBS here:

paperjam: $(OBJS)
	$(LD) -o $@ $^ $(LDLIBS)

Everything you put into the LDLIBS environment variable gets passed to the linker (ld) as a command line argument.

secret environment variable: CPPFLAGS

Makefiles sometimes define their own environment variables that they pass to the compiler/linker, but make also has a bunch of “implicit” environment variables which it will automatically pass to the C compiler and linker. There’s a full list of implicit environment variables here, but one of them is CPPFLAGS, which gets automatically passed to the C compiler.

(technically it would be more normal to use CXXFLAGS for this, but this particular Makefile hardcodes CXXFLAGS so setting CPPFLAGS was the only way I could find to set the compiler flags without editing the Makefile)

As an aside: it took me a long time to realize how closely tied to C/C++ `make` is -- I used to think that `make` was just a general build system (and of course you can use it for anything!) but it has a lot of affordances for building C/C++ programs that it doesn't have for building any other kind of program.

two ways to pass environment variables to make

I learned thanks to @zwol that there are actually two ways to pass environment variables to make:

  1. CXXFLAGS=xyz make (the usual way)
  2. make CXXFLAGS=xyz

The difference between them is that make CXXFLAGS=xyz will override the value of CXXFLAGS set in the Makefile but CXXFLAGS=xyz make won’t.

I’m not sure which way is the norm but I’m going to use the first way in this post.

how to use CPPFLAGS and LDLIBS to fix this compiler error

Now that we’ve talked about how CPPFLAGS and LDLIBS get passed to the compiler and linker, here’s the final incantation that I used to get the program to build successfully!

CPPFLAGS="-I/opt/homebrew/include" LDLIBS="-L/opt/homebrew/lib -liconv" make paperjam

This passes -I/opt/homebrew/include to the compiler and -L/opt/homebrew/lib -liconv to the linker.

Also I don’t want to pretend that I “magically” knew that those were the right arguments to pass, figuring them out involved a bunch of confused Googling that I skipped over in this post. I will say that:

  • the -I compiler flag tells the compiler which directory to find header files in, like /opt/homebrew/include/qpdf/QPDF.hh
  • the -L linker flag tells the linker which directory to find libraries in, like /opt/homebrew/lib/libqpdf.a
  • the -l linker flag tells the linker which libraries to link in, like -liconv means “link in the iconv library”, or -lm means “link math

tip: how to just build 1 specific file: make $FILENAME

Yesterday I discovered this cool tool called qf which you can use to quickly open files from the output of ripgrep.

qf is in a big directory of various tools, but I only wanted to compile qf. So I just compiled qf, like this:

make qf

Basically if you know (or can guess) the output filename of the file you’re trying to build, you can tell make to just build that file by running make $FILENAME

tip: you don’t need a Makefile

I sometimes write 5-line C programs with no dependencies, and I just learned that if I have a file called blah.c, I can just compile it like this without creating a Makefile:

make blah

It gets automaticaly expanded to cc -o blah blah.c, which saves a bit of typing. I have no idea if I’m going to remember this (I might just keep typing gcc -o blah blah.c anyway) but it seems like a fun trick.

tip: look at how other packaging systems built the same C program

If you’re having trouble building a C program, maybe other people had problems building it too! Every Linux distribution has build files for every package that they build, so even if you can’t install packages from that distribution directly, maybe you can get tips from that Linux distro for how to build the package. Realizing this (thanks to my friend Dave) was a huge ah-ha moment for me.

For example, this line from the nix package for paperjam says:

  env.NIX_LDFLAGS = lib.optionalString stdenv.hostPlatform.isDarwin "-liconv";

This is basically saying “pass the linker flag -liconv to build this on a Mac”, so that’s a clue we could use to build it.

That same file also says env.NIX_CFLAGS_COMPILE = "-DPOINTERHOLDER_TRANSITION=1";. I’m not sure what this means, but when I try to build the paperjam package I do get an error about something called a PointerHolder, so I guess that’s somehow related to the “PointerHolder transition”.

step 5: installing the binary

Once you’ve managed to compile the program, probably you want to install it somewhere! Some Makefiles have an install target that let you install the tool on your system with make install. I’m always a bit scared of this (where is it going to put the files? what if I want to uninstall them later?), so if I’m compiling a pretty simple program I’ll often just manually copy the binary to install it instead, like this:

cp qf ~/bin

step 6: maybe make your own package!

Once I figured out how to do all of this, I realized that I could use my new make knowledge to contribute a paperjam package to Homebrew! Then I could just brew install paperjam on future systems.

The good thing is that even if the details of how all of the different packaging systems, they fundamentally all use C compilers and linkers.

it can be useful to understand a little about C even if you’re not a C programmer

I think all of this is an interesting example of how it can useful to understand some basics of how C programs work (like “they have header files”) even if you’re never planning to write a nontrivial C program if your life.

It feels good to have some ability to compile C/C++ programs myself, even though I’m still not totally confident about all of the compiler and linker flags and I still plan to never learn anything about how autotools works other than “you run ./configure to generate the Makefile”.

Two things I left out of this post:

  • LD_LIBRARY_PATH / DYLD_LIBRARY_PATH (which you use to tell the dynamic linker at runtime where to find dynamically linked files) because I can’t remember the last time I ran into an LD_LIBRARY_PATH issue and couldn’t find an example.
  • pkg-config, which I think is important but I don’t understand yet
2025-05-12T22:01:23-07:00 Fullscreen Open in Tab
Enterprise-Ready MCP

I've seen a lot of complaints about how MCP isn't ready for the enterprise.

I agree, although maybe not for the reasons you think. But don't worry, this isn't just a rant! I believe we can fix it!

The good news is the recent updates to the MCP authorization spec that separate out the role of the authorization server from the MCP server have now put the building blocks in place to make this a lot easier.

But let's back up and talk about what enterprise buyers expect when they are evaluating AI tools to bring into their companies.

Single Sign-On

At a minimum, an enterprise admin expects to be able to put an application under their single sign-on system. This enables the company to manage which users are allowed to use which applications, and prevents their users from needing to have their own passwords at the applications. The goal is to get every application managed under their single sign-on (SSO) system. Many large companies have more than 200 applications, so having them all managed through their SSO solution is a lot better than employees having to manage 200 passwords for each application!

There's a lot more than SSO too, like lifecycle management, entitlements, and logout. We're tackling these in the IPSIE working group in the OpenID Foundation. But for the purposes of this discussion, let's stick to the basics of SSO.

So what does this have to do with MCP?

An AI agent using MCP is just another application enterprises expect to be able to integrate into their single-sign-on (SSO) system. Let's take the example of Claude. When rolled out at a company, ideally every employee would log in to their company Claude account using the company identity provider (IdP). This lets the enterprise admin decide how many Claude licenses to purchase and who should be able to use it.

Connecting to External Apps

The next thing that should happen after a user logs in to Claude via SSO is they need to connect Claude to their other enterprise apps. This includes the built-in integrations in Claude like Google Calendar and Google Drive, as well as any MCP servers exposed by other apps in use within the enterprise. That could cover other SaaS apps like Zoom, Atlassian, and Slack, as well as home-grown internal apps.

Today, this process involves a somewhat cumbersome series of steps each individual employee must take. Here's an example of what the user needs to do to connect their AI agent to external apps:

First, the user logs in to Claude using SSO. This involves a redirect from Claude to the enterprise IdP where they authenticate with one or more factors, and then are redirected back.

SSO Log in to Claude

Next, they need to connect the external app from within Claude. Claude provides a button to initiate the connection. This takes the user to that app (in this example, Google), which redirects them to the IdP to authenticate again, eventually getting redirected back to the app where an OAuth consent prompt is displayed asking the user to approve access, and finally the user is redirected back to Claude and the connection is established.

Connect Google

The user has to repeat these steps for every MCP server that they want to connect to Claude. There are two main problems with this:

  • This user experience is not great. That's a lot of clicking that the user has to do.
  • The enterprise admin has no visibility or control over the connection established between the two applications.

Both of these are significant problems. If you have even just 10 MCP servers rolled out in the enterprise, you're asking users to click through 10 SSO and OAuth prompts to establish the connections, and it will only get worse as MCP is more widely adopted within apps. But also, should we really be asking the user if it's okay for Claude to access their data in Google Drive? In a company context, that's not actually the user's decision. That decision should be made by the enterprise IT admin.

In "An Open Letter to Third-party Suppliers", Patrick Opet, Chief Information Security Officer of JPMorgan Chase writes:

"Modern integration patterns, however, dismantle these essential boundaries, relying heavily on modern identity protocols (e.g., OAuth) to create direct, often unchecked interactions between third-party services and firms' sensitive internal resources."

Right now, these app-to-app connections are happening behind the back of the IdP. What we need is a way to move the connections between the applications into the IdP where they can be managed by the enterprise admin.

Let's see how this works if we leverage a new (in-progress) OAuth extension called "Identity and Authorization Chaining Across Domains", which I'll refer to as "Cross-App Access" for short, enabling the enterprise IdP to sit in the middle of the OAuth exchange between the two apps.

A Brief Intro to Cross-App Access

In this example, we'll use Claude as the application that is trying to connect to Slack's (hypothetical) MCP server. We'll start with a high-level overview of the flow, and later go over the detailed protocol.

First, the user logs in to Claude through the IdP as normal. This results in Claude getting either an ID token or SAML assertion from the IdP, which tells Claude who the user is. (This works the same for SAML assertions or ID tokens, so I'll use ID tokens in the example from here out.) This is no different than what the user would do today when signing in to Claude.

Step 1 and 2 SSO

Then, instead of prompting the user to connect Slack, Claude takes the ID token back to the IdP in a request that says "Claude is requesting access to this user's Slack account."

The IdP validates the ID token, sees it was issued to Claude, and verifies that the admin has allowed Claude to access Slack on behalf of the given user. Assuming everything checks out, the IdP issues a new token back to Claude.

Step 3 and 4 Cross-Domain Request

Claude takes the intermediate token from the IdP to Slack saying "hi, I would like an access token for the Slack MCP server. The IdP gave me this token with the details of the user to issue the access token for." Slack validates the token the same way it would have validated an ID token. (Remember, Slack is already configured for SSO to the IdP for this customer as well, so it already has a way to validate these tokens.) Slack is able to issue an access token giving Claude access to this user's resources in its MCP server.

Step 5-7 Access Token Request

This solves the two big problems:

  • The exchange happens entirely without any user interaction, so the user never sees any prompts or any OAuth consent screens.
  • Since the IdP sits in between the exchange, this gives the enterprise admin a chance to configure the policies around which applications are allowed this direct connection.

The other nice side effect of this is since there is no user interaction required, the first time a new user logs in to Claude, all their enterprise apps will be automatically connected without them having to click any buttons!

Cross-App Access Protocol

Now let's look at what this looks like in the actual protocol. This is based on the adopted in-progress OAuth specification "Identity and Authorization Chaining Across Domains". This spec is actually a combination of two RFCs: Token Exchange (RFC 8693), and JWT Profile for Authorization Grants (RFC 7523). Both RFCs as well as the "Identity and Authorization Chaining Across Domains" spec are very flexible. While this means it is possible to apply this to many different use cases, it does mean we need to be a bit more specific in how to use it for this use case. For that purpose, I've written a profile of the Identity Chaining draft called "Identity Assertion Authorization Grant" to fill in the missing pieces for the specific use case detailed here.

Let's go through it step by step. For this example we'll use the following entities:

  • Claude - the "Requesting Application", which is attempting to access Slack
  • Slack - the "Resource Application", which has the resources being accessed through MCP
  • Okta - the enterprise identity provider which users at the example company can use to sign in to both apps

Cross-App Access Diagram

Single Sign-On

First, Claude gets the user to sign in using a standard OpenID Connect (or SAML) flow in order to obtain an ID token. There isn't anything unique to this spec regarding this first stage, so I will skip the details of the OpenID Connect flow and we'll start with the ID token as the input to the next step.

Token Exchange

Claude, the requesting application, then makes a Token Exchange request (RFC 8693) to the IdP's token endpoint with the following parameters:

  • requested_token_type: The value urn:ietf:params:oauth:token-type:id-jag indicates that an ID Assertion JWT is being requested.
  • audience: The Issuer URL of the Resource Application's authorization server.
  • subject_token: The identity assertion (e.g. the OpenID Connect ID Token or SAML assertion) for the target end-user.
  • subject_token_type: Either urn:ietf:params:oauth:token-type:id_token or urn:ietf:params:oauth:token-type:saml2 as defined by RFC 8693.

This request will also include the client credentials that Claude would use in a traditional OAuth token request, which could be a client secret or a JWT Bearer Assertion.

POST /oauth2/token HTTP/1.1
Host: acme.okta.com
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&requested_token_type=urn:ietf:params:oauth:token-type:id-jag
&audience=https://auth.slack.com/
&subject_token=eyJraWQiOiJzMTZ0cVNtODhwREo4VGZCXzdrSEtQ...
&subject_token_type=urn:ietf:params:oauth:token-type:id_token
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=eyJhbGciOiJSUzI1NiIsImtpZCI6IjIyIn0...

ID Assertion Validation and Policy Evaluation

At this point, the IdP evaluates the request and decides whether to issue the requested "ID Assertion JWT". The request will be evaluated based on the validity of the arguments, as well as the configured policy by the customer.

For example, the IdP validates that the ID token in this request was issued to the same client that matches the provided client authentication. It evaluates that the user still exists and is active, and that the user is assigned the Resource Application. Other policies can be evaluated at the discretion of the IdP, just like it can during a single sign-on flow.

If the IdP agrees that the requesting app should be authorized to access the given user's data in the resource app's MCP server, it will respond with a Token Exchange response to issue the token:

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
  "issued_token_type": "urn:ietf:params:oauth:token-type:id-jag",
  "access_token": "eyJhbGciOiJIUzI1NiIsI...",
  "token_type": "N_A",
  "expires_in": 300
}

The claims in the issued JWT are defined in "Identity Assertion Authorization Grant". The JWT is signed using the same key that the IdP signs ID tokens with. This is a critical aspect that makes this work, since again we assumed that both apps would already be configured for SSO to the IdP so would already be aware of the signing key for that purpose.

At this point, Claude is ready to request a token for the Resource App's MCP server

Access Token Request

The JWT received in the previous request can now be used as a "JWT Authorization Grant" as described by RFC 7523. To do this, Claude makes a request to the MCP authorization server's token endpoint with the following parameters:

  • grant_type: urn:ietf:params:oauth:grant-type:jwt-bearer
  • assertion: The Identity Assertion Authorization Grant JWT obtained in the previous token exchange step

For example:

POST /oauth2/token HTTP/1.1
Host: auth.slack.com
Authorization: Basic yZS1yYW5kb20tc2VjcmV0v3JOkF0XG5Qx2

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
assertion=eyJhbGciOiJIUzI1NiIsI...

Slack's authorization server can now evaluate this request to determine whether to issue an access token. The authorization server can validate the JWT by checking the issuer (iss) in the JWT to determine which enterprise IdP the token is from, and then check the signature using the public key discovered at that server. There are other claims to be validated as well, described in Section 6.1 of the Identity Assertion Authorization Grant.

Assuming all the validations pass, Slack is ready to issue an access token to Claude in the token response:

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
  "token_type": "Bearer",
  "access_token": "2YotnFZFEjr1zCsicMWpAA",
  "expires_in": 86400
}

This token response is the same format that Slack's authorization server would be responding to a traditional OAuth flow. That's another key aspect of this design that makes it scalable. We don't need the resource app to use any particular access token format, since only that server is responsible for validating those tokens.

Now that Claude has the access token, it can make a request to the (hypothetical) Slack MCP server using the bearer token the same way it would have if it got the token using the traditional redirect-based OAuth flow.

Note: Eventually we'll need to define the specific behavior of when to return a refresh token in this token response. The goal is to ensure the client goes through the IdP often enough for the IdP to enforce its access policies. A refresh token could potentially undermine that if the refresh token lifetime is too long. It follows that ultimately the IdP should enforce the refresh token lifetime, so we will need to define a way for the IdP to communicate to the authorization server whether and how long to issue refresh tokens. This would enable the authorization server to make its own decision on access token lifetime, while still respecting the enterprise IdP policy.

Cross-App Access Sequence Diagram

Here's the flow again, this time as a sequence diagram.

Cross-App Access Sequence Diagram

  1. The client initiates a login request
  2. The user's browser is redirected to the IdP
  3. The user logs in at the IdP
  4. The IdP returns an OAuth authorization code to the user's browser
  5. The user's browser delivers the authorization code to the client
  6. The client exchanges the authorization code for an ID token at the IdP
  7. The IdP returns an ID token to the client

At this point, the user is logged in to the MCP client. Everything up until this point has been a standard OpenID Connect flow.

  1. The client makes a direct Token Exchange request to the IdP to exchange the ID token for a cross-domain "ID Assertion JWT"
  2. The IdP validates the request and checks the internal policy
  3. The IdP returns the ID-JAG to the client
  4. The client makes a token request using the ID-JAG to the MCP authorization server
  5. The authorization server validates the token using the signing key it also uses for its OpenID Connect flow with the IdP
  6. The authorization server returns an access token
  7. The client makes a request with the access token to the MCP server
  8. The MCP server returns the response

For a more detailed step by step of the flow, see Appendix A.3 of the Identity Assertion Authorization Grant.

Next Steps

If this is something you're interested in, we'd love your help! The in-progress spec is publicly available, and we're looking for people interested in helping prototype it. If you're building an MCP server and you want to make it enterprise-ready, I'd be happy to help you build this!

You can find me at a few related events coming up:

And of course you can always find me on LinkedIn or email me at aaron.parecki@okta.com.

2025-04-03T16:39:37-07:00 Fullscreen Open in Tab
Let's fix OAuth in MCP
Update: The changes described in this blog post have been incorporated into the 2025-06-18 version of the MCP spec!

Let's not overthink auth in MCP.

Yes, the MCP server is going to need its own auth server. But it's not as bad as it sounds. Let me explain.

First let's get a few pieces of terminology straight.

The confusion that's happening in the discussions I've seen so far is because the spec and diagrams show that the MCP server itself is handing authorization. That's not necessary.

oauth roles

In OAuth, we talk about the "authorization server" and "resource server" as distinct roles. I like to think of the authorization server as the "token factory", that's the thing that makes the access tokens. The resource server (usually an API) needs to be able to validate the tokens created by the authorization server.

combined AS and RS

It's possible to build a single server that is both a resource server and authorization server, and in fact many OAuth systems are built that way, especially large consumer services.

separate AS and RS

But nothing about the spec requires that the two roles are combined, it's also possible to run these as two totally unrelated services.

This flexibility that's been baked into OAuth for over a decade is what has led to the rapid adoption, as well the proliferation of open source and commercial products that provide an OAuth authorization server as a service.

So how does this relate to MCP?

I can annotate the flow from the Model Context Protocol spec to show the parts where the client talks to the MCP Resource Server separately from where the client talks to the MCP Authorization Server.

MCP Flow showing AS and RS highlighted

Here is the updated sequence diagram showing communication with each role separately.

New MCP diagram showing separate AS and RS

Why is it important to call out this change?

I've seen a few conversations in various places about how requiring the MCP Server to be both an authorization server and resource server is too much of a burden. But actually, very little needs to change about the spec to enable this separation of concerns that OAuth already provides.

I've also seen various suggestions of other ways to separate the authorization server from the MCP server, like delegating to an enterprise IdP and having the MCP server validate access tokens issued by the IdP. These other options also conflate the OAuth roles in an awkward way and would result in some undesirable properties or relationships between the various parties involved.

So what needs to change in the MCP spec to enable this?

Discovery

The main thing currently forcing the MCP Server to be both the authorization server and resource server is how the client does discovery.

One design goal of MCP is to enable a client to bootstrap everything it needs based on only the server URL provided. I think this is a great design goal, and luckily is something that can be achieved even when separating the roles in the way I've described.

The MCP spec currently says that clients are expected to fetch the OAuth Server Metadata (RFC8414) file from the MCP Server base URL, resulting in a URL such as:

https://example.com/.well-known/oauth-authorization-server

This ends up meaning the MCP Resource Server must also be an Authorization Server, which leads to the complications the community has encountered so far. The good news is there is an OAuth spec we can apply here instead: Protected Resource Metadata.

Protected Resource Metadata

The Protected Resource Metadata spec is used by a Resource Server to advertise metadata about itself, including which Authorization Server can be used with it. This spec is both new and old. It was started in 2016, but was never adopted by the OAuth working group until 2023, after I had presented at an IETF meeting about the need for clients to be able to bootstrap OAuth flows given an OAuth resource server. The spec is now awaiting publication as an RFC, and should get its RFC number in a couple months. (Update: This became RFC 9728 on April 23, 2025!)

Applying this to the MCP server would result in a sequence like the following:

New discovery flow for MCP

  1. The MCP Client fetches the Resource Server Metadata file by appending /.well-known/oauth-protected-resource to the MCP Server base URL.
  2. The MCP Client finds the authorization_servers property in the JSON response, and builds the Authorization Server Metadata URL by appending /.well-known/oauth-authorization-server
  3. The MCP Client fetches the Authorization Server Metadata to find the endpoints it needs for the OAuth flow, the authorization endpoint and token endpoint
  4. The MCP Client initiates an OAuth flow and continues as normal


Note: The Protected Resource Metadata spec also supports the Resource Server returning WWW-Authenticate with a link to the resource metadata URL if you want to avoid the requirement that MCP Servers host their metadata URLs at the .well-known endpoint, it just requires an extra HTTP request to support this.

Access Token Validation

Two things to keep in mind about how the MCP Server validates access tokens with this new separation of concerns.

If you do build the MCP Authorization Server and Resource Server as part of the same system, you don't need to do anything special to validate the access tokens the Authorization Server issues. You probably already have some sort of infrastructure in place for your normal API to validate tokens issued by your Authorization Server, so nothing changes there.

If you are using an external Authorization Server, whether that's an open source product or a commercial hosted service, that product will have its own docs for how you can validate the tokens it creates. There's a good chance it already supports the standardized JWT Access Tokens described in RFC 9068, in which case you can use off-the-shelf JWT validation middleware for common frameworks.

In either case, the critical design goal here is that the MCP Authorization Server issues access tokens that only ever need to be validated by the MCP Resource Server. This is in line with the security recommendations in Section 2.3 of RFC 9700, in particular that "access tokens SHOULD be audience-restricted to a specific resource server". In other words, it would be a bad idea for the MCP Client to be issued an access token that works with both the MCP Resource Server and the service's REST API.

Why Require the MCP Server to have an Authorization Server in the first place?

Another argument I've seen is that MCP Server developers shouldn't have to build any OAuth infrastructure at all, instead they should be able to delegate all the OAuth bits to an external service.

In principle, I agree. Getting API access and authorization right is tricky, that's why there are entire companies dedicated to solving the problem.

The architecture laid out above enables this exact separation of concerns. The difference between this architecture and some of the other proposals I've seen is that this cleanly separates the security boundaries so that there are minimal dependencies among the parties involved.

But, one thing I haven't seen mentioned in the discussions is that there actually is no requirement than an OAuth Authorization Server provide any UI itself.

An Authorization Server with no UI?

While it is desirable from a security perspective that the MCP Resource Server has a corresponding Authorization Server that issues access tokens for it, that Authorization Server doesn't actually need to have any UI or even any concept of user login or accounts. You can actually build an Authorization Server that delegates all user account management to an external service. You can see an example of this in PayPal's MCP server they recently launched.

PayPal's traditional API already supports OAuth, the authorization and token endpoints are:

  • https://www.paypal.com/signin/authorize
  • https://api-m.paypal.com/v1/oauth2/token

When PayPal built their MCP server, they launched it at https://mcp.paypal.com. If you fetch the metadata for the MCP Server, you'll find the two OAuth endpoints for the MCP Authorization Server:

  • https://mcp.paypal.com/authorize
  • https://mcp.paypal.com/token

When the MCP Client redirects the user to the authorization endpoint, the MCP server itself doesn't provide any UI. Instead, it immediately redirects the user to the real PayPal authorization endpoint which then prompts the user to log in and authorize the client.

Roles with backend API and Authorization Servers

This points to yet another benefit of architecting the MCP Authorization Server and Resource Server this way. It enables implementers to delegate the actual user management to their existing OAuth server with no changes needed to the MCP Client. The MCP Client isn't even aware that this extra redirect step was inserted in the middle. As far as the MCP Client is concerned, it has been talking to only the MCP Authorization Server. It just so happens that the MCP Authorization Server has sent the user elsewhere to actually log in.

Dynamic Client Registration

There's one more point I want to make about why having a dedicated MCP Authorization Server is helpful architecturally.

The MCP spec strongly recommends that MCP Servers (authorization servers) support Dynamic Client Registration. If MCP is successful, there will be a large number of MCP Clients talking to a large number of MCP Servers, and the user is the one deciding which combinations of clients and servers to use. This means it is not scalable to require that every MCP Client developer register their client with every MCP Server.

This is similar to the idea of using an email client with the user's chosen email server. Obviously Mozilla can't register Thunderbird with every email server out there. Instead, there needs to be a way to dynamically establish a client's identity with the OAuth server at runtime. Dynamic Client Registration is one option for how to do that.

The problem is most commercial APIs are not going to enable Dynamic Client Registration on their production servers. For example, in order to get client credentials to use the Google APIs, you need to register as a developer and then register an OAuth client after logging in. Dynamic Client Registration would allow a client to register itself without the link to the developer's account. That would mean there is no paper trail for who the client was developed by. The Dynamic Client Registration endpoint can't require authentication by definition, so is a public endpoint that can create clients, which as you can imagine opens up some potential security issues.

I do, however, think it would be reasonable to expect production services to enable Dynamic Client Registration only on the MCP's Authorization Server. This way the dynamically-registered clients wouldn't be able to use the regular REST API, but would only be able to interact with the MCP API.

Mastodon and BlueSky also have a similar problem of needing clients to show up at arbitrary authorization servers without prior coordination between the client developer and authorization server operator. I call this the "OAuth for the Open Web" problem. Mastodon used Dynamic Client Registration as their solution, and has since documented some of the issues that this creates, linked here and here.

BlueSky decided to take a different approach and instead uses an https URL as a client identifier, bypassing the need for a client registration step entirely. This has the added bonus of having at least some level of confidence of the client identity because the client identity is hosted at a domain. It would be a perfectly viable approach to use this method for MCP as well. There is a discussion on that within MCP here. This is an ongoing topic within the OAuth working group, I have a couple of drafts in progress to formalize this pattern, Client ID Metadata Document and Client ID Scheme.

Enterprise IdP Integration

Lastly, I want to touch on the idea of enabling users to log in to MCP Servers with their enterprise IdP.

When an enterprise company purchases software, they expect to be able to tie it in to their single-sign-on solution. For example, when I log in to work Slack, I enter my work email and Slack redirects me to my work IdP where I log in. This way employees don't need to have passwords with every app they use in the enterprise, they can log in to everything with the same enterprise account, and all the apps can be protected with multi-factor authentication through the IdP. This also gives the company control over which users can access which apps, as well as a way to revoke a user's access at any time.

So how does this relate to MCP?

Well, plenty of people are already trying to figure out how to let their employees safely use AI tools within the enterprise. So we need a way to let employees use their enterprise IdP to log in and authorize MCP Clients to access MCP Servers.

If you're building an MCP Server in front of an existing application that already supports enterprise Single Sign-On, then you don't need to do anything differently in the MCP Client or Server and you already have support for this. When the MCP Client redirects to the MCP Authorization Server, the MCP Authorization Server redirects to the main Authorization Server, which would then prompt the user for their company email/domain and redirect to the enterprise IdP to log in.

This brings me to yet another thing I've been seeing conflated in the discussions: user login and user authorization.

OAuth is an authorization delegation protocol. OAuth doesn't actually say anything about how users authenticate at the OAuth server, it only talks about how the user can authorize access to an application. This is actually a really great thing, because it means we can get super creative with how users authenticate.

User logs in and authorizes

Remember the yellow box "User logs in and authorizes" from the original sequence diagram? These are actually two totally distinct steps. The OAuth authorization server is responsible for getting the user to log in somehow, but there's no requirement that how the user logs in is with a username/password. This is where we can insert a single-sign-on flow to an enterprise IdP, or really anything you can imagine.

So think of this as two separate boxes: "user logs in", and "user authorizes". Then, we can replace the "user logs in" box with an entirely new OpenID Connect flow out to the enterprise IdP to log the user in, and after they are logged in they can authorize the client.

User logs in with OIDC

I'll spare you the complete expanded sequence diagram, since it looks a lot more complicated than it actually is. But I again want to stress that this is nothing new, this is already how things are commonly done today.

This all just becomes cleaner to understand when you separate the MCP Authorization Server from the MCP Resource Server.

We can push all the complexity of user login, token minting, and more onto the MCP Authorization Server, keeping the MCP Resource Server free to do the much simpler task of validating access tokens and serving resources.

Future Improvements of Enterprise IdP Integration

There are two things I want to call out about how enterprise IdP integration could be improved. Both of these are entire topics on their own, so I will only touch on the problems and link out to other places where work is happening to solve them.

There are two points of friction with the current state of enterprise login for SaaS apps.

  • IdP discovery
  • User consent

IdP Discovery

When a user logs in to a SaaS app, they need to tell the app how to find their enterprise IdP. This is commonly done by either asking the user to enter their work email, or asking the user to enter their tenant URL at the service.

Sign in with SSO

Neither of these is really a great user experience. It would be a lot better if the browser already knew which enterprise IdP the user should be sent to. This is one of my goals with the work happening in FedCM. With this new browser API, the browser can mediate the login, telling the SaaS app which enterprise IdP to use automatically only needing the user to click their account icon rather than type anything in.

User Consent

Another point of friction in the enterprise happens when a user starts connecting multiple applications to each other within the company. For example, if you drop in a Google Docs link into Slack, Slack will prompt you to connect your Google account to preview the link. Multiply this by N number of applications that can preview links, and M number of applications you might drop links to, and you end up sending the user through a huge number of OAuth consent flows.

The problem is only made worse with the explosion of AI tools. Every AI tool will need access to data in every other application in the enterprise. That is a lot of OAuth consent flows for the user to manage. Plus, the user shouldn't really be the one granting consent for Slack to access the company Google Docs account anyway. That consent should ideally be managed by the enterprise IT admin.

What we actually need is a way to enable the IT admin to grant consent for apps to talk to each other company-wide, removing the need for users to be sent through an OAuth flow at all.

This is the basis of another OAuth spec I've been working on, the Identity Assertion Authorization Grant.

The same problem applies to MCP Servers, and with the separation of concerns laid out above, it becomes straightforward to add this extension to move the consent to the enterprise and streamline the user experience.

Get in touch!

If these sound like interesting problems, please get in touch! You can find me on LinkedIn or reach me via email at aaron@parecki.com.

2025-03-07T00:00:00+00:00 Fullscreen Open in Tab
Standards for ANSI escape codes

Hello! Today I want to talk about ANSI escape codes.

For a long time I was vaguely aware of ANSI escape codes (“that’s how you make text red in the terminal and stuff”) but I had no real understanding of where they were supposed to be defined or whether or not there were standards for them. I just had a kind of vague “there be dragons” feeling around them. While learning about the terminal this year, I’ve learned that:

  1. ANSI escape codes are responsible for a lot of usability improvements in the terminal (did you know there’s a way to copy to your system clipboard when SSHed into a remote machine?? It’s an escape code called OSC 52!)
  2. They aren’t completely standardized, and because of that they don’t always work reliably. And because they’re also invisible, it’s extremely frustrating to troubleshoot escape code issues.

So I wanted to put together a list for myself of some standards that exist around escape codes, because I want to know if they have to feel unreliable and frustrating, or if there’s a future where we could all rely on them with more confidence.

what’s an escape code?

Have you ever pressed the left arrow key in your terminal and seen ^[[D? That’s an escape code! It’s called an “escape code” because the first character is the “escape” character, which is usually written as ESC, \x1b, \E, \033, or ^[.

Escape codes are how your terminal emulator communicates various kinds of information (colours, mouse movement, etc) with programs running in the terminal. There are two kind of escape codes:

  1. input codes which your terminal emulator sends for keypresses or mouse movements that don’t fit into Unicode. For example “left arrow key” is ESC[D, “Ctrl+left arrow” might be ESC[1;5D, and clicking the mouse might be something like ESC[M :3.
  2. output codes which programs can print out to colour text, move the cursor around, clear the screen, hide the cursor, copy text to the clipboard, enable mouse reporting, set the window title, etc.

Now let’s talk about standards!

ECMA-48

The first standard I found relating to escape codes was ECMA-48, which was originally published in 1976.

ECMA-48 does two things:

  1. Define some general formats for escape codes (like “CSI” codes, which are ESC[ + something and “OSC” codes, which are ESC] + something)
  2. Define some specific escape codes, like how “move the cursor to the left” is ESC[D, or “turn text red” is ESC[31m. In the spec, the “cursor left” one is called CURSOR LEFT and the one for changing colours is called SELECT GRAPHIC RENDITION.

The formats are extensible, so there’s room for others to define more escape codes in the future. Lots of escape codes that are popular today aren’t defined in ECMA-48: for example it’s pretty common for terminal applications (like vim, htop, or tmux) to support using the mouse, but ECMA-48 doesn’t define escape codes for the mouse.

xterm control sequences

There are a bunch of escape codes that aren’t defined in ECMA-48, for example:

  • enabling mouse reporting (where did you click in your terminal?)
  • bracketed paste (did you paste that text or type it in?)
  • OSC 52 (which terminal applications can use to copy text to your system clipboard)

I believe (correct me if I’m wrong!) that these and some others came from xterm, are documented in XTerm Control Sequences, and have been widely implemented by other terminal emulators.

This list of “what xterm supports” is not a standard exactly, but xterm is extremely influential and so it seems like an important document.

terminfo

In the 80s (and to some extent today, but my understanding is that it was MUCH more dramatic in the 80s) there was a huge amount of variation in what escape codes terminals actually supported.

To deal with this, there’s a database of escape codes for various terminals called “terminfo”.

It looks like the standard for terminfo is called X/Open Curses, though you need to create an account to view that standard for some reason. It defines the database format as well as a C library interface (“curses”) for accessing the database.

For example you can run this bash snippet to see every possible escape code for “clear screen” for all of the different terminals your system knows about:

for term in $(toe -a | awk '{print $1}')
do
  echo $term
  infocmp -1 -T "$term" 2>/dev/null | grep 'clear=' | sed 's/clear=//g;s/,//g'
done

On my system (and probably every system I’ve ever used?), the terminfo database is managed by ncurses.

should programs use terminfo?

I think it’s interesting that there are two main approaches that applications take to handling ANSI escape codes:

  1. Use the terminfo database to figure out which escape codes to use, depending on what’s in the TERM environment variable. Fish does this, for example.
  2. Identify a “single common set” of escape codes which works in “enough” terminal emulators and just hardcode those.

Some examples of programs/libraries that take approach #2 (“don’t use terminfo”) include:

I got curious about why folks might be moving away from terminfo and I found this very interesting and extremely detailed rant about terminfo from one of the fish maintainers, which argues that:

[the terminfo authors] have done a lot of work that, at the time, was extremely important and helpful. My point is that it no longer is.

I’m not going to do it justice so I’m not going to summarize it, I think it’s worth reading.

is there a “single common set” of escape codes?

I was just talking about the idea that you can use a “common set” of escape codes that will work for most people. But what is that set? Is there any agreement?

I really do not know the answer to this at all, but from doing some reading it seems like it’s some combination of:

  • The codes that the VT100 supported (though some aren’t relevant on modern terminals)
  • what’s in ECMA-48 (which I think also has some things that are no longer relevant)
  • What xterm supports (though I’d guess that not everything in there is actually widely supported enough)

and maybe ultimately “identify the terminal emulators you think your users are going to use most frequently and test in those”, the same way web developers do when deciding which CSS features are okay to use

I don’t think there are any resources like Can I use…? or Baseline for the terminal though. (in theory terminfo is supposed to be the “caniuse” for the terminal but it seems like it often takes 10+ years to add new terminal features when people invent them which makes it very limited)

some reasons to use terminfo

I also asked on Mastodon why people found terminfo valuable in 2025 and got a few reasons that made sense to me:

  • some people expect to be able to use the TERM environment variable to control how programs behave (for example with TERM=dumb), and there’s no standard for how that should work in a post-terminfo world
  • even though there’s less variation between terminal emulators than there was in the 80s, there’s far from zero variation: there are graphical terminals, the Linux framebuffer console, the situation you’re in when connecting to a server via its serial console, Emacs shell mode, and probably more that I’m missing
  • there is no one standard for what the “single common set” of escape codes is, and sometimes programs use escape codes which aren’t actually widely supported enough

terminfo & user agent detection

The way that ncurses uses the TERM environment variable to decide which escape codes to use reminds me of how webservers used to sometimes use the browser user agent to decide which version of a website to serve.

It also seems like it’s had some of the same results – the way iTerm2 reports itself as being “xterm-256color” feels similar to how Safari’s user agent is “Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3 Safari/605.1.15”. In both cases the terminal emulator / browser ends up changing its user agent to get around user agent detection that isn’t working well.

On the web we ended up deciding that user agent detection was not a good practice and to instead focus on standardization so we can serve the same HTML/CSS to all browsers. I don’t know if the same approach is the future in the terminal though – I think the terminal landscape today is much more fragmented than the web ever was as well as being much less well funded.

some more documents/standards

A few more documents and standards related to escape codes, in no particular order:

why I think this is interesting

I sometimes see people saying that the unix terminal is “outdated”, and since I love the terminal so much I’m always curious about what incremental changes might make it feel less “outdated”.

Maybe if we had a clearer standards landscape (like we do on the web!) it would be easier for terminal emulator developers to build new features and for authors of terminal applications to more confidently adopt those features so that we can all benefit from them and have a richer experience in the terminal.

Obviously standardizing ANSI escape codes is not easy (ECMA-48 was first published almost 50 years ago and we’re still not there!). I don’t even know what all of the challenges are. But the situation with HTML/CSS/JS used to be extremely bad too and now it’s MUCH better, so maybe there’s hope.

2025-02-13T12:27:56+00:00 Fullscreen Open in Tab
How to add a directory to your PATH

I was talking to a friend about how to add a directory to your PATH today. It’s something that feels “obvious” to me since I’ve been using the terminal for a long time, but when I searched for instructions for how to do it, I actually couldn’t find something that explained all of the steps – a lot of them just said “add this to ~/.bashrc”, but what if you’re not using bash? What if your bash config is actually in a different file? And how are you supposed to figure out which directory to add anyway?

So I wanted to try to write down some more complete directions and mention some of the gotchas I’ve run into over the years.

Here’s a table of contents:

step 1: what shell are you using?

If you’re not sure what shell you’re using, here’s a way to find out. Run this:

ps -p $$ -o pid,comm=
  • if you’re using bash, it’ll print out 97295 bash
  • if you’re using zsh, it’ll print out 97295 zsh
  • if you’re using fish, it’ll print out an error like “In fish, please use $fish_pid” ($$ isn’t valid syntax in fish, but in any case the error message tells you that you’re using fish, which you probably already knew)

Also bash is the default on Linux and zsh is the default on Mac OS (as of 2024). I’ll only cover bash, zsh, and fish in these directions.

step 2: find your shell’s config file

  • in zsh, it’s probably ~/.zshrc
  • in bash, it might be ~/.bashrc, but it’s complicated, see the note in the next section
  • in fish, it’s probably ~/.config/fish/config.fish (you can run echo $__fish_config_dir if you want to be 100% sure)

a note on bash’s config file

Bash has three possible config files: ~/.bashrc, ~/.bash_profile, and ~/.profile.

If you’re not sure which one your system is set up to use, I’d recommend testing this way:

  1. add echo hi there to your ~/.bashrc
  2. Restart your terminal
  3. If you see “hi there”, that means ~/.bashrc is being used! Hooray!
  4. Otherwise remove it and try the same thing with ~/.bash_profile
  5. You can also try ~/.profile if the first two options don’t work.

(there are a lot of elaborate flow charts out there that explain how bash decides which config file to use but IMO it’s not worth it to internalize them and just testing is the fastest way to be sure)

step 3: figure out which directory to add

Let’s say that you’re trying to install and run a program called http-server and it doesn’t work, like this:

$ npm install -g http-server
$ http-server
bash: http-server: command not found

How do you find what directory http-server is in? Honestly in general this is not that easy – often the answer is something like “it depends on how npm is configured”. A few ideas:

  • Often when setting up a new installer (like cargo, npm, homebrew, etc), when you first set it up it’ll print out some directions about how to update your PATH. So if you’re paying attention you can get the directions then.
  • Sometimes installers will automatically update your shell’s config file to update your PATH for you
  • Sometimes just Googling “where does npm install things?” will turn up the answer
  • Some tools have a subcommand that tells you where they’re configured to install things, like:
    • Node/npm: npm config get prefix (then append /bin/)
    • Go: go env GOPATH (then append /bin/)
    • asdf: asdf info | grep ASDF_DIR (then append /bin/ and /shims/)

step 3.1: double check it’s the right directory

Once you’ve found a directory you think might be the right one, make sure it’s actually correct! For example, I found out that on my machine, http-server is in ~/.npm-global/bin. I can make sure that it’s the right directory by trying to run the program http-server in that directory like this:

$ ~/.npm-global/bin/http-server
Starting up http-server, serving ./public

It worked! Now that you know what directory you need to add to your PATH, let’s move to the next step!

step 4: edit your shell config

Now we have the 2 critical pieces of information we need:

  1. Which directory you’re trying to add to your PATH (like ~/.npm-global/bin/)
  2. Where your shell’s config is (like ~/.bashrc, ~/.zshrc, or ~/.config/fish/config.fish)

Now what you need to add depends on your shell:

bash instructions:

Open your shell’s config file, and add a line like this:

export PATH=$PATH:~/.npm-global/bin/

(obviously replace ~/.npm-global/bin with the actual directory you’re trying to add)

zsh instructions:

You can do the same thing as in bash, but zsh also has some slightly fancier syntax you can use if you prefer:

path=(
  $path
  ~/.npm-global/bin
)

fish instructions:

In fish, the syntax is different:

set PATH $PATH ~/.npm-global/bin

(in fish you can also use fish_add_path, some notes on that further down)

step 5: restart your shell

Now, an extremely important step: updating your shell’s config won’t take effect if you don’t restart it!

Two ways to do this:

  1. open a new terminal (or terminal tab), and maybe close the old one so you don’t get confused
  2. Run bash to start a new shell (or zsh if you’re using zsh, or fish if you’re using fish)

I’ve found that both of these usually work fine.

And you should be done! Try running the program you were trying to run and hopefully it works now.

If not, here are a couple of problems that you might run into:

problem 1: it ran the wrong program

If the wrong version of a program is running, you might need to add the directory to the beginning of your PATH instead of the end.

For example, on my system I have two versions of python3 installed, which I can see by running which -a:

$ which -a python3
/usr/bin/python3
/opt/homebrew/bin/python3

The one your shell will use is the first one listed.

If you want to use the Homebrew version, you need to add that directory (/opt/homebrew/bin) to the beginning of your PATH instead, by putting this in your shell’s config file (it’s /opt/homebrew/bin/:$PATH instead of the usual $PATH:/opt/homebrew/bin/)

export PATH=/opt/homebrew/bin/:$PATH

or in fish:

set PATH ~/.cargo/bin $PATH

problem 2: the program isn’t being run from your shell

All of these directions only work if you’re running the program from your shell. If you’re running the program from an IDE, from a GUI, in a cron job, or some other way, you’ll need to add the directory to your PATH in a different way, and the exact details might depend on the situation.

in a cron job

Some options:

  • use the full path to the program you’re running, like /home/bork/bin/my-program
  • put the full PATH you want as the first line of your crontab (something like PATH=/bin:/usr/bin:/usr/local/bin:….). You can get the full PATH you’re using in your shell by running echo "PATH=$PATH".

I’m honestly not sure how to handle it in an IDE/GUI because I haven’t run into that in a long time, will add directions here if someone points me in the right direction.

problem 3: duplicate PATH entries making it harder to debug

If you edit your path and start a new shell by running bash (or zsh, or fish), you’ll often end up with duplicate PATH entries, because the shell keeps adding new things to your PATH every time you start your shell.

Personally I don’t think I’ve run into a situation where this kind of duplication breaks anything, but the duplicates can make it harder to debug what’s going on with your PATH if you’re trying to understand its contents.

Some ways you could deal with this:

  1. If you’re debugging your PATH, open a new terminal to do it in so you get a “fresh” state. This should avoid the duplication.
  2. Deduplicate your PATH at the end of your shell’s config (for example in zsh apparently you can do this with typeset -U path)
  3. Check that the directory isn’t already in your PATH when adding it (for example in fish I believe you can do this with fish_add_path --path /some/directory)

How to deduplicate your PATH is shell-specific and there isn’t always a built in way to do it so you’ll need to look up how to accomplish it in your shell.

problem 4: losing your history after updating your PATH

Here’s a situation that’s easy to get into in bash or zsh:

  1. Run a command (it fails)
  2. Update your PATH
  3. Run bash to reload your config
  4. Press the up arrow a couple of times to rerun the failed command (or open a new terminal)
  5. The failed command isn’t in your history! Why not?

This happens because in bash, by default, history is not saved until you exit the shell.

Some options for fixing this:

  • Instead of running bash to reload your config, run source ~/.bashrc (or source ~/.zshrc in zsh). This will reload the config inside your current session.
  • Configure your shell to continuously save your history instead of only saving the history when the shell exits. (How to do this depends on whether you’re using bash or zsh, the history options in zsh are a bit complicated and I’m not exactly sure what the best way is)

a note on source

When you install cargo (Rust’s installer) for the first time, it gives you these instructions for how to set up your PATH, which don’t mention a specific directory at all.

This is usually done by running one of the following (note the leading DOT):

. "$HOME/.cargo/env"        	# For sh/bash/zsh/ash/dash/pdksh
source "$HOME/.cargo/env.fish"  # For fish

The idea is that you add that line to your shell’s config, and their script automatically sets up your PATH (and potentially other things) for you.

This is pretty common (for example Homebrew suggests you eval brew shellenv), and there are two ways to approach this:

  1. Just do what the tool suggests (like adding . "$HOME/.cargo/env" to your shell’s config)
  2. Figure out which directories the script they’re telling you to run would add to your PATH, and then add those manually. Here’s how I’d do that:
    • Run . "$HOME/.cargo/env" in my shell (or the fish version if using fish)
    • Run echo "$PATH" | tr ':' '\n' | grep cargo to figure out which directories it added
    • See that it says /Users/bork/.cargo/bin and shorten that to ~/.cargo/bin
    • Add the directory ~/.cargo/bin to PATH (with the directions in this post)

I don’t think there’s anything wrong with doing what the tool suggests (it might be the “best way”!), but personally I usually use the second approach because I prefer knowing exactly what configuration I’m changing.

a note on fish_add_path

fish has a handy function called fish_add_path that you can run to add a directory to your PATH like this:

fish_add_path /some/directory

This is cool (it’s such a simple command!) but I’ve stopped using it for a couple of reasons:

  1. Sometimes fish_add_path will update the PATH for every session in the future (with a “universal variable”) and sometimes it will update the PATH just for the current session and it’s hard for me to tell which one it will do. In theory the docs explain this but I could not understand them.
  2. If you ever need to remove the directory from your PATH a few weeks or months later because maybe you made a mistake, it’s kind of hard to do (there are instructions in this comments of this github issue though).

that’s all

Hopefully this will help some people. Let me know (on Mastodon or Bluesky) if you there are other major gotchas that have tripped you up when adding a directory to your PATH, or if you have questions about this post!

2025-02-05T16:57:00+00:00 Fullscreen Open in Tab
Some terminal frustrations

A few weeks ago I ran a terminal survey (you can read the results here) and at the end I asked:

What’s the most frustrating thing about using the terminal for you?

1600 people answered, and I decided to spend a few days categorizing all the responses. Along the way I learned that classifying qualitative data is not easy but I gave it my best shot. I ended up building a custom tool to make it faster to categorize everything.

As with all of my surveys the methodology isn’t particularly scientific. I just posted the survey to Mastodon and Twitter, ran it for a couple of days, and got answers from whoever happened to see it and felt like responding.

Here are the top categories of frustrations!

I think it’s worth keeping in mind while reading these comments that

  • 40% of people answering this survey have been using the terminal for 21+ years
  • 95% of people answering the survey have been using the terminal for at least 4 years

These comments aren’t coming from total beginners.

Here are the categories of frustrations! The number in brackets is the number of people with that frustration. I’m mostly writing this up for myself because I’m trying to write a zine about the terminal and I wanted to get a sense for what people are having trouble with.

remembering syntax (115)

People talked about struggles remembering:

  • the syntax for CLI tools like awk, jq, sed, etc
  • the syntax for redirects
  • keyboard shortcuts for tmux, text editing, etc

One example comment:

There are just so many little “trivia” details to remember for full functionality. Even after all these years I’ll sometimes forget where it’s 2 or 1 for stderr, or forget which is which for > and >>.

switching terminals is hard (91)

People talked about struggling with switching systems (for example home/work computer or when SSHing) and running into:

  • OS differences in keyboard shortcuts (like Linux vs Mac)
  • systems which don’t have their preferred text editor (“no vim” or “only vim”)
  • different versions of the same command (like Mac OS grep vs GNU grep)
  • no tab completion
  • a shell they aren’t used to (“the subtle differences between zsh and bash”)

as well as differences inside the same system like pagers being not consistent with each other (git diff pagers, other pagers).

One example comment:

I got used to fish and vi mode which are not available when I ssh into servers, containers.

color (85)

Lots of problems with color, like:

  • programs setting colors that are unreadable with a light background color
  • finding a colorscheme they like (and getting it to work consistently across different apps)
  • color not working inside several layers of SSH/tmux/etc
  • not liking the defaults
  • not wanting color at all and struggling to turn it off

This comment felt relatable to me:

Getting my terminal theme configured in a reasonable way between the terminal emulator and fish (I did this years ago and remember it being tedious and fiddly and now feel like I’m locked into my current theme because it works and I dread touching any of that configuration ever again).

keyboard shortcuts (84)

Half of the comments on keyboard shortcuts were about how on Linux/Windows, the keyboard shortcut to copy/paste in the terminal is different from in the rest of the OS.

Some other issues with keyboard shortcuts other than copy/paste:

  • using Ctrl-W in a browser-based terminal and closing the window
  • the terminal only supports a limited set of keyboard shortcuts (no Ctrl-Shift-, no Super, no Hyper, lots of ctrl- shortcuts aren’t possible like Ctrl-,)
  • the OS stopping you from using a terminal keyboard shortcut (like by default Mac OS uses Ctrl+left arrow for something else)
  • issues using emacs in the terminal
  • backspace not working (2)

other copy and paste issues (75)

Aside from “the keyboard shortcut for copy and paste is different”, there were a lot of OTHER issues with copy and paste, like:

  • copying over SSH
  • how tmux and the terminal emulator both do copy/paste in different ways
  • dealing with many different clipboards (system clipboard, vim clipboard, the “middle click” clipboard on Linux, tmux’s clipboard, etc) and potentially synchronizing them
  • random spaces added when copying from the terminal
  • pasting multiline commands which automatically get run in a terrifying way
  • wanting a way to copy text without using the mouse

discoverability (55)

There were lots of comments about this, which all came down to the same basic complaint – it’s hard to discover useful tools or features! This comment kind of summed it all up:

How difficult it is to learn independently. Most of what I know is an assorted collection of stuff I’ve been told by random people over the years.

steep learning curve (44)

A lot of comments about it generally having a steep learning curve. A couple of example comments:

After 15 years of using it, I’m not much faster than using it than I was 5 or maybe even 10 years ago.

and

That I know I could make my life easier by learning more about the shortcuts and commands and configuring the terminal but I don’t spend the time because it feels overwhelming.

history (42)

Some issues with shell history:

  • history not being shared between terminal tabs (16)
  • limits that are too short (4)
  • history not being restored when terminal tabs are restored
  • losing history because the terminal crashed
  • not knowing how to search history

One example comment:

It wasted a lot of time until I figured it out and still annoys me that “history” on zsh has such a small buffer; I have to type “history 0” to get any useful length of history.

bad documentation (37)

People talked about:

  • documentation being generally opaque
  • lack of examples in man pages
  • programs which don’t have man pages

Here’s a representative comment:

Finding good examples and docs. Man pages often not enough, have to wade through stack overflow

scrollback (36)

A few issues with scrollback:

  • programs printing out too much data making you lose scrollback history
  • resizing the terminal messes up the scrollback
  • lack of timestamps
  • GUI programs that you start in the background printing stuff out that gets in the way of other programs’ outputs

One example comment:

When resizing the terminal (in particular: making it narrower) leads to broken rewrapping of the scrollback content because the commands formatted their output based on the terminal window width.

“it feels outdated” (33)

Lots of comments about how the terminal feels hampered by legacy decisions and how users often end up needing to learn implementation details that feel very esoteric. One example comment:

Most of the legacy cruft, it would be great to have a green field implementation of the CLI interface.

shell scripting (32)

Lots of complaints about POSIX shell scripting. There’s a general feeling that shell scripting is difficult but also that switching to a different less standard scripting language (fish, nushell, etc) brings its own problems.

Shell scripting. My tolerance to ditch a shell script and go to a scripting language is pretty low. It’s just too messy and powerful. Screwing up can be costly so I don’t even bother.

more issues

Some more issues that were mentioned at least 10 times:

  • (31) inconsistent command line arguments: is it -h or help or –help?
  • (24) keeping dotfiles in sync across different systems
  • (23) performance (e.g. “my shell takes too long to start”)
  • (20) window management (potentially with some combination of tmux tabs, terminal tabs, and multiple terminal windows. Where did that shell session go?)
  • (17) generally feeling scared/uneasy (“The debilitating fear that I’m going to do some mysterious Bad Thing with a command and I will have absolutely no idea how to fix or undo it or even really figure out what happened”)
  • (16) terminfo issues (“Having to learn about terminfo if/when I try a new terminal emulator and ssh elsewhere.”)
  • (16) lack of image support (sixel etc)
  • (15) SSH issues (like having to start over when you lose the SSH connection)
  • (15) various tmux/screen issues (for example lack of integration between tmux and the terminal emulator)
  • (15) typos & slow typing
  • (13) the terminal getting messed up for various reasons (pressing Ctrl-S, cating a binary, etc)
  • (12) quoting/escaping in the shell
  • (11) various Windows/PowerShell issues

n/a (122)

There were also 122 answers to the effect of “nothing really” or “only that I can’t do EVERYTHING in the terminal”

One example comment:

Think I’ve found work arounds for most/all frustrations

that’s all!

I’m not going to make a lot of commentary on these results, but here are a couple of categories that feel related to me:

  • remembering syntax & history (often the thing you need to remember is something you’ve run before!)
  • discoverability & the learning curve (the lack of discoverability is definitely a big part of what makes it hard to learn)
  • “switching systems is hard” & “it feels outdated” (tools that haven’t really changed in 30 or 40 years have many problems but they do tend to be always there no matter what system you’re on, which is very useful and makes them hard to stop using)

Trying to categorize all these results in a reasonable way really gave me an appreciation for social science researchers’ skills.

2025-01-11T09:46:01+00:00 Fullscreen Open in Tab
What's involved in getting a "modern" terminal setup?

Hello! Recently I ran a terminal survey and I asked people what frustrated them. One person commented:

There are so many pieces to having a modern terminal experience. I wish it all came out of the box.

My immediate reaction was “oh, getting a modern terminal experience isn’t that hard, you just need to….”, but the more I thought about it, the longer the “you just need to…” list got, and I kept thinking about more and more caveats.

So I thought I would write down some notes about what it means to me personally to have a “modern” terminal experience and what I think can make it hard for people to get there.

what is a “modern terminal experience”?

Here are a few things that are important to me, with which part of the system is responsible for them:

  • multiline support for copy and paste: if you paste 3 commands in your shell, it should not immediately run them all! That’s scary! (shell, terminal emulator)
  • infinite shell history: if I run a command in my shell, it should be saved forever, not deleted after 500 history entries or whatever. Also I want commands to be saved to the history immediately when I run them, not only when I exit the shell session (shell)
  • a useful prompt: I can’t live without having my current directory and current git branch in my prompt (shell)
  • 24-bit colour: this is important to me because I find it MUCH easier to theme neovim with 24-bit colour support than in a terminal with only 256 colours (terminal emulator)
  • clipboard integration between vim and my operating system so that when I copy in Firefox, I can just press p in vim to paste (text editor, maybe the OS/terminal emulator too)
  • good autocomplete: for example commands like git should have command-specific autocomplete (shell)
  • having colours in ls (shell config)
  • a terminal theme I like: I spend a lot of time in my terminal, I want it to look nice and I want its theme to match my terminal editor’s theme. (terminal emulator, text editor)
  • automatic terminal fixing: If a programs prints out some weird escape codes that mess up my terminal, I want that to automatically get reset so that my terminal doesn’t get messed up (shell)
  • keybindings: I want Ctrl+left arrow to work (shell or application)
  • being able to use the scroll wheel in programs like less: (terminal emulator and applications)

There are a million other terminal conveniences out there and different people value different things, but those are the ones that I would be really unhappy without.

how I achieve a “modern experience”

My basic approach is:

  1. use the fish shell. Mostly don’t configure it, except to:
    • set the EDITOR environment variable to my favourite terminal editor
    • alias ls to ls --color=auto
  2. use any terminal emulator with 24-bit colour support. In the past I’ve used GNOME Terminal, Terminator, and iTerm, but I’m not picky about this. I don’t really configure it other than to choose a font.
  3. use neovim, with a configuration that I’ve been very slowly building over the last 9 years or so (the last time I deleted my vim config and started from scratch was 9 years ago)
  4. use the base16 framework to theme everything

A few things that affect my approach:

  • I don’t spend a lot of time SSHed into other machines
  • I’d rather use the mouse a little than come up with keyboard-based ways to do everything
  • I work on a lot of small projects, not one big project

some “out of the box” options for a “modern” experience

What if you want a nice experience, but don’t want to spend a lot of time on configuration? Figuring out how to configure vim in a way that I was satisfied with really did take me like ten years, which is a long time!

My best ideas for how to get a reasonable terminal experience with minimal config are:

  • shell: either fish or zsh with oh-my-zsh
  • terminal emulator: almost anything with 24-bit colour support, for example all of these are popular:
    • linux: GNOME Terminal, Konsole, Terminator, xfce4-terminal
    • mac: iTerm (Terminal.app doesn’t have 256-colour support)
    • cross-platform: kitty, alacritty, wezterm, or ghostty
  • shell config:
    • set the EDITOR environment variable to your favourite terminal text editor
    • maybe alias ls to ls --color=auto
  • text editor: this is a tough one, maybe micro or helix? I haven’t used either of them seriously but they both seem like very cool projects and I think it’s amazing that you can just use all the usual GUI editor commands (Ctrl-C to copy, Ctrl-V to paste, Ctrl-A to select all) in micro and they do what you’d expect. I would probably try switching to helix except that retraining my vim muscle memory seems way too hard. Also helix doesn’t have a GUI or plugin system yet.

Personally I wouldn’t use xterm, rxvt, or Terminal.app as a terminal emulator, because I’ve found in the past that they’re missing core features (like 24-bit colour in Terminal.app’s case) that make the terminal harder to use for me.

I don’t want to pretend that getting a “modern” terminal experience is easier than it is though – I think there are two issues that make it hard. Let’s talk about them!

issue 1 with getting to a “modern” experience: the shell

bash and zsh are by far the two most popular shells, and neither of them provide a default experience that I would be happy using out of the box, for example:

  • you need to customize your prompt
  • they don’t come with git completions by default, you have to set them up
  • by default, bash only stores 500 (!) lines of history and (at least on Mac OS) zsh is only configured to store 2000 lines, which is still not a lot
  • I find bash’s tab completion very frustrating, if there’s more than one match then you can’t tab through them

And even though I love fish, the fact that it isn’t POSIX does make it hard for a lot of folks to make the switch.

Of course it’s totally possible to learn how to customize your prompt in bash or whatever, and it doesn’t even need to be that complicated (in bash I’d probably start with something like export PS1='[\u@\h \W$(__git_ps1 " (%s)")]\$ ', or maybe use starship). But each of these “not complicated” things really does add up and it’s especially tough if you need to keep your config in sync across several systems.

An extremely popular solution to getting a “modern” shell experience is oh-my-zsh. It seems like a great project and I know a lot of people use it very happily, but I’ve struggled with configuration systems like that in the past – it looks like right now the base oh-my-zsh adds about 3000 lines of config, and often I find that having an extra configuration system makes it harder to debug what’s happening when things go wrong. I personally have a tendency to use the system to add a lot of extra plugins, make my system slow, get frustrated that it’s slow, and then delete it completely and write a new config from scratch.

issue 2 with getting to a “modern” experience: the text editor

In the terminal survey I ran recently, the most popular terminal text editors by far were vim, emacs, and nano.

I think the main options for terminal text editors are:

  • use vim or emacs and configure it to your liking, you can probably have any feature you want if you put in the work
  • use nano and accept that you’re going to have a pretty limited experience (for example I don’t think you can select text with the mouse and then “cut” it in nano)
  • use micro or helix which seem to offer a pretty good out-of-the-box experience, potentially occasionally run into issues with using a less mainstream text editor
  • just avoid using a terminal text editor as much as possible, maybe use VSCode, use VSCode’s terminal for all your terminal needs, and mostly never edit files in the terminal. Or I know a lot of people use code as their EDITOR in the terminal.

issue 3: individual applications

The last issue is that sometimes individual programs that I use are kind of annoying. For example on my Mac OS machine, /usr/bin/sqlite3 doesn’t support the Ctrl+Left Arrow keyboard shortcut. Fixing this to get a reasonable terminal experience in SQLite was a little complicated, I had to:

  • realize why this is happening (Mac OS won’t ship GNU tools, and “Ctrl-Left arrow” support comes from GNU readline)
  • find a workaround (install sqlite from homebrew, which does have readline support)
  • adjust my environment (put Homebrew’s sqlite3 in my PATH)

I find that debugging application-specific issues like this is really not easy and often it doesn’t feel “worth it” – often I’ll end up just dealing with various minor inconveniences because I don’t want to spend hours investigating them. The only reason I was even able to figure this one out at all is that I’ve been spending a huge amount of time thinking about the terminal recently.

A big part of having a “modern” experience using terminal programs is just using newer terminal programs, for example I can’t be bothered to learn a keyboard shortcut to sort the columns in top, but in htop I can just click on a column heading with my mouse to sort it. So I use htop instead! But discovering new more “modern” command line tools isn’t easy (though I made a list here), finding ones that I actually like using in practice takes time, and if you’re SSHed into another machine, they won’t always be there.

everything affects everything else

Something I find tricky about configuring my terminal to make everything “nice” is that changing one seemingly small thing about my workflow can really affect everything else. For example right now I don’t use tmux. But if I needed to use tmux again (for example because I was doing a lot of work SSHed into another machine), I’d need to think about a few things, like:

  • if I wanted tmux’s copy to synchronize with my system clipboard over SSH, I’d need to make sure that my terminal emulator has OSC 52 support
  • if I wanted to use iTerm’s tmux integration (which makes tmux tabs into iTerm tabs), I’d need to change how I configure colours – right now I set them with a shell script that I run when my shell starts, but that means the colours get lost when restoring a tmux session.

and probably more things I haven’t thought of. “Using tmux means that I have to change how I manage my colours” sounds unlikely, but that really did happen to me and I decided “well, I don’t want to change how I manage colours right now, so I guess I’m not using that feature!”.

It’s also hard to remember which features I’m relying on – for example maybe my current terminal does have OSC 52 support and because copying from tmux over SSH has always Just Worked I don’t even realize that that’s something I need, and then it mysteriously stops working when I switch terminals.

change things slowly

Personally even though I think my setup is not that complicated, it’s taken me 20 years to get to this point! Because terminal config changes are so likely to have unexpected and hard-to-understand consequences, I’ve found that if I change a lot of terminal configuration all at once it makes it much harder to understand what went wrong if there’s a problem, which can be really disorienting.

So I usually prefer to make pretty small changes, and accept that changes can might take me a REALLY long time to get used to. For example I switched from using ls to eza a year or two ago and while I like it (because eza -l prints human-readable file sizes by default) I’m still not quite sure about it. But also sometimes it’s worth it to make a big change, like I made the switch to fish (from bash) 10 years ago and I’m very happy I did.

getting a “modern” terminal is not that easy

Trying to explain how “easy” it is to configure your terminal really just made me think that it’s kind of hard and that I still sometimes get confused.

I’ve found that there’s never one perfect way to configure things in the terminal that will be compatible with every single other thing. I just need to try stuff, figure out some kind of locally stable state that works for me, and accept that if I start using a new tool it might disrupt the system and I might need to rethink things.

2024-12-12T09:28:22+00:00 Fullscreen Open in Tab
"Rules" that terminal programs follow

Recently I’ve been thinking about how everything that happens in the terminal is some combination of:

  1. Your operating system’s job
  2. Your shell’s job
  3. Your terminal emulator’s job
  4. The job of whatever program you happen to be running (like top or vim or cat)

The first three (your operating system, shell, and terminal emulator) are all kind of known quantities – if you’re using bash in GNOME Terminal on Linux, you can more or less reason about how how all of those things interact, and some of their behaviour is standardized by POSIX.

But the fourth one (“whatever program you happen to be running”) feels like it could do ANYTHING. How are you supposed to know how a program is going to behave?

This post is kind of long so here’s a quick table of contents:

programs behave surprisingly consistently

As far as I know, there are no real standards for how programs in the terminal should behave – the closest things I know of are:

  • POSIX, which mostly dictates how your terminal emulator / OS / shell should work together. I think it does specify a few things about how core utilities like cp should work but AFAIK it doesn’t have anything to say about how for example htop should behave.
  • these command line interface guidelines

But even though there are no standards, in my experience programs in the terminal behave in a pretty consistent way. So I wanted to write down a list of “rules” that in my experience programs mostly follow.

these are meant to be descriptive, not prescriptive

My goal here isn’t to convince authors of terminal programs that they should follow any of these rules. There are lots of exceptions to these and often there’s a good reason for those exceptions.

But it’s very useful for me to know what behaviour to expect from a random new terminal program that I’m using. Instead of “uh, programs could do literally anything”, it’s “ok, here are the basic rules I expect, and then I can keep a short mental list of exceptions”.

So I’m just writing down what I’ve observed about how programs behave in my 20 years of using the terminal, why I think they behave that way, and some examples of cases where that rule is “broken”.

it’s not always obvious which “rules” are the program’s responsibility to implement

There are a bunch of common conventions that I think are pretty clearly the program’s responsibility to implement, like:

  • config files should go in ~/.BLAHrc or ~/.config/BLAH/FILE or /etc/BLAH/ or something
  • --help should print help text
  • programs should print “regular” output to stdout and errors to stderr

But in this post I’m going to focus on things that it’s not 100% obvious are the program’s responsibility. For example it feels to me like a “law of nature” that pressing Ctrl-D should quit a REPL, but programs often need to explicitly implement support for it – even though cat doesn’t need to implement Ctrl-D support, ipython does. (more about that in “rule 3” below)

Understanding which things are the program’s responsibility makes it much less surprising when different programs’ implementations are slightly different.

rule 1: noninteractive programs should quit when you press Ctrl-C

The main reason for this rule is that noninteractive programs will quit by default on Ctrl-C if they don’t set up a SIGINT signal handler, so this is kind of a “you should act like the default” rule.

Something that trips a lot of people up is that this doesn’t apply to interactive programs like python3 or bc or less. This is because in an interactive program, Ctrl-C has a different job – if the program is running an operation (like for example a search in less or some Python code in python3), then Ctrl-C will interrupt that operation but not stop the program.

As an example of how this works in an interactive program: here’s the code in prompt-toolkit (the library that iPython uses for handling input) that aborts a search when you press Ctrl-C.

rule 2: TUIs should quit when you press q

TUI programs (like less or htop) will usually quit when you press q.

This rule doesn’t apply to any program where pressing q to quit wouldn’t make sense, like tmux or text editors.

rule 3: REPLs should quit when you press Ctrl-D on an empty line

REPLs (like python3 or ed) will usually quit when you press Ctrl-D on an empty line. This rule is similar to the Ctrl-C rule – the reason for this is that by default if you’re running a program (like cat) in “cooked mode”, then the operating system will return an EOF when you press Ctrl-D on an empty line.

Most of the REPLs I use (sqlite3, python3, fish, bash, etc) don’t actually use cooked mode, but they all implement this keyboard shortcut anyway to mimic the default behaviour.

For example, here’s the code in prompt-toolkit that quits when you press Ctrl-D, and here’s the same code in readline.

I actually thought that this one was a “Law of Terminal Physics” until very recently because I’ve basically never seen it broken, but you can see that it’s just something that each individual input library has to implement in the links above.

Someone pointed out that the Erlang REPL does not quit when you press Ctrl-D, so I guess not every REPL follows this “rule”.

rule 4: don’t use more than 16 colours

Terminal programs rarely use colours other than the base 16 ANSI colours. This is because if you specify colours with a hex code, it’s very likely to clash with some users’ background colour. For example if I print out some text as #EEEEEE, it would be almost invisible on a white background, though it would look fine on a dark background.

But if you stick to the default 16 base colours, you have a much better chance that the user has configured those colours in their terminal emulator so that they work reasonably well with their background color. Another reason to stick to the default base 16 colours is that it makes less assumptions about what colours the terminal emulator supports.

The only programs I usually see breaking this “rule” are text editors, for example Helix by default will use a purple background which is not a default ANSI colour. It seems fine for Helix to break this rule since Helix isn’t a “core” program and I assume any Helix user who doesn’t like that colorscheme will just change the theme.

rule 5: vaguely support readline keybindings

Almost every program I use supports readline keybindings if it would make sense to do so. For example, here are a bunch of different programs and a link to where they define Ctrl-E to go to the end of the line:

None of those programs actually uses readline directly, they just sort of mimic emacs/readline keybindings. They don’t always mimic them exactly: for example atuin seems to use Ctrl-A as a prefix, so Ctrl-A doesn’t go to the beginning of the line.

Also all of these programs seem to implement their own internal cut and paste buffers so you can delete a line with Ctrl-U and then paste it with Ctrl-Y.

The exceptions to this are:

  • some programs (like git, cat, and nc) don’t have any line editing support at all (except for backspace, Ctrl-W, and Ctrl-U)
  • as usual text editors are an exception, every text editor has its own approach to editing text

I wrote more about this “what keybindings does a program support?” question in entering text in the terminal is complicated.

rule 5.1: Ctrl-W should delete the last word

I’ve never seen a program (other than a text editor) where Ctrl-W doesn’t delete the last word. This is similar to the Ctrl-C rule – by default if a program is in “cooked mode”, the OS will delete the last word if you press Ctrl-W, and delete the whole line if you press Ctrl-U. So usually programs will imitate that behaviour.

I can’t think of any exceptions to this other than text editors but if there are I’d love to hear about them!

rule 6: disable colours when writing to a pipe

Most programs will disable colours when writing to a pipe. For example:

  • rg blah will highlight all occurrences of blah in the output, but if the output is to a pipe or a file, it’ll turn off the highlighting.
  • ls --color=auto will use colour when writing to a terminal, but not when writing to a pipe

Both of those programs will also format their output differently when writing to the terminal: ls will organize files into columns, and ripgrep will group matches with headings.

If you want to force the program to use colour (for example because you want to look at the colour), you can use unbuffer to force the program’s output to be a tty like this:

unbuffer rg blah |  less -R

I’m sure that there are some programs that “break” this rule but I can’t think of any examples right now. Some programs have an --color flag that you can use to force colour to be on, in the example above you could also do rg --color=always | less -R.

rule 7: - means stdin/stdout

Usually if you pass - to a program instead of a filename, it’ll read from stdin or write to stdout (whichever is appropriate). For example, if you want to format the Python code that’s on your clipboard with black and then copy it, you could run:

pbpaste | black - | pbcopy

(pbpaste is a Mac program, you can do something similar on Linux with xclip)

My impression is that most programs implement this if it would make sense and I can’t think of any exceptions right now, but I’m sure there are many exceptions.

these “rules” take a long time to learn

These rules took me a long time for me to learn because I had to:

  1. learn that the rule applied anywhere at all ("Ctrl-C will exit programs")
  2. notice some exceptions (“okay, Ctrl-C will exit find but not less”)
  3. subconsciously figure out what the pattern is ("Ctrl-C will generally quit noninteractive programs, but in interactive programs it might interrupt the current operation instead of quitting the program")
  4. eventually maybe formulate it into an explicit rule that I know

A lot of my understanding of the terminal is honestly still in the “subconscious pattern recognition” stage. The only reason I’ve been taking the time to make things explicit at all is because I’ve been trying to explain how it works to others. Hopefully writing down these “rules” explicitly will make learning some of this stuff a little bit faster for others.

2024-11-29T08:23:31+00:00 Fullscreen Open in Tab
Why pipes sometimes get "stuck": buffering

Here’s a niche terminal problem that has bothered me for years but that I never really understood until a few weeks ago. Let’s say you’re running this command to watch for some specific output in a log file:

tail -f /some/log/file | grep thing1 | grep thing2

If log lines are being added to the file relatively slowly, the result I’d see is… nothing! It doesn’t matter if there were matches in the log file or not, there just wouldn’t be any output.

I internalized this as “uh, I guess pipes just get stuck sometimes and don’t show me the output, that’s weird”, and I’d handle it by just running grep thing1 /some/log/file | grep thing2 instead, which would work.

So as I’ve been doing a terminal deep dive over the last few months I was really excited to finally learn exactly why this happens.

why this happens: buffering

The reason why “pipes get stuck” sometimes is that it’s VERY common for programs to buffer their output before writing it to a pipe or file. So the pipe is working fine, the problem is that the program never even wrote the data to the pipe!

This is for performance reasons: writing all output immediately as soon as you can uses more system calls, so it’s more efficient to save up data until you have 8KB or so of data to write (or until the program exits) and THEN write it to the pipe.

In this example:

tail -f /some/log/file | grep thing1 | grep thing2

the problem is that grep thing1 is saving up all of its matches until it has 8KB of data to write, which might literally never happen.

programs don’t buffer when writing to a terminal

Part of why I found this so disorienting is that tail -f file | grep thing will work totally fine, but then when you add the second grep, it stops working!! The reason for this is that the way grep handles buffering depends on whether it’s writing to a terminal or not.

Here’s how grep (and many other programs) decides to buffer its output:

  • Check if stdout is a terminal or not using the isatty function
    • If it’s a terminal, use line buffering (print every line immediately as soon as you have it)
    • Otherwise, use “block buffering” – only print data if you have at least 8KB or so of data to print

So if grep is writing directly to your terminal then you’ll see the line as soon as it’s printed, but if it’s writing to a pipe, you won’t.

Of course the buffer size isn’t always 8KB for every program, it depends on the implementation. For grep the buffering is handled by libc, and libc’s buffer size is defined in the BUFSIZ variable. Here’s where that’s defined in glibc.

(as an aside: “programs do not use 8KB output buffers when writing to a terminal” isn’t, like, a law of terminal physics, a program COULD use an 8KB buffer when writing output to a terminal if it wanted, it would just be extremely weird if it did that, I can’t think of any program that behaves that way)

commands that buffer & commands that don’t

One annoying thing about this buffering behaviour is that you kind of need to remember which commands buffer their output when writing to a pipe.

Some commands that don’t buffer their output:

  • tail
  • cat
  • tee

I think almost everything else will buffer output, especially if it’s a command where you’re likely to be using it for batch processing. Here’s a list of some common commands that buffer their output when writing to a pipe, along with the flag that disables block buffering.

  • grep (--line-buffered)
  • sed (-u)
  • awk (there’s a fflush() function)
  • tcpdump (-l)
  • jq (-u)
  • tr (-u)
  • cut (can’t disable buffering)

Those are all the ones I can think of, lots of unix commands (like sort) may or may not buffer their output but it doesn’t matter because sort can’t do anything until it finishes receiving input anyway.

Also I did my best to test both the Mac OS and GNU versions of these but there are a lot of variations and I might have made some mistakes.

programming languages where the default “print” statement buffers

Also, here are a few programming language where the default print statement will buffer output when writing to a pipe, and some ways to disable buffering if you want:

  • C (disable with setvbuf)
  • Python (disable with python -u, or PYTHONUNBUFFERED=1, or sys.stdout.reconfigure(line_buffering=False), or print(x, flush=True))
  • Ruby (disable with STDOUT.sync = true)
  • Perl (disable with $| = 1)

I assume that these languages are designed this way so that the default print function will be fast when you’re doing batch processing.

Also whether output is buffered or not might depend on how you print, for example in C++ cout << "hello\n" buffers when writing to a pipe but cout << "hello" << endl will flush its output.

when you press Ctrl-C on a pipe, the contents of the buffer are lost

Let’s say you’re running this command as a hacky way to watch for DNS requests to example.com, and you forgot to pass -l to tcpdump:

sudo tcpdump -ni any port 53 | grep example.com

When you press Ctrl-C, what happens? In a magical perfect world, what I would want to happen is for tcpdump to flush its buffer, grep would search for example.com, and I would see all the output I missed.

But in the real world, what happens is that all the programs get killed and the output in tcpdump’s buffer is lost.

I think this problem is probably unavoidable – I spent a little time with strace to see how this works and grep receives the SIGINT before tcpdump anyway so even if tcpdump tried to flush its buffer grep would already be dead.

After a little more investigation, there is a workaround: if you find tcpdump’s PID and kill -TERM $PID, then tcpdump will flush the buffer so you can see the output. That’s kind of a pain but I tested it and it seems to work.

redirecting to a file also buffers

It’s not just pipes, this will also buffer:

sudo tcpdump -ni any port 53 > output.txt

Redirecting to a file doesn’t have the same “Ctrl-C will totally destroy the contents of the buffer” problem though – in my experience it usually behaves more like you’d want, where the contents of the buffer get written to the file before the program exits. I’m not 100% sure whether this is something you can always rely on or not.

a bunch of potential ways to avoid buffering

Okay, let’s talk solutions. Let’s say you’ve run this command:

tail -f /some/log/file | grep thing1 | grep thing2

I asked people on Mastodon how they would solve this in practice and there were 5 basic approaches. Here they are:

solution 1: run a program that finishes quickly

Historically my solution to this has been to just avoid the “command writing to pipe slowly” situation completely and instead run a program that will finish quickly like this:

cat /some/log/file | grep thing1 | grep thing2 | tail

This doesn’t do the same thing as the original command but it does mean that you get to avoid thinking about these weird buffering issues.

(you could also do grep thing1 /some/log/file but I often prefer to use an “unnecessary” cat)

solution 2: remember the “line buffer” flag to grep

You could remember that grep has a flag to avoid buffering and pass it like this:

tail -f /some/log/file | grep --line-buffered thing1 | grep thing2

solution 3: use awk

Some people said that if they’re specifically dealing with a multiple greps situation, they’ll rewrite it to use a single awk instead, like this:

tail -f /some/log/file |  awk '/thing1/ && /thing2/'

Or you would write a more complicated grep, like this:

tail -f /some/log/file |  grep -E 'thing1.*thing2'

(awk also buffers, so for this to work you’ll want awk to be the last command in the pipeline)

solution 4: use stdbuf

stdbuf uses LD_PRELOAD to turn off libc’s buffering, and you can use it to turn off output buffering like this:

tail -f /some/log/file | stdbuf -o0 grep thing1 | grep thing2

Like any LD_PRELOAD solution it’s a bit unreliable – it doesn’t work on static binaries, I think won’t work if the program isn’t using libc’s buffering, and doesn’t always work on Mac OS. Harry Marr has a really nice How stdbuf works post.

solution 5: use unbuffer

unbuffer program will force the program’s output to be a TTY, which means that it’ll behave the way it normally would on a TTY (less buffering, colour output, etc). You could use it in this example like this:

tail -f /some/log/file | unbuffer grep thing1 | grep thing2

Unlike stdbuf it will always work, though it might have unwanted side effects, for example grep thing1’s will also colour matches.

If you want to install unbuffer, it’s in the expect package.

that’s all the solutions I know about!

It’s a bit hard for me to say which one is “best”, I think personally I’m mostly likely to use unbuffer because I know it’s always going to work.

If I learn about more solutions I’ll try to add them to this post.

I’m not really sure how often this comes up

I think it’s not very common for me to have a program that slowly trickles data into a pipe like this, normally if I’m using a pipe a bunch of data gets written very quickly, processed by everything in the pipeline, and then everything exits. The only examples I can come up with right now are:

  • tcpdump
  • tail -f
  • watching log files in a different way like with kubectl logs
  • the output of a slow computation

what if there were an environment variable to disable buffering?

I think it would be cool if there were a standard environment variable to turn off buffering, like PYTHONUNBUFFERED in Python. I got this idea from a couple of blog posts by Mark Dominus in 2018. Maybe NO_BUFFER like NO_COLOR?

The design seems tricky to get right; Mark points out that NETBSD has environment variables called STDBUF, STDBUF1, etc which gives you a ton of control over buffering but I imagine most developers don’t want to implement many different environment variables to handle a relatively minor edge case.

I’m also curious about whether there are any programs that just automatically flush their output buffers after some period of time (like 1 second). It feels like it would be nice in theory but I can’t think of any program that does that so I imagine there are some downsides.

stuff I left out

Some things I didn’t talk about in this post since these posts have been getting pretty long recently and seriously does anyone REALLY want to read 3000 words about buffering?

  • the difference between line buffering and having totally unbuffered output
  • how buffering to stderr is different from buffering to stdout
  • this post is only about buffering that happens inside the program, your operating system’s TTY driver also does a little bit of buffering sometimes
  • other reasons you might need to flush your output other than “you’re writing to a pipe”
2024-11-18T09:35:42+00:00 Fullscreen Open in Tab
Importing a frontend Javascript library without a build system

I like writing Javascript without a build system and for the millionth time yesterday I ran into a problem where I needed to figure out how to import a Javascript library in my code without using a build system, and it took FOREVER to figure out how to import it because the library’s setup instructions assume that you’re using a build system.

Luckily at this point I’ve mostly learned how to navigate this situation and either successfully use the library or decide it’s too difficult and switch to a different library, so here’s the guide I wish I had to importing Javascript libraries years ago.

I’m only going to talk about using Javacript libraries on the frontend, and only about how to use them in a no-build-system setup.

In this post I’m going to talk about:

  1. the three main types of Javascript files a library might provide (ES Modules, the “classic” global variable kind, and CommonJS)
  2. how to figure out which types of files a Javascript library includes in its build
  3. ways to import each type of file in your code

the three kinds of Javascript files

There are 3 basic types of Javascript files a library can provide:

  1. the “classic” type of file that defines a global variable. This is the kind of file that you can just <script src> and it’ll Just Work. Great if you can get it but not always available
  2. an ES module (which may or may not depend on other files, we’ll get to that)
  3. a “CommonJS” module. This is for Node, you can’t use it in a browser at all without using a build system.

I’m not sure if there’s a better name for the “classic” type but I’m just going to call it “classic”. Also there’s a type called “AMD” but I’m not sure how relevant it is in 2024.

Now that we know the 3 types of files, let’s talk about how to figure out which of these the library actually provides!

where to find the files: the NPM build

Every Javascript library has a build which it uploads to NPM. You might be thinking (like I did originally) – Julia! The whole POINT is that we’re not using Node to build our library! Why are we talking about NPM?

But if you’re using a link from a CDN like https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js, you’re still using the NPM build! All the files on the CDNs originally come from NPM.

Because of this, I sometimes like to npm install the library even if I’m not planning to use Node to build my library at all – I’ll just create a new temp folder, npm install there, and then delete it when I’m done. I like being able to poke around in the files in the NPM build on my filesystem, because then I can be 100% sure that I’m seeing everything that the library is making available in its build and that the CDN isn’t hiding something from me.

So let’s npm install a few libraries and try to figure out what types of Javascript files they provide in their builds!

example library 1: chart.js

First let’s look inside Chart.js, a plotting library.

$ cd /tmp/whatever
$ npm install chart.js
$ cd node_modules/chart.js/dist
$ ls *.*js
chart.cjs  chart.js  chart.umd.js  helpers.cjs  helpers.js

This library seems to have 3 basic options:

option 1: chart.cjs. The .cjs suffix tells me that this is a CommonJS file, for using in Node. This means it’s impossible to use it directly in the browser without some kind of build step.

option 2:chart.js. The .js suffix by itself doesn’t tell us what kind of file it is, but if I open it up, I see import '@kurkle/color'; which is an immediate sign that this is an ES module – the import ... syntax is ES module syntax.

option 3: chart.umd.js. “UMD” stands for “Universal Module Definition”, which I think means that you can use this file either with a basic <script src>, CommonJS, or some third thing called AMD that I don’t understand.

how to use a UMD file

When I was using Chart.js I picked Option 3. I just needed to add this to my code:

<script src="./chart.umd.js"> </script>

and then I could use the library with the global Chart environment variable. Couldn’t be easier. I just copied chart.umd.js into my Git repository so that I didn’t have to worry about using NPM or the CDNs going down or anything.

the build files aren’t always in the dist directory

A lot of libraries will put their build in the dist directory, but not always! The build files’ location is specified in the library’s package.json.

For example here’s an excerpt from Chart.js’s package.json.

  "jsdelivr": "./dist/chart.umd.js",
  "unpkg": "./dist/chart.umd.js",
  "main": "./dist/chart.cjs",
  "module": "./dist/chart.js",

I think this is saying that if you want to use an ES Module (module) you should use dist/chart.js, but the jsDelivr and unpkg CDNs should use ./dist/chart.umd.js. I guess main is for Node.

chart.js’s package.json also says "type": "module", which according to this documentation tells Node to treat files as ES modules by default. I think it doesn’t tell us specifically which files are ES modules and which ones aren’t but it does tell us that something in there is an ES module.

example library 2: @atcute/oauth-browser-client

@atcute/oauth-browser-client is a library for logging into Bluesky with OAuth in the browser.

Let’s see what kinds of Javascript files it provides in its build!

$ npm install @atcute/oauth-browser-client
$ cd node_modules/@atcute/oauth-browser-client/dist
$ ls *js
constants.js  dpop.js  environment.js  errors.js  index.js  resolvers.js

It seems like the only plausible root file in here is index.js, which looks something like this:

export { configureOAuth } from './environment.js';
export * from './errors.js';
export * from './resolvers.js';

This export syntax means it’s an ES module. That means we can use it in the browser without a build step! Let’s see how to do that.

how to use an ES module with importmaps

Using an ES module isn’t an easy as just adding a <script src="whatever.js">. Instead, if the ES module has dependencies (like @atcute/oauth-browser-client does) the steps are:

  1. Set up an import map in your HTML
  2. Put import statements like import { configureOAuth } from '@atcute/oauth-browser-client'; in your JS code
  3. Include your JS code in your HTML like this: <script type="module" src="YOURSCRIPT.js"></script>

The reason we need an import map instead of just doing something like import { BrowserOAuthClient } from "./oauth-client-browser.js" is that internally the module has more import statements like import {something} from @atcute/client, and we need to tell the browser where to get the code for @atcute/client and all of its other dependencies.

Here’s what the importmap I used looks like for @atcute/oauth-browser-client:

<script type="importmap">
{
  "imports": {
    "nanoid": "./node_modules/nanoid/bin/dist/index.js",
    "nanoid/non-secure": "./node_modules/nanoid/non-secure/index.js",
    "nanoid/url-alphabet": "./node_modules/nanoid/url-alphabet/dist/index.js",
    "@atcute/oauth-browser-client": "./node_modules/@atcute/oauth-browser-client/dist/index.js",
    "@atcute/client": "./node_modules/@atcute/client/dist/index.js",
    "@atcute/client/utils/did": "./node_modules/@atcute/client/dist/utils/did.js"
  }
}
</script>

Getting these import maps to work is pretty fiddly, I feel like there must be a tool to generate them automatically but I haven’t found one yet. It’s definitely possible to write a script that automatically generates the importmaps using esbuild’s metafile but I haven’t done that and maybe there’s a better way.

I decided to set up importmaps yesterday to get github.com/jvns/bsky-oauth-example to work, so there’s some example code in that repo.

Also someone pointed me to Simon Willison’s download-esm, which will download an ES module and rewrite the imports to point to the JS files directly so that you don’t need importmaps. I haven’t tried it yet but it seems like a great idea.

problems with importmaps: too many files

I did run into some problems with using importmaps in the browser though – it needed to download dozens of Javascript files to load my site, and my webserver in development couldn’t keep up for some reason. I kept seeing files fail to load randomly and then had to reload the page and hope that they would succeed this time.

It wasn’t an issue anymore when I deployed my site to production, so I guess it was a problem with my local dev environment.

Also one slightly annoying thing about ES modules in general is that you need to be running a webserver to use them, I’m sure this is for a good reason but it’s easier when you can just open your index.html file without starting a webserver.

Because of the “too many files” thing I think actually using ES modules with importmaps in this way isn’t actually that appealing to me, but it’s good to know it’s possible.

how to use an ES module without importmaps

If the ES module doesn’t have dependencies then it’s even easier – you don’t need the importmaps! You can just:

  • put <script type="module" src="YOURCODE.js"></script> in your HTML. The type="module" is important.
  • put import {whatever} from "https://example.com/whatever.js" in YOURCODE.js

alternative: use esbuild

If you don’t want to use importmaps, you can also use a build system like esbuild. I talked about how to do that in Some notes on using esbuild, but this blog post is about ways to avoid build systems completely so I’m not going to talk about that option here. I do still like esbuild though and I think it’s a good option in this case.

what’s the browser support for importmaps?

CanIUse says that importmaps are in “Baseline 2023: newly available across major browsers” so my sense is that in 2024 that’s still maybe a little bit too new? I think I would use importmaps for some fun experimental code that I only wanted like myself and 12 people to use, but if I wanted my code to be more widely usable I’d use esbuild instead.

example library 3: @atproto/oauth-client-browser

Let’s look at one final example library! This is a different Bluesky auth library than @atcute/oauth-browser-client.

$ npm install @atproto/oauth-client-browser
$ cd node_modules/@atproto/oauth-client-browser/dist
$ ls *js
browser-oauth-client.js  browser-oauth-database.js  browser-runtime-implementation.js  errors.js  index.js  indexed-db-store.js  util.js

Again, it seems like only real candidate file here is index.js. But this is a different situation from the previous example library! Let’s take a look at index.js:

There’s a bunch of stuff like this in index.js:

__exportStar(require("@atproto/oauth-client"), exports);
__exportStar(require("./browser-oauth-client.js"), exports);
__exportStar(require("./errors.js"), exports);
var util_js_1 = require("./util.js");

This require() syntax is CommonJS syntax, which means that we can’t use this file in the browser at all, we need to use some kind of build step, and ESBuild won’t work either.

Also in this library’s package.json it says "type": "commonjs" which is another way to tell it’s CommonJS.

how to use a CommonJS module with esm.sh

Originally I thought it was impossible to use CommonJS modules without learning a build system, but then someone Bluesky told me about esm.sh! It’s a CDN that will translate anything into an ES Module. skypack.dev does something similar, I’m not sure what the difference is but one person mentioned that if one doesn’t work sometimes they’ll try the other one.

For @atproto/oauth-client-browser using it seems pretty simple, I just need to put this in my HTML:

<script type="module" src="script.js"> </script>

and then put this in script.js.

import { BrowserOAuthClient } from "https://esm.sh/@atproto/oauth-client-browser@0.3.0"

It seems to Just Work, which is cool! Of course this is still sort of using a build system – it’s just that esm.sh is running the build instead of me. My main concerns with this approach are:

  • I don’t really trust CDNs to keep working forever – usually I like to copy dependencies into my repository so that they don’t go away for some reason in the future.
  • I’ve heard of some issues with CDNs having security compromises which scares me.
  • I don’t really understand what esm.sh is doing.

esbuild can also convert CommonJS modules into ES modules

I also learned that you can also use esbuild to convert a CommonJS module into an ES module, though there are some limitations – the import { BrowserOAuthClient } from syntax doesn’t work. Here’s a github issue about that.

I think the esbuild approach is probably more appealing to me than the esm.sh approach because it’s a tool that I already have on my computer so I trust it more. I haven’t experimented with this much yet though.

summary of the three types of files

Here’s a summary of the three types of JS files you might encounter, options for how to use them, and how to identify them.

Unhelpfully a .js or .min.js file extension could be any of these 3 options, so if the file is something.js you need to do more detective work to figure out what you’re dealing with.

  1. “classic” JS files
    • How to use it:: <script src="whatever.js"></script>
    • Ways to identify it:
      • The website has a big friendly banner in its setup instructions saying “Use this with a CDN!” or something
      • A .umd.js extension
      • Just try to put it in a <script src=... tag and see if it works
  2. ES Modules
    • Ways to use it:
      • If there are no dependencies, just import {whatever} from "./my-module.js" directly in your code
      • If there are dependencies, create an importmap and import {whatever} from "my-module"
      • Use esbuild or any ES Module bundler
    • Ways to identify it:
      • Look for an import or export statement. (not module.exports = ..., that’s CommonJS)
      • An .mjs extension
      • maybe "type": "module" in package.json (though it’s not clear to me which file exactly this refers to)
  3. CommonJS Modules
    • Ways to use it:
      • Use https://esm.sh to convert it into an ES module, like https://esm.sh/@atproto/oauth-client-browser@0.3.0
      • Use a build somehow (??)
    • Ways to identify it:
      • Look for require() or module.exports = ... in the code
      • A .cjs extension
      • maybe "type": "commonjs" in package.json (though it’s not clear to me which file exactly this refers to)

it’s really nice to have ES modules standardized

The main difference between CommonJS modules and ES modules from my perspective is that ES modules are actually a standard. This makes me feel a lot more confident using them, because browsers commit to backwards compatibility for web standards forever – if I write some code using ES modules today, I can feel sure that it’ll still work the same way in 15 years.

It also makes me feel better about using tooling like esbuild because even if the esbuild project dies, because it’s implementing a standard it feels likely that there will be another similar tool in the future that I can replace it with.

the JS community has built a lot of very cool tools

A lot of the time when I talk about this stuff I get responses like “I hate javascript!!! it’s the worst!!!”. But my experience is that there are a lot of great tools for Javascript (I just learned about https://esm.sh yesterday which seems great! I love esbuild!), and that if I take the time to learn how things works I can take advantage of some of those tools and make my life a lot easier.

So the goal of this post is definitely not to complain about Javascript, it’s to understand the landscape so I can use the tooling in a way that feels good to me.

questions I still have

Here are some questions I still have, I’ll add the answers into the post if I learn the answer.

  • Is there a tool that automatically generates importmaps for an ES Module that I have set up locally? (apparently yes: jspm)
  • How can I convert a CommonJS module into an ES module on my computer, the way https://esm.sh does? (apparently esbuild can sort of do this, though named exports don’t work)
  • When people normally build CommonJS modules into regular JS code, what’s code is doing that? Obviously there are tools like webpack, rollup, esbuild, etc, but do those tools all implement their own JS parsers/static analysis? How many JS parsers are there out there?
  • Is there any way to bundle an ES module into a single file (like atcute-client.js), but so that in the browser I can still import multiple different paths from that file (like both @atcute/client/lexicons and @atcute/client)?

all the tools

Here’s a list of every tool we talked about in this post:

Writing this post has made me think that even though I usually don’t want to have a build that I run every time I update the project, I might be willing to have a build step (using download-esm or something) that I run only once when setting up the project and never run again except maybe if I’m updating my dependency versions.

that’s all!

Thanks to Marco Rogers who taught me a lot of the things in this post. I’ve probably made some mistakes in this post and I’d love to know what they are – let me know on Bluesky or Mastodon!

2024-11-09T09:24:29+00:00 Fullscreen Open in Tab
New microblog with TILs

I added a new section to this site a couple weeks ago called TIL (“today I learned”).

the goal: save interesting tools & facts I posted on social media

One kind of thing I like to post on Mastodon/Bluesky is “hey, here’s a cool thing”, like the great SQLite repl litecli, or the fact that cross compiling in Go Just Works and it’s amazing, or cryptographic right answers, or this great diff tool. Usually I don’t want to write a whole blog post about those things because I really don’t have much more to say than “hey this is useful!”

It started to bother me that I didn’t have anywhere to put those things: for example recently I wanted to use diffdiff and I just could not remember what it was called.

the solution: make a new section of this blog

So I quickly made a new folder called /til/, added some custom styling (I wanted to style the posts to look a little bit like a tweet), made a little Rake task to help me create new posts quickly (rake new_til), and set up a separate RSS Feed for it.

I think this new section of the blog might be more for myself than anything, now when I forget the link to Cryptographic Right Answers I can hopefully look it up on the TIL page. (you might think “julia, why not use bookmarks??” but I have been failing to use bookmarks for my whole life and I don’t see that changing ever, putting things in public is for whatever reason much easier for me)

So far it’s been working, often I can actually just make a quick post in 2 minutes which was the goal.

inspired by Simon Willison’s TIL blog

My page is inspired by Simon Willison’s great TIL blog, though my TIL posts are a lot shorter.

I don’t necessarily want everything to be archived

This came about because I spent a lot of time on Twitter, so I’ve been thinking about what I want to do about all of my tweets.

I keep reading the advice to “POSSE” (“post on your own site, syndicate elsewhere”), and while I find the idea appealing in principle, for me part of the appeal of social media is that it’s a little bit ephemeral. I can post polls or questions or observations or jokes and then they can just kind of fade away as they become less relevant.

I find it a lot easier to identify specific categories of things that I actually want to have on a Real Website That I Own:

and then let everything else be kind of ephemeral.

I really believe in the advice to make email lists though – the first two (blog posts & comics) both have email lists and RSS feeds that people can subscribe to if they want. I might add a quick summary of any TIL posts from that week to the “blog posts from this week” mailing list.

2024-11-04T09:18:03+00:00 Fullscreen Open in Tab
My IETF 121 Agenda

Here's where you can find me at IETF 121 in Dublin!

Monday

Tuesday

  • 9:30 - 11:30 • oauth
  • 13:00 - 14:30 • spice
  • 16:30 - 17:30 • scim

Thursday

Get in Touch

My Current Drafts

2024-10-31T08:00:10+00:00 Fullscreen Open in Tab
ASCII control characters in my terminal

Hello! I’ve been thinking about the terminal a lot and yesterday I got curious about all these “control codes”, like Ctrl-A, Ctrl-C, Ctrl-W, etc. What’s the deal with all of them?

a table of ASCII control characters

Here’s a table of all 33 ASCII control characters, and what they do on my machine (on Mac OS), more or less. There are about a million caveats, but I’ll talk about what it means and all the problems with this diagram that I know about.

You can also view it as an HTML page (I just made it an image so it would show up in RSS).

different kinds of codes are mixed together

The first surprising thing about this diagram to me is that there are 33 control codes, split into (very roughly speaking) these categories:

  1. Codes that are handled by the operating system’s terminal driver, for example when the OS sees a 3 (Ctrl-C), it’ll send a SIGINT signal to the current program
  2. Everything else is passed through to the application as-is and the application can do whatever it wants with them. Some subcategories of those:
    • Codes that correspond to a literal keypress of a key on your keyboard (Enter, Tab, Backspace). For example when you press Enter, your terminal gets sent 13.
    • Codes used by readline: “the application can do whatever it wants” often means “it’ll do more or less what the readline library does, whether the application actually uses readline or not”, so I’ve labelled a bunch of the codes that readline uses
    • Other codes, for example I think Ctrl-X has no standard meaning in the terminal in general but emacs uses it very heavily

There’s no real structure to which codes are in which categories, they’re all just kind of randomly scattered because this evolved organically.

(If you’re curious about readline, I wrote more about readline in entering text in the terminal is complicated, and there are a lot of cheat sheets out there)

there are only 33 control codes

Something else that I find a little surprising is that are only 33 control codes – A to Z, plus 7 more (@, [, \, ], ^, _, ?). This means that if you want to have for example Ctrl-1 as a keyboard shortcut in a terminal application, that’s not really meaningful – on my machine at least Ctrl-1 is exactly the same thing as just pressing 1, Ctrl-3 is the same as Ctrl-[, etc.

Also Ctrl+Shift+C isn’t a control code – what it does depends on your terminal emulator. On Linux Ctrl-Shift-X is often used by the terminal emulator to copy or open a new tab or paste for example, it’s not sent to the TTY at all.

Also I use Ctrl+Left Arrow all the time, but that isn’t a control code, instead it sends an ANSI escape sequence (ctrl-[[1;5D) which is a different thing which we absolutely do not have space for in this post.

This “there are only 33 codes” thing is totally different from how keyboard shortcuts work in a GUI where you can have Ctrl+KEY for any key you want.

the official ASCII names aren’t very meaningful to me

Each of these 33 control codes has a name in ASCII (for example 3 is ETX). When all of these control codes were originally defined, they weren’t being used for computers or terminals at all, they were used for the telegraph machine. Telegraph machines aren’t the same as UNIX terminals so a lot of the codes were repurposed to mean something else.

Personally I don’t find these ASCII names very useful, because 50% of the time the name in ASCII has no actual relationship to what that code does on UNIX systems today. So it feels easier to just ignore the ASCII names completely instead of trying to figure which ones still match their original meaning.

It’s hard to use Ctrl-M as a keyboard shortcut

Another thing that’s a bit weird is that Ctrl-M is literally the same as Enter, and Ctrl-I is the same as Tab, which makes it hard to use those two as keyboard shortcuts.

From some quick research, it seems like some folks do still use Ctrl-I and Ctrl-M as keyboard shortcuts (here’s an example), but to do that you need to configure your terminal emulator to treat them differently than the default.

For me the main takeaway is that if I ever write a terminal application I should avoid Ctrl-I and Ctrl-M as keyboard shortcuts in it.

how to identify what control codes get sent

While writing this I needed to do a bunch of experimenting to figure out what various key combinations did, so I wrote this Python script echo-key.py that will print them out.

There’s probably a more official way but I appreciated having a script I could customize.

caveat: on canonical vs noncanonical mode

Two of these codes (Ctrl-W and Ctrl-U) are labelled in the table as “handled by the OS”, but actually they’re not always handled by the OS, it depends on whether the terminal is in “canonical” mode or in “noncanonical mode”.

In canonical mode, programs only get input when you press Enter (and the OS is in charge of deleting characters when you press Backspace or Ctrl-W). But in noncanonical mode the program gets input immediately when you press a key, and the Ctrl-W and Ctrl-U codes are passed through to the program to handle any way it wants.

Generally in noncanonical mode the program will handle Ctrl-W and Ctrl-U similarly to how the OS does, but there are some small differences.

Some examples of programs that use canonical mode:

  • probably pretty much any noninteractive program, like grep or cat
  • git, I think

Examples of programs that use noncanonical mode:

  • python3, irb and other REPLs
  • your shell
  • any full screen TUI like less or vim

caveat: all of the “OS terminal driver” codes are configurable with stty

I said that Ctrl-C sends SIGINT but technically this is not necessarily true, if you really want to you can remap all of the codes labelled “OS terminal driver”, plus Backspace, using a tool called stty, and you can view the mappings with stty -a.

Here are the mappings on my machine right now:

$ stty -a
cchars: discard = ^O; dsusp = ^Y; eof = ^D; eol = <undef>;
	eol2 = <undef>; erase = ^?; intr = ^C; kill = ^U; lnext = ^V;
	min = 1; quit = ^\; reprint = ^R; start = ^Q; status = ^T;
	stop = ^S; susp = ^Z; time = 0; werase = ^W;

I have personally never remapped any of these and I cannot imagine a reason I would (I think it would be a recipe for confusion and disaster for me), but I asked on Mastodon and people said the most common reasons they used stty were:

  • fix a broken terminal with stty sane
  • set stty erase ^H to change how Backspace works
  • set stty ixoff
  • some people even map SIGINT to a different key, like their DELETE key

caveat: on signals

Two signals caveats:

  1. If the ISIG terminal mode is turned off, then the OS won’t send signals. For example vim turns off ISIG
  2. Apparently on BSDs, there’s an extra control code (Ctrl-T) which sends SIGINFO

You can see which terminal modes a program is setting using strace like this, terminal modes are set with the ioctl system call:

$ strace -tt -o out  vim
$ grep ioctl out | grep SET

here are the modes vim sets when it starts (ISIG and ICANON are missing!):

17:43:36.670636 ioctl(0, TCSETS, {c_iflag=IXANY|IMAXBEL|IUTF8,
c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST, c_cflag=B38400|CS8|CREAD,
c_lflag=ECHOK|ECHOCTL|ECHOKE|PENDIN, ...}) = 0

and it resets the modes when it exits:

17:43:38.027284 ioctl(0, TCSETS, {c_iflag=ICRNL|IXANY|IMAXBEL|IUTF8,
c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST|ONLCR, c_cflag=B38400|CS8|CREAD,
c_lflag=ISIG|ICANON|ECHO|ECHOE|ECHOK|IEXTEN|ECHOCTL|ECHOKE|PENDIN, ...}) = 0

I think the specific combination of modes vim is using here might be called “raw mode”, man cfmakeraw talks about that.

there are a lot of conflicts

Related to “there are only 33 codes”, there are a lot of conflicts where different parts of the system want to use the same code for different things, for example by default Ctrl-S will freeze your screen, but if you turn that off then readline will use Ctrl-S to do a forward search.

Another example is that on my machine sometimes Ctrl-T will send SIGINFO and sometimes it’ll transpose 2 characters and sometimes it’ll do something completely different depending on:

  • whether the program has ISIG set
  • whether the program uses readline / imitates readline’s behaviour

caveat: on “backspace” and “other backspace”

In this diagram I’ve labelled code 127 as “backspace” and 8 as “other backspace”. Uh, what?

I think this was the single biggest topic of discussion in the replies on Mastodon – apparently there’s a LOT of history to this and I’d never heard of any of it before.

First, here’s how it works on my machine:

  1. I press the Backspace key
  2. The TTY gets sent the byte 127, which is called DEL in ASCII
  3. the OS terminal driver and readline both have 127 mapped to “backspace” (so it works both in canonical mode and noncanonical mode)
  4. The previous character gets deleted

If I press Ctrl+H, it has the same effect as Backspace if I’m using readline, but in a program without readline support (like cat for instance), it just prints out ^H.

Apparently Step 2 above is different for some folks – their Backspace key sends the byte 8 instead of 127, and so if they want Backspace to work then they need to configure the OS (using stty) to set erase = ^H.

There’s an incredible section of the Debian Policy Manual on keyboard configuration that describes how Delete and Backspace should work according to Debian policy, which seems very similar to how it works on my Mac today. My understanding (via this mastodon post) is that this policy was written in the 90s because there was a lot of confusion about what Backspace should do in the 90s and there needed to be a standard to get everything to work.

There’s a bunch more historical terminal stuff here but that’s all I’ll say for now.

there’s probably a lot more diversity in how this works

I’ve probably missed a bunch more ways that “how it works on my machine” might be different from how it works on other people’s machines, and I’ve probably made some mistakes about how it works on my machine too. But that’s all I’ve got for today.

Some more stuff I know that I’ve left out: according to stty -a Ctrl-O is “discard”, Ctrl-R is “reprint”, and Ctrl-Y is “dsusp”. I have no idea how to make those actually do anything (pressing them does not do anything obvious, and some people have told me what they used to do historically but it’s not clear to me if they have a use in 2024), and a lot of the time in practice they seem to just be passed through to the application anyway so I just labelled Ctrl-R and Ctrl-Y as readline.

not all of this is that useful to know

Also I want to say that I think the contents of this post are kind of interesting but I don’t think they’re necessarily that useful. I’ve used the terminal pretty successfully every day for the last 20 years without knowing literally any of this – I just knew what Ctrl-C, Ctrl-D, Ctrl-Z, Ctrl-R, Ctrl-L did in practice (plus maybe Ctrl-A, Ctrl-E and Ctrl-W) and did not worry about the details for the most part, and that was almost always totally fine except when I was trying to use xterm.js.

But I had fun learning about it so maybe it’ll be interesting to you too.

2024-10-27T07:47:04+00:00 Fullscreen Open in Tab
Using less memory to look up IP addresses in Mess With DNS

I’ve been having problems for the last 3 years or so where Mess With DNS periodically runs out of memory and gets OOM killed.

This hasn’t been a big priority for me: usually it just goes down for a few minutes while it restarts, and it only happens once a day at most, so I’ve just been ignoring. But last week it started actually causing a problem so I decided to look into it.

This was kind of winding road where I learned a lot so here’s a table of contents:

there’s about 100MB of memory available

I run Mess With DNS on a VM without about 465MB of RAM, which according to ps aux (the RSS column) is split up something like:

  • 100MB for PowerDNS
  • 200MB for Mess With DNS
  • 40MB for hallpass

That leaves about 110MB of memory free.

A while back I set GOMEMLIMIT to 250MB to try to make sure the garbage collector ran if Mess With DNS used more than 250MB of memory, and I think this helped but it didn’t solve everything.

the problem: OOM killing the backup script

A few weeks ago I started backing up Mess With DNS’s database for the first time using restic.

This has been working okay, but since Mess With DNS operates without much extra memory I think restic sometimes needed more memory than was available on the system, and so the backup script sometimes got OOM killed.

This was a problem because

  1. backups might be corrupted sometimes
  2. more importantly, restic takes out a lock when it runs, and so I’d have to manually do an unlock if I wanted the backups to continue working. Doing manual work like this is the #1 thing I try to avoid with all my web services (who has time for that!) so I really wanted to do something about it.

There’s probably more than one solution to this, but I decided to try to make Mess With DNS use less memory so that there was more available memory on the system, mostly because it seemed like a fun problem to try to solve.

what’s using memory: IP addresses

I’d run a memory profile of Mess With DNS a bunch of times in the past, so I knew exactly what was using most of Mess With DNS’s memory: IP addresses.

When it starts, Mess With DNS loads this database where you can look up the ASN of every IP address into memory, so that when it receives a DNS query it can take the source IP address like 74.125.16.248 and tell you that IP address belongs to GOOGLE.

This database by itself used about 117MB of memory, and a simple du told me that was too much – the original text files were only 37MB!

$ du -sh *.tsv
26M	ip2asn-v4.tsv
11M	ip2asn-v6.tsv

The way it worked originally is that I had an array of these:

type IPRange struct {
	StartIP net.IP
	EndIP   net.IP
	Num     int
	Name    string
	Country string
}

and I searched through it with a binary search to figure out if any of the ranges contained the IP I was looking for. Basically the simplest possible thing and it’s super fast, my machine can do about 9 million lookups per second.

attempt 1: use SQLite

I’ve been using SQLite recently, so my first thought was – maybe I can store all of this data on disk in an SQLite database, give the tables an index, and that’ll use less memory.

So I:

  • wrote a quick Python script using sqlite-utils to import the TSV files into an SQLite database
  • adjusted my code to select from the database instead

This did solve the initial memory goal (after a GC it now hardly used any memory at all because the table was on disk!), though I’m not sure how much GC churn this solution would cause if we needed to do a lot of queries at once. I did a quick memory profile and it seemed to allocate about 1KB of memory per lookup.

Let’s talk about the issues I ran into with using SQLite though.

problem: how to store IPv6 addresses

SQLite doesn’t have support for big integers and IPv6 addresses are 128 bits, so I decided to store them as text. I think BLOB might have been better, I originally thought BLOBs couldn’t be compared but the sqlite docs say they can.

I ended up with this schema:

CREATE TABLE ipv4_ranges (
   start_ip INTEGER NOT NULL,
   end_ip INTEGER NOT NULL,
   asn INTEGER NOT NULL,
   country TEXT NOT NULL,
   name TEXT NOT NULL
);
CREATE TABLE ipv6_ranges (
   start_ip TEXT NOT NULL,
   end_ip TEXT NOT NULL,
   asn INTEGER,
   country TEXT,
   name TEXT
);
CREATE INDEX idx_ipv4_ranges_start_ip ON ipv4_ranges (start_ip);
CREATE INDEX idx_ipv6_ranges_start_ip ON ipv6_ranges (start_ip);
CREATE INDEX idx_ipv4_ranges_end_ip ON ipv4_ranges (end_ip);
CREATE INDEX idx_ipv6_ranges_end_ip ON ipv6_ranges (end_ip);

Also I learned that Python has an ipaddress module, so I could use ipaddress.ip_address(s).exploded to make sure that the IPv6 addresses were expanded so that a string comparison would compare them properly.

problem: it’s 500x slower

I ran a quick microbenchmark, something like this. It printed out that it could look up 17,000 IPv6 addresses per second, and similarly for IPv4 addresses.

This was pretty discouraging – being able to look up 17k addresses per section is kind of fine (Mess With DNS does not get a lot of traffic), but I compared it to the original binary search code and the original code could do 9 million per second.

	ips := []net.IP{}
	count := 20000
	for i := 0; i < count; i++ {
		// create a random IPv6 address
		bytes := randomBytes()
		ip := net.IP(bytes[:])
		ips = append(ips, ip)
	}
	now := time.Now()
	success := 0
	for _, ip := range ips {
		_, err := ranges.FindASN(ip)
		if err == nil {
			success++
		}
	}
	fmt.Println(success)
	elapsed := time.Since(now)
	fmt.Println("number per second", float64(count)/elapsed.Seconds())

time for EXPLAIN QUERY PLAN

I’d never really done an EXPLAIN in sqlite, so I thought it would be a fun opportunity to see what the query plan was doing.

sqlite> explain query plan select * from ipv6_ranges where '2607:f8b0:4006:0824:0000:0000:0000:200e' BETWEEN start_ip and end_ip;
QUERY PLAN
`--SEARCH ipv6_ranges USING INDEX idx_ipv6_ranges_end_ip (end_ip>?)

It looks like it’s just using the end_ip index and not the start_ip index, so maybe it makes sense that it’s slower than the binary search.

I tried to figure out if there was a way to make SQLite use both indexes, but I couldn’t find one and maybe it knows best anyway.

At this point I gave up on the SQLite solution, I didn’t love that it was slower and also it’s a lot more complex than just doing a binary search. I felt like I’d rather keep something much more similar to the binary search.

A few things I tried with SQLite that did not cause it to use both indexes:

  • using a compound index instead of two separate indexes
  • running ANALYZE
  • using INTERSECT to intersect the results of start_ip < ? and ? < end_ip. This did make it use both indexes, but it also seemed to make the query literally 1000x slower, probably because it needed to create the results of both subqueries in memory and intersect them.

attempt 2: use a trie

My next idea was to use a trie, because I had some vague idea that maybe a trie would use less memory, and I found this library called ipaddress-go that lets you look up IP addresses using a trie.

I tried using it here’s the code, but I think I was doing something wildly wrong because, compared to my naive array + binary search:

  • it used WAY more memory (800MB to store just the IPv4 addresses)
  • it was a lot slower to do the lookups (it could do only 100K/second instead of 9 million/second)

I’m not really sure what went wrong here but I gave up on this approach and decided to just try to make my array use less memory and stick to a simple binary search.

some notes on memory profiling

One thing I learned about memory profiling is that you can use runtime package to see how much memory is currently allocated in the program. That’s how I got all the memory numbers in this post. Here’s the code:

func memusage() {
	runtime.GC()
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	fmt.Printf("Alloc = %v MiB\n", m.Alloc/1024/1024)
	// write mem.prof
	f, err := os.Create("mem.prof")
	if err != nil {
		log.Fatal(err)
	}
	pprof.WriteHeapProfile(f)
	f.Close()
}

Also I learned that if you use pprof to analyze a heap profile there are two ways to analyze it: you can pass either --alloc-space or --inuse-space to go tool pprof. I don’t know how I didn’t realize this before but alloc-space will tell you about everything that was allocated, and inuse-space will just include memory that’s currently in use.

Anyway I ran go tool pprof -pdf --inuse_space mem.prof > mem.pdf a lot. Also every time I use pprof I find myself referring to my own intro to pprof, it’s probably the blog post I wrote that I use the most often. I should add --alloc-space and --inuse-space to it.

attempt 3: make my array use less memory

I was storing my ip2asn entries like this:

type IPRange struct {
	StartIP net.IP
	EndIP   net.IP
	Num     int
	Name    string
	Country string
}

I had 3 ideas for ways to improve this:

  1. There was a lot of repetition of Name and the Country, because a lot of IP ranges belong to the same ASN
  2. net.IP is an []byte under the hood, which felt like it involved an unnecessary pointer, was there a way to inline it into the struct?
  3. Maybe I didn’t need both the start IP and the end IP, often the ranges were consecutive so maybe I could rearrange things so that I only had the start IP

idea 3.1: deduplicate the Name and Country

I figured I could store the ASN info in an array, and then just store the index into the array in my IPRange struct. Here are the structs so you can see what I mean:

type IPRange struct {
	StartIP netip.Addr
	EndIP   netip.Addr
	ASN     uint32
	Idx     uint32
}

type ASNInfo struct {
	Country string
	Name    string
}

type ASNPool struct {
	asns   []ASNInfo
	lookup map[ASNInfo]uint32
}

This worked! It brought memory usage from 117MB to 65MB – a 50MB savings. I felt good about this.

Here’s all of the code for that part.

how big are ASNs?

As an aside – I’m storing the ASN in a uint32, is that right? I looked in the ip2asn file and the biggest one seems to be 401307, though there are a few lines that say 4294901931 which is much bigger, but also are just inside the range of a uint32. So I can definitely use a uint32.

59.101.179.0	59.101.179.255	4294901931	Unknown	AS4294901931

idea 3.2: use netip.Addr instead of net.IP

It turns out that I’m not the only one who felt that net.IP was using an unnecessary amount of memory – in 2021 the folks at Tailscale released a new IP address library for Go which solves this and many other issues. They wrote a great blog post about it.

I discovered (to my delight) that not only does this new IP address library exist and do exactly what I want, it’s also now in the Go standard library as netip.Addr. Switching to netip.Addr was very easy and saved another 20MB of memory, bringing us to 46MB.

I didn’t try my third idea (remove the end IP from the struct) because I’d already been programming for long enough on a Saturday morning and I was happy with my progress.

It’s always such a great feeling when I think “hey, I don’t like this, there must be a better way” and then immediately discover that someone has already made the exact thing I want, thought about it a lot more than me, and implemented it much better than I would have.

all of this was messier in real life

Even though I tried to explain this in a simple linear way “I tried X, then I tried Y, then I tried Z”, that’s kind of a lie – I always try to take my actual debugging process (total chaos) and make it seem more linear and understandable because the reality is just too annoying to write down. It’s more like:

  • try sqlite
  • try a trie
  • second guess everything that I concluded about sqlite, go back and look at the results again
  • wait what about indexes
  • very very belatedly realize that I can use runtime to check how much memory everything is using, start doing that
  • look at the trie again, maybe I misunderstood everything
  • give up and go back to binary search
  • look at all of the numbers for tries/sqlite again to make sure I didn’t misunderstand

A note on using 512MB of memory

Someone asked why I don’t just give the VM more memory. I could very easily afford to pay for a VM with 1GB of memory, but I feel like 512MB really should be enough (and really that 256MB should be enough!) so I’d rather stay inside that constraint. It’s kind of a fun puzzle.

a few ideas from the replies

Folks had a lot of good ideas I hadn’t thought of. Recording them as inspiration if I feel like having another Fun Performance Day at some point.

  • Try Go’s unique package for the ASNPool. Someone tried this and it uses more memory, probably because Go’s pointers are 64 bits
  • Try compiling with GOARCH=386 to use 32-bit pointers to sace space (maybe in combination with using unique!)
  • It should be possible to store all of the IPv6 addresses in just 64 bits, because only the first 64 bits of the address are public
  • Interpolation search might be faster than binary search since IP addresses are numeric
  • Try the MaxMind db format with mmdbwriter or mmdbctl
  • Tailscale’s art routing table package

the result: saved 70MB of memory!

I deployed the new version and now Mess With DNS is using less memory! Hooray!

A few other notes:

  • lookups are a little slower – in my microbenchmark they went from 9 million lookups/second to 6 million, maybe because I added a little indirection. Using less memory and a little more CPU seemed like a good tradeoff though.
  • it’s still using more memory than the raw text files do (46MB vs 37MB), I guess pointers take up space and that’s okay.

I’m honestly not sure if this will solve all my memory problems, probably not! But I had fun, I learned a few things about SQLite, I still don’t know what to think about tries, and it made me love binary search even more than I already did.

2024-10-07T09:19:57+00:00 Fullscreen Open in Tab
Some notes on upgrading Hugo

Warning: this is a post about very boring yakshaving, probably only of interest to people who are trying to upgrade Hugo from a very old version to a new version. But what are blogs for if not documenting one’s very boring yakshaves from time to time?

So yesterday I decided to try to upgrade Hugo. There’s no real reason to do this – I’ve been using Hugo version 0.40 to generate this blog since 2018, it works fine, and I don’t have any problems with it. But I thought – maybe it won’t be as hard as I think, and I kind of like a tedious computer task sometimes!

I thought I’d document what I learned along the way in case it’s useful to anyone else doing this very specific migration. I upgraded from Hugo v0.40 (from 2018) to v0.135 (from 2024).

Here are most of the changes I had to make:

change 1: template "theme/partials/thing.html is now partial thing.html

I had to replace a bunch of instances of {{ template "theme/partials/header.html" . }} with {{ partial "header.html" . }}.

This happened in v0.42:

We have now virtualized the filesystems for project and theme files. This makes everything simpler, faster and more powerful. But it also means that template lookups on the form {{ template “theme/partials/pagination.html” . }} will not work anymore. That syntax has never been documented, so it’s not expected to be in wide use.

change 2: .Data.Pages is now site.RegularPages

This seems to be discussed in the release notes for 0.57.2

I just needed to replace .Data.Pages with site.RegularPages in the template on the homepage as well as in my RSS feed template.

change 3: .Next and .Prev got flipped

I had this comment in the part of my theme where I link to the next/previous blog post:

“next” and “previous” in hugo apparently mean the opposite of what I’d think they’d mean intuitively. I’d expect “next” to mean “in the future” and “previous” to mean “in the past” but it’s the opposite

It looks they changed this in ad705aac064 so that “next” actually is in the future and “prev” actually is in the past. I definitely find the new behaviour more intuitive.

downloading the Hugo changelogs with a script

Figuring out why/when all of these changes happened was a little difficult. I ended up hacking together a bash script to download all of the changelogs from github as text files, which I could then grep to try to figure out what happened. It turns out it’s pretty easy to get all of the changelogs from the GitHub API.

So far everything was not so bad – there was also a change around taxonomies that’s I can’t quite explain, but it was all pretty manageable, but then we got to the really tough one: the markdown renderer.

change 4: the markdown renderer (blackfriday -> goldmark)

The blackfriday markdown renderer (which was previously the default) was removed in v0.100.0. This seems pretty reasonable:

It has been deprecated for a long time, its v1 version is not maintained anymore, and there are many known issues. Goldmark should be a mature replacement by now.

Fixing all my Markdown changes was a huge pain – I ended up having to update 80 different Markdown files (out of 700) so that they would render properly, and I’m not totally sure

why bother switching renderers?

The obvious question here is – why bother even trying to upgrade Hugo at all if I have to switch Markdown renderers? My old site was running totally fine and I think it wasn’t necessarily a good use of time, but the one reason I think it might be useful in the future is that the new renderer (goldmark) uses the CommonMark markdown standard, which I’m hoping will be somewhat more futureproof. So maybe I won’t have to go through this again? We’ll see.

Also it turned out that the new Goldmark renderer does fix some problems I had (but didn’t know that I had) with smart quotes and how lists/blockquotes interact.

finding all the Markdown problems: the process

The hard part of this Markdown change was even figuring out what changed. Almost all of the problems (including #2 and #3 above) just silently broke the site, they didn’t cause any errors or anything. So I had to diff the HTML to hunt them down.

Here’s what I ended up doing:

  1. Generate the site with the old version, put it in public_old
  2. Generate the new version, put it in public
  3. Diff every single HTML file in public/ and public_old with this diff.sh script and put the results in a diffs/ folder
  4. Run variations on find diffs -type f | xargs cat | grep -C 5 '(31m|32m)' | less -r over and over again to look at every single change until I found something that seemed wrong
  5. Update the Markdown to fix the problem
  6. Repeat until everything seemed okay

(the grep 31m|32m thing is searching for red/green text in the diff)

This was very time consuming but it was a little bit fun for some reason so I kept doing it until it seemed like nothing too horrible was left.

the new markdown rules

Here’s a list of every type of Markdown change I had to make. It’s very possible these are all extremely specific to me but it took me a long time to figure them all out so maybe this will be helpful to one other person who finds this in the future.

4.1: mixing HTML and markdown

This doesn’t work anymore (it doesn’t expand the link):

<small>
[a link](https://example.com)
</small>

I need to do this instead:

<small>

[a link](https://example.com)

</small>

This works too:

<small> [a link](https://example.com) </small>

4.2: << is changed into «

I didn’t want this so I needed to configure:

markup:
  goldmark:
    extensions:
      typographer:
        leftAngleQuote: '&lt;&lt;'
        rightAngleQuote: '&gt;&gt;'

4.3: nested lists sometimes need 4 space indents

This doesn’t render as a nested list anymore if I only indent by 2 spaces, I need to put 4 spaces.

1. a
  * b
  * c
2. b

The problem is that the amount of indent needed depends on the size of the list markers. Here’s a reference in CommonMark for this.

4.4: blockquotes inside lists work better

Previously the > quote here didn’t render as a blockquote, and with the new renderer it does.

* something
> quote
* something else

I found a bunch of Markdown that had been kind of broken (which I hadn’t noticed) that works better with the new renderer, and this is an example of that.

Lists inside blockquotes also seem to work better.

4.5: headings inside lists

Previously this didn’t render as a heading, but now it does. So I needed to replace the # with &num;.

* # passengers: 20

4.6: + or 1) at the beginning of the line makes it a list

I had something which looked like this:

`1 / (1
+ exp(-1)) = 0.73`

With Blackfriday it rendered like this:

<p><code>1 / (1
+ exp(-1)) = 0.73</code></p>

and with Goldmark it rendered like this:

<p>`1 / (1</p>
<ul>
<li>exp(-1)) = 0.73`</li>
</ul>

Same thing if there was an accidental 1) at the beginning of a line, like in this Markdown snippet

I set up a small Hadoop cluster (1 master, 2 workers, replication set to 
1) on 

To fix this I just had to rewrap the line so that the + wasn’t the first character.

The Markdown is formatted this way because I wrap my Markdown to 80 characters a lot and the wrapping isn’t very context sensitive.

4.7: no more smart quotes in code blocks

There were a bunch of places where the old renderer (Blackfriday) was doing unwanted things in code blocks like replacing ... with or replacing quotes with smart quotes. I hadn’t realized this was happening and I was very happy to have it fixed.

4.8: better quote management

The way this gets rendered got better:

"Oh, *interesting*!"
  • old: “Oh, interesting!“
  • new: “Oh, interesting!”

Before there were two left smart quotes, now the quotes match.

4.9: images are no longer wrapped in a p tag

Previously if I had an image like this:

<img src="https://jvns.ca/images/rustboot1.png">

it would get wrapped in a <p> tag, now it doesn’t anymore. I dealt with this just by adding a margin-bottom: 0.75em to images in the CSS, hopefully that’ll make them display well enough.

4.10: <br> is now wrapped in a p tag

Previously this wouldn’t get wrapped in a p tag, but now it seems to:

<br><br>

I just gave up on fixing this though and resigned myself to maybe having some extra space in some cases. Maybe I’ll try to fix it later if I feel like another yakshave.

4.11: some more goldmark settings

I also needed to

  • turn off code highlighting (because it wasn’t working properly and I didn’t have it before anyway)
  • use the old “blackfriday” method to generate heading IDs so they didn’t change
  • allow raw HTML in my markdown

Here’s what I needed to add to my config.yaml to do all that:

markup:
  highlight:
    codeFences: false
  goldmark:
    renderer:
      unsafe: true
    parser:
      autoHeadingIDType: blackfriday

Maybe I’ll try to get syntax highlighting working one day, who knows. I might prefer having it off though.

a little script to compare blackfriday and goldmark

I also wrote a little program to compare the Blackfriday and Goldmark output for various markdown snippets, here it is in a gist.

It’s not really configured the exact same way Blackfriday and Goldmark were in my Hugo versions, but it was still helpful to have to help me understand what was going on.

a quick note on maintaining themes

My approach to themes in Hugo has been:

  1. pay someone to make a nice design for the site (for example wizardzines.com was designed by Melody Starling)
  2. use a totally custom theme
  3. commit that theme to the same Github repo as the site

So I just need to edit the theme files to fix any problems. Also I wrote a lot of the theme myself so I’m pretty familiar with how it works.

Relying on someone else to keep a theme updated feels kind of scary to me, I think if I were using a third-party theme I’d just copy the code into my site’s github repo and then maintain it myself.

which static site generators have better backwards compatibility?

I asked on Mastodon if anyone had used a static site generator with good backwards compatibility.

The main answers seemed to be Jekyll and 11ty. Several people said they’d been using Jekyll for 10 years without any issues, and 11ty says it has stability as a core goal.

I think a big factor in how appealing Jekyll/11ty are is how easy it is for you to maintain a working Ruby / Node environment on your computer: part of the reason I stopped using Jekyll was that I got tired of having to maintain a working Ruby installation. But I imagine this wouldn’t be a problem for a Ruby or Node developer.

Several people said that they don’t build their Jekyll site locally at all – they just use GitHub Pages to build it.

that’s it!

Overall I’ve been happy with Hugo – I started using it because it had fast build times and it was a static binary, and both of those things are still extremely useful to me. I might have spent 10 hours on this upgrade, but I’ve probably spent 1000+ hours writing blog posts without thinking about Hugo at all so that seems like an extremely reasonable ratio.

I find it hard to be too mad about the backwards incompatible changes, most of them were quite a long time ago, Hugo does a great job of making their old releases available so you can use the old release if you want, and the most difficult one is removing support for the blackfriday Markdown renderer in favour of using something CommonMark-compliant which seems pretty reasonable to me even if it is a huge pain.

But it did take a long time and I don’t think I’d particularly recommend moving 700 blog posts to a new Markdown renderer unless you’re really in the mood for a lot of computer suffering for some reason.

The new renderer did fix a bunch of problems so I think overall it might be a good thing, even if I’ll have to remember to make 2 changes to how I write Markdown (4.1 and 4.3).

Also I’m still using Hugo 0.54 for https://wizardzines.com so maybe these notes will be useful to Future Me if I ever feel like upgrading Hugo for that site.

Hopefully I didn’t break too many things on the blog by doing this, let me know if you see anything broken!

2024-10-01T10:01:44+00:00 Fullscreen Open in Tab
Terminal colours are tricky

Yesterday I was thinking about how long it took me to get a colorscheme in my terminal that I was mostly happy with (SO MANY YEARS), and it made me wonder what about terminal colours made it so hard.

So I asked people on Mastodon what problems they’ve run into with colours in the terminal, and I got a ton of interesting responses! Let’s talk about some of the problems and a few possible ways to fix them.

problem 1: blue on black

One of the top complaints was “blue on black is hard to read”. Here’s an example of that: if I open Terminal.app, set the background to black, and run ls, the directories are displayed in a blue that isn’t that easy to read:

To understand why we’re seeing this blue, let’s talk about ANSI colours!

the 16 ANSI colours

Your terminal has 16 numbered colours – black, red, green, yellow, blue, magenta, cyan, white, and “bright” version of each of those.

Programs can use them by printing out an “ANSI escape code” – for example if you want to see each of the 16 colours in your terminal, you can run this Python program:

def color(num, text):
    return f"\033[38;5;{num}m{text}\033[0m"

for i in range(16):
    print(color(i, f"number {i:02}"))

what are the ANSI colours?

This made me wonder – if blue is colour number 5, who decides what hex color that should correspond to?

The answer seems to be “there’s no standard, terminal emulators just choose colours and it’s not very consistent”. Here’s a screenshot of a table from Wikipedia, where you can see that there’s a lot of variation:

problem 1.5: bright yellow on white

Bright yellow on white is even worse than blue on black, here’s what I get in a terminal with the default settings:

That’s almost impossible to read (and some other colours like light green cause similar issues), so let’s talk about solutions!

two ways to reconfigure your colours

If you’re annoyed by these colour contrast issues (or maybe you just think the default ANSI colours are ugly), you might think – well, I’ll just choose a different “blue” and pick something I like better!

There are two ways you can do this:

Way 1: Configure your terminal emulator: I think most modern terminal emulators have a way to reconfigure the colours, and some of them even come with some preinstalled themes that you might like better than the defaults.

Way 2: Run a shell script: There are ANSI escape codes that you can print out to tell your terminal emulator to reconfigure its colours. Here’s a shell script that does that, from the base16-shell project. You can see that it has a few different conventions for changing the colours – I guess different terminal emulators have different escape codes for changing their colour palette, and so the script is trying to pick the right style of escape code based on the TERM environment variable.

what are the pros and cons of the 2 ways of configuring your colours?

I prefer to use the “shell script” method, because:

  • if I switch terminal emulators for some reason, I don’t need to a different configuration system, my colours still Just Work
  • I use base16-shell with base16-vim to make my vim colours match my terminal colours, which is convenient

some advantages of configuring colours in your terminal emulator:

  • if you use a popular terminal emulator, there are probably a lot more nice terminal themes out there that you can choose from
  • not all terminal emulators support the “shell script method”, and even if they do, the results can be a little inconsistent

This is what my shell has looked like for probably the last 5 years (using the solarized light base16 theme), and I’m pretty happy with it. Here’s htop:

Okay, so let’s say you’ve found a terminal colorscheme that you like. What else can go wrong?

problem 2: programs using 256 colours

Here’s what some output of fd, a find alternative, looks like in my colorscheme:

The contrast is pretty bad here, and I definitely don’t have that lime green in my normal colorscheme. What’s going on?

We can see what color codes fd is using using the unbuffer program to capture its output including the color codes:

$ unbuffer fd . > out
$ vim out
^[[38;5;48mbad-again.sh^[[0m
^[[38;5;48mbad.sh^[[0m
^[[38;5;48mbetter.sh^[[0m
out

^[[38;5;48 means “set the foreground color to color 48”. Terminals don’t only have 16 colours – many terminals these days actually have 3 ways of specifying colours:

  1. the 16 ANSI colours we already talked about
  2. an extended set of 256 colours
  3. a further extended set of 24-bit hex colours, like #ffea03

So fd is using one of the colours from the extended 256-color set. bat (a cat alternative) does something similar – here’s what it looks like by default in my terminal.

This looks fine though and it really seems like it’s trying to work well with a variety of terminal themes.

some newer tools seem to have theme support

I think it’s interesting that some of these newer terminal tools (fd, cat, delta, and probably more) have support for arbitrary custom themes. I guess the downside of this approach is that the default theme might clash with your terminal’s background, but the upside is that it gives you a lot more control over theming the tool’s output than just choosing 16 ANSI colours.

I don’t really use bat, but if I did I’d probably use bat --theme ansi to just use the ANSI colours that I have set in my normal terminal colorscheme.

problem 3: the grays in Solarized

A bunch of people on Mastodon mentioned a specific issue with grays in the Solarized theme: when I list a directory, the base16 Solarized Light theme looks like this:

but iTerm’s default Solarized Light theme looks like this:

This is because in the iTerm theme (which is the original Solarized design), colors 9-14 (the “bright blue”, “bright red”, etc) are mapped to a series of grays, and when I run ls, it’s trying to use those “bright” colours to color my directories and executables.

My best guess for why the original Solarized theme is designed this way is to make the grays available to the vim Solarized colorscheme.

I’m pretty sure I prefer the modified base16 version I use where the “bright” colours are actually colours instead of all being shades of gray though. (I didn’t actually realize the version I was using wasn’t the “original” Solarized theme until I wrote this post)

In any case I really love Solarized and I’m very happy it exists so that I can use a modified version of it.

problem 4: a vim theme that doesn’t match the terminal background

If I my vim theme has a different background colour than my terminal theme, I get this ugly border, like this:

This one is a pretty minor issue though and I think making your terminal background match your vim background is pretty straightforward.

problem 5: programs setting a background color

A few people mentioned problems with terminal applications setting an unwanted background colour, so let’s look at an example of that.

Here ngrok has set the background to color #16 (“black”), but the base16-shell script I use sets color 16 to be bright orange, so I get this, which is pretty bad:

I think the intention is for ngrok to look something like this:

I think base16-shell sets color #16 to orange (instead of black) so that it can provide extra colours for use by base16-vim. This feels reasonable to me – I use base16-vim in the terminal, so I guess I’m using that feature and it’s probably more important to me than ngrok (which I rarely use) behaving a bit weirdly.

This particular issue is a maybe obscure clash between ngrok and my colorschem, but I think this kind of clash is pretty common when a program sets an ANSI background color that the user has remapped for some reason.

a nice solution to contrast issues: “minimum contrast”

A bunch of terminals (iTerm2, tabby, kitty’s text_fg_override_threshold, and folks tell me also Ghostty and Windows Terminal) have a “minimum contrast” feature that will automatically adjust colours to make sure they have enough contrast.

Here’s an example from iTerm. This ngrok accident from before has pretty bad contrast, I find it pretty difficult to read:

With “minimum contrast” set to 40 in iTerm, it looks like this instead:

I didn’t have minimum contrast turned on before but I just turned it on today because it makes such a big difference when something goes wrong with colours in the terminal.

problem 6: TERM being set to the wrong thing

A few people mentioned that they’ll SSH into a system that doesn’t support the TERM environment variable that they have set locally, and then the colours won’t work.

I think the way TERM works is that systems have a terminfo database, so if the value of the TERM environment variable isn’t in the system’s terminfo database, then it won’t know how to output colours for that terminal. I don’t know too much about terminfo, but someone linked me to this terminfo rant that talks about a few other issues with terminfo.

I don’t have a system on hand to reproduce this one so I can’t say for sure how to fix it, but this stackoverflow question suggests running something like TERM=xterm ssh instead of ssh.

problem 7: picking “good” colours is hard

A couple of problems people mentioned with designing / finding terminal colorschemes:

  • some folks are colorblind and have trouble finding an appropriate colorscheme
  • accidentally making the background color too close to the cursor or selection color, so they’re hard to find
  • generally finding colours that work with every program is a struggle (for example you can see me having a problem with this with ngrok above!)

problem 8: making nethack/mc look right

Another problem people mentioned is using a program like nethack or midnight commander which you might expect to have a specific colourscheme based on the default ANSI terminal colours.

For example, midnight commander has a really specific classic look:

But in my Solarized theme, midnight commander looks like this:

The Solarized version feels like it could be disorienting if you’re very used to the “classic” look.

One solution Simon Tatham mentioned to this is using some palette customization ANSI codes (like the ones base16 uses that I talked about earlier) to change the color palette right before starting the program, for example remapping yellow to a brighter yellow before starting Nethack so that the yellow characters look better.

problem 9: commands disabling colours when writing to a pipe

If I run fd | less, I see something like this, with the colours disabled.

In general I find this useful – if I pipe a command to grep, I don’t want it to print out all those color escape codes, I just want the plain text. But what if you want to see the colours?

To see the colours, you can run unbuffer fd | less -r! I just learned about unbuffer recently and I think it’s really cool, unbuffer opens a tty for the command to write to so that it thinks it’s writing to a TTY. It also fixes issues with programs buffering their output when writing to a pipe, which is why it’s called unbuffer.

Here’s what the output of unbuffer fd | less -r looks like for me:

Also some commands (including fd) support a --color=always flag which will force them to always print out the colours.

problem 10: unwanted colour in ls and other commands

Some people mentioned that they don’t want ls to use colour at all, perhaps because ls uses blue, it’s hard to read on black, and maybe they don’t feel like customizing their terminal’s colourscheme to make the blue more readable or just don’t find the use of colour helpful.

Some possible solutions to this one:

  • you can run ls --color=never, which is probably easiest
  • you can also set LS_COLORS to customize the colours used by ls. I think some other programs other than ls support the LS_COLORS environment variable too.
  • also some programs support setting NO_COLOR=true (there’s a list here)

Here’s an example of running LS_COLORS="fi=0:di=0:ln=0:pi=0:so=0:bd=0:cd=0:or=0:ex=0" ls:

problem 11: the colours in vim

I used to have a lot of problems with configuring my colours in vim – I’d set up my terminal colours in a way that I thought was okay, and then I’d start vim and it would just be a disaster.

I think what was going on here is that today, there are two ways to set up a vim colorscheme in the terminal:

  1. using your ANSI terminal colours – you tell vim which ANSI colour number to use for the background, for functions, etc.
  2. using 24-bit hex colours – instead of ANSI terminal colours, the vim colorscheme can use hex codes like #faea99 directly

20 years ago when I started using vim, terminals with 24-bit hex color support were a lot less common (or maybe they didn’t exist at all), and vim certainly didn’t have support for using 24-bit colour in the terminal. From some quick searching through git, it looks like vim added support for 24-bit colour in 2016 – just 8 years ago!

So to get colours to work properly in vim before 2016, you needed to synchronize your terminal colorscheme and your vim colorscheme. Here’s what that looked like, the colorscheme needed to map the vim color classes like cterm05 to ANSI colour numbers.

But in 2024, the story is really different! Vim (and Neovim, which I use now) support 24-bit colours, and as of Neovim 0.10 (released in May 2024), the termguicolors setting (which tells Vim to use 24-bit hex colours for colorschemes) is turned on by default in any terminal with 24-bit color support.

So this “you need to synchronize your terminal colorscheme and your vim colorscheme” problem is not an issue anymore for me in 2024, since I don’t plan to use terminals without 24-bit color support in the future.

The biggest consequence for me of this whole thing is that I don’t need base16 to set colors 16-21 to weird stuff anymore to integrate with vim – I can just use a terminal theme and a vim theme, and as long as the two themes use similar colours (so it’s not jarring for me to switch between them) there’s no problem. I think I can just remove those parts from my base16 shell script and totally avoid the problem with ngrok and the weird orange background I talked about above.

some more problems I left out

I think there are a lot of issues around the intersection of multiple programs, like using some combination tmux/ssh/vim that I couldn’t figure out how to reproduce well enough to talk about them. Also I’m sure I missed a lot of other things too.

base16 has really worked for me

I’ve personally had a lot of success with using base16-shell with base16-vim – I just need to add a couple of lines to my fish config to set it up (+ a few .vimrc lines) and then I can move on and accept any remaining problems that that doesn’t solve.

I don’t think base16 is for everyone though, some limitations I’m aware of with base16 that might make it not work for you:

  • it comes with a limited set of builtin themes and you might not like any of them
  • the Solarized base16 theme (and maybe all of the themes?) sets the “bright” ANSI colours to be exactly the same as the normal colours, which might cause a problem if you’re relying on the “bright” colours to be different from the regular ones
  • it sets colours 16-21 in order to give the vim colorschemes from base16-vim access to more colours, which might not be relevant if you always use a terminal with 24-bit color support, and can cause problems like the ngrok issue above
  • also the way it sets colours 16-21 could be a problem in terminals that don’t have 256-color support, like the linux framebuffer terminal

Apparently there’s a community fork of base16 called tinted-theming, which I haven’t looked into much yet.

some other colorscheme tools

Just one so far but I’ll link more if people tell me about them:

okay, that was a lot

We talked about a lot in this post and while I think learning about all these details is kind of fun if I’m in the mood to do a deep dive, I find it SO FRUSTRATING to deal with it when I just want my colours to work! Being surprised by unreadable text and having to find a workaround is just not my idea of a good day.

Personally I’m a zero-configuration kind of person and it’s not that appealing to me to have to put together a lot of custom configuration just to make my colours in the terminal look acceptable. I’d much rather just have some reasonable defaults that I don’t have to change.

minimum contrast seems like an amazing feature

My one big takeaway from writing this was to turn on “minimum contrast” in my terminal, I think it’s going to fix most of the occasional accidental unreadable text issues I run into and I’m pretty excited about it.

2024-09-27T11:16:00+00:00 Fullscreen Open in Tab
Some Go web dev notes

I spent a lot of time in the past couple of weeks working on a website in Go that may or may not ever see the light of day, but I learned a couple of things along the way I wanted to write down. Here they are:

go 1.22 now has better routing

I’ve never felt motivated to learn any of the Go routing libraries (gorilla/mux, chi, etc), so I’ve been doing all my routing by hand, like this.

	// DELETE /records:
	case r.Method == "DELETE" && n == 1 && p[0] == "records":
		if !requireLogin(username, r.URL.Path, r, w) {
			return
		}
		deleteAllRecords(ctx, username, rs, w, r)
	// POST /records/<ID>
	case r.Method == "POST" && n == 2 && p[0] == "records" && len(p[1]) > 0:
		if !requireLogin(username, r.URL.Path, r, w) {
			return
		}
		updateRecord(ctx, username, p[1], rs, w, r)

But apparently as of Go 1.22, Go now has better support for routing in the standard library, so that code can be rewritten something like this:

	mux.HandleFunc("DELETE /records/", app.deleteAllRecords)
	mux.HandleFunc("POST /records/{record_id}", app.updateRecord)

Though it would also need a login middleware, so maybe something more like this, with a requireLogin middleware.

	mux.Handle("DELETE /records/", requireLogin(http.HandlerFunc(app.deleteAllRecords)))

a gotcha with the built-in router: redirects with trailing slashes

One annoying gotcha I ran into was: if I make a route for /records/, then a request for /records will be redirected to /records/.

I ran into an issue with this where sending a POST request to /records redirected to a GET request for /records/, which broke the POST request because it removed the request body. Thankfully Xe Iaso wrote a blog post about the exact same issue which made it easier to debug.

I think the solution to this is just to use API endpoints like POST /records instead of POST /records/, which seems like a more normal design anyway.

sqlc automatically generates code for my db queries

I got a little bit tired of writing so much boilerplate for my SQL queries, but I didn’t really feel like learning an ORM, because I know what SQL queries I want to write, and I didn’t feel like learning the ORM’s conventions for translating things into SQL queries.

But then I found sqlc, which will compile a query like this:


-- name: GetVariant :one
SELECT *
FROM variants
WHERE id = ?;

into Go code like this:

const getVariant = `-- name: GetVariant :one
SELECT id, created_at, updated_at, disabled, product_name, variant_name
FROM variants
WHERE id = ?
`

func (q *Queries) GetVariant(ctx context.Context, id int64) (Variant, error) {
	row := q.db.QueryRowContext(ctx, getVariant, id)
	var i Variant
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Disabled,
		&i.ProductName,
		&i.VariantName,
	)
	return i, err
}

What I like about this is that if I’m ever unsure about what Go code to write for a given SQL query, I can just write the query I want, read the generated function and it’ll tell me exactly what to do to call it. It feels much easier to me than trying to dig through the ORM’s documentation to figure out how to construct the SQL query I want.

Reading Brandur’s sqlc notes from 2024 also gave me some confidence that this is a workable path for my tiny programs. That post gives a really helpful example of how to conditionally update fields in a table using CASE statements (for example if you have a table with 20 columns and you only want to update 3 of them).

sqlite tips

Someone on Mastodon linked me to this post called Optimizing sqlite for servers. My projects are small and I’m not so concerned about performance, but my main takeaways were:

  • have a dedicated object for writing to the database, and run db.SetMaxOpenConns(1) on it. I learned the hard way that if I don’t do this then I’ll get SQLITE_BUSY errors from two threads trying to write to the db at the same time.
  • if I want to make reads faster, I could have 2 separate db objects, one for writing and one for reading

There are a more tips in that post that seem useful (like “COUNT queries are slow” and “Use STRICT tables”), but I haven’t done those yet.

Also sometimes if I have two tables where I know I’ll never need to do a JOIN beteween them, I’ll just put them in separate databases so that I can connect to them independently.

Go 1.19 introduced a way to set a GC memory limit

I run all of my Go projects in VMs with relatively little memory, like 256MB or 512MB. I ran into an issue where my application kept getting OOM killed and it was confusing – did I have a memory leak? What?

After some Googling, I realized that maybe I didn’t have a memory leak, maybe I just needed to reconfigure the garbage collector! It turns out that by default (according to A Guide to the Go Garbage Collector), Go’s garbage collector will let the application allocate memory up to 2x the current heap size.

Mess With DNS’s base heap size is around 170MB and the amount of memory free on the VM is around 160MB right now, so if its memory doubled, it’ll get OOM killed.

In Go 1.19, they added a way to tell Go “hey, if the application starts using this much memory, run a GC”. So I set the GC memory limit to 250MB and it seems to have resulted in the application getting OOM killed less often:

export GOMEMLIMIT=250MiB

some reasons I like making websites in Go

I’ve been making tiny websites (like the nginx playground) in Go on and off for the last 4 years or so and it’s really been working for me. I think I like it because:

  • there’s just 1 static binary, all I need to do to deploy it is copy the binary. If there are static files I can just embed them in the binary with embed.
  • there’s a built-in webserver that’s okay to use in production, so I don’t need to configure WSGI or whatever to get it to work. I can just put it behind Caddy or run it on fly.io or whatever.
  • Go’s toolchain is very easy to install, I can just do apt-get install golang-go or whatever and then a go build will build my project
  • it feels like there’s very little to remember to start sending HTTP responses – basically all there is are functions like Serve(w http.ResponseWriter, r *http.Request) which read the request and send a response. If I need to remember some detail of how exactly that’s accomplished, I just have to read the function!
  • also net/http is in the standard library, so you can start making websites without installing any libraries at all. I really appreciate this one.
  • Go is a pretty systems-y language, so if I need to run an ioctl or something that’s easy to do

In general everything about it feels like it makes projects easy to work on for 5 days, abandon for 2 years, and then get back into writing code without a lot of problems.

For contrast, I’ve tried to learn Rails a couple of times and I really want to love Rails – I’ve made a couple of toy websites in Rails and it’s always felt like a really magical experience. But ultimately when I come back to those projects I can’t remember how anything works and I just end up giving up. It feels easier to me to come back to my Go projects that are full of a lot of repetitive boilerplate, because at least I can read the code and figure out how it works.

things I haven’t figured out yet

some things I haven’t done much of yet in Go:

  • rendering HTML templates: usually my Go servers are just APIs and I make the frontend a single-page app with Vue. I’ve used html/template a lot in Hugo (which I’ve used for this blog for the last 8 years) but I’m still not sure how I feel about it.
  • I’ve never made a real login system, usually my servers don’t have users at all.
  • I’ve never tried to implement CSRF

In general I’m not sure how to implement security-sensitive features so I don’t start projects which need login/CSRF/etc. I imagine this is where a framework would help.

it’s cool to see the new features Go has been adding

Both of the Go features I mentioned in this post (GOMEMLIMIT and the routing) are new in the last couple of years and I didn’t notice when they came out. It makes me think I should pay closer attention to the release notes for new Go versions.

2024-09-12T15:09:12+00:00 Fullscreen Open in Tab
Reasons I still love the fish shell

I wrote about how much I love fish in this blog post from 2017 and, 7 years of using it every day later, I’ve found even more reasons to love it. So I thought I’d write a new post with both the old reasons I loved it and some reasons.

This came up today because I was trying to figure out why my terminal doesn’t break anymore when I cat a binary to my terminal, the answer was “fish fixes the terminal!”, and I just thought that was really nice.

1. no configuration

In 10 years of using fish I have never found a single thing I wanted to configure. It just works the way I want. My fish config file just has:

  • environment variables
  • aliases (alias ls eza, alias vim nvim, etc)
  • the occasional direnv hook fish | source to integrate a tool like direnv
  • a script I run to set up my terminal colours

I’ve been told that configuring things in fish is really easy if you ever do want to configure something though.

2. autosuggestions from my shell history

My absolute favourite thing about fish is that I type, it’ll automatically suggest (in light grey) a matching command that I ran recently. I can press the right arrow key to accept the completion, or keep typing to ignore it.

Here’s what that looks like. In this example I just typed the “v” key and it guessed that I want to run the previous vim command again.

2.5 “smart” shell autosuggestions

One of my favourite subtle autocomplete features is how fish handles autocompleting commands that contain paths in them. For example, if I run:

$ ls blah.txt

that command will only be autocompleted in directories that contain blah.txt – it won’t show up in a different directory. (here’s a short comment about how it works)

As an example, if in this directory I type bash scripts/, it’ll only suggest history commands including files that actually exist in my blog’s scripts folder, and not the dozens of other irrelevant scripts/ commands I’ve run in other folders.

I didn’t understand exactly how this worked until last week, it just felt like fish was magically able to suggest the right commands. It still feels a little like magic and I love it.

3. pasting multiline commands

If I copy and paste multiple lines, bash will run them all, like this:

[bork@grapefruit linux-playground (main)]$ echo hi
hi
[bork@grapefruit linux-playground (main)]$ touch blah
[bork@grapefruit linux-playground (main)]$ echo hi
hi

This is a bit alarming – what if I didn’t actually want to run all those commands?

Fish will paste them all at a single prompt, so that I can press Enter if I actually want to run them. Much less scary.

bork@grapefruit ~/work/> echo hi

                         touch blah
                         echo hi

4. nice tab completion

If I run ls and press tab, it’ll display all the filenames in a nice grid. I can use either Tab, Shift+Tab, or the arrow keys to navigate the grid.

Also, I can tab complete from the middle of a filename – if the filename starts with a weird character (or if it’s just not very unique), I can type some characters from the middle and press tab.

Here’s what the tab completion looks like:

bork@grapefruit ~/work/> ls 
api/  blah.py     fly.toml   README.md
blah  Dockerfile  frontend/  test_websocket.sh

I honestly don’t complete things other than filenames very much so I can’t speak to that, but I’ve found the experience of tab completing filenames to be very good.

5. nice default prompt (including git integration)

Fish’s default prompt includes everything I want:

  • username
  • hostname
  • current folder
  • git integration
  • status of last command exit (if the last command failed)

Here’s a screenshot with a few different variations on the default prompt, including if the last command was interrupted (the SIGINT) or failed.

6. nice history defaults

In bash, the maximum history size is 500 by default, presumably because computers used to be slow and not have a lot of disk space. Also, by default, commands don’t get added to your history until you end your session. So if your computer crashes, you lose some history.

In fish:

  1. the default history size is 256,000 commands. I don’t see any reason I’d ever need more.
  2. if you open a new tab, everything you’ve ever run (including commands in open sessions) is immediately available to you
  3. in an existing session, the history search will only include commands from the current session, plus everything that was in history at the time that you started the shell

I’m not sure how clearly I’m explaining how fish’s history system works here, but it feels really good to me in practice. My impression is that the way it’s implemented is the commands are continually added to the history file, but fish only loads the history file once, on startup.

I’ll mention here that if you want to have a fancier history system in another shell it might be worth checking out atuin or fzf.

7. press up arrow to search history

I also like fish’s interface for searching history: for example if I want to edit my fish config file, I can just type:

$ config.fish

and then press the up arrow to go back the last command that included config.fish. That’ll complete to:

$ vim ~/.config/fish/config.fish

and I’m done. This isn’t so different from using Ctrl+R in bash to search your history but I think I like it a little better over all, maybe because Ctrl+R has some behaviours that I find confusing (for example you can end up accidentally editing your history which I don’t like).

8. the terminal doesn’t break

I used to run into issues with bash where I’d accidentally cat a binary to the terminal, and it would break the terminal.

Every time fish displays a prompt, it’ll try to fix up your terminal so that you don’t end up in weird situations like this. I think this is some of the code in fish to prevent broken terminals.

Some things that it does are:

  • turn on echo so that you can see the characters you type
  • make sure that newlines work properly so that you don’t get that weird staircase effect
  • reset your terminal background colour, etc

I don’t think I’ve run into any of these “my terminal is broken” issues in a very long time, and I actually didn’t even realize that this was because of fish – I thought that things somehow magically just got better, or maybe I wasn’t making as many mistakes. But I think it was mostly fish saving me from myself, and I really appreciate that.

9. Ctrl+S is disabled

Also related to terminals breaking: fish disables Ctrl+S (which freezes your terminal and then you need to remember to press Ctrl+Q to unfreeze it). It’s a feature that I’ve never wanted and I’m happy to not have it.

Apparently you can disable Ctrl+S in other shells with stty -ixon.

10. nice syntax highlighting

By default commands that don’t exist are highlighted in red, like this.

11. easier loops

I find the loop syntax in fish a lot easier to type than the bash syntax. It looks like this:

for i in *.yaml
  echo $i
end

Also it’ll add indentation in your loops which is nice.

12. easier multiline editing

Related to loops: you can edit multiline commands much more easily than in bash (just use the arrow keys to navigate the multiline command!). Also when you use the up arrow to get a multiline command from your history, it’ll show you the whole command the exact same way you typed it instead of squishing it all onto one line like bash does:

$ bash
$ for i in *.png
> do
> echo $i
> done
$ # press up arrow
$ for i in *.png; do echo $i; done ink

13. Ctrl+left arrow

This might just be me, but I really appreciate that fish has the Ctrl+left arrow / Ctrl+right arrow keyboard shortcut for moving between words when writing a command.

I’m honestly a bit confused about where this keyboard shortcut is coming from (the only documented keyboard shortcut for this I can find in fish is Alt+left arrow / Alt + right arrow which seems to do the same thing), but I’m pretty sure this is a fish shortcut.

A couple of notes about getting this shortcut to work / where it comes from:

  • one person said they needed to switch their terminal emulator from the “Linux console” keybindings to “Default (XFree 4)” to get it to work in fish
  • on Mac OS, Ctrl+left arrow switches workspaces by default, so I had to turn that off.
  • Also apparently Ubuntu configures libreadline in /etc/inputrc to make Ctrl+left/right arrow go back/forward a word, so it’ll work in bash on Ubuntu and maybe other Linux distros too. Here’s a stack overflow question talking about that

a downside: not everything has a fish integration

Sometimes tools don’t have instructions for integrating them with fish. That’s annoying, but:

  • I’ve found this has gotten better over the last 10 years as fish has gotten more popular. For example Python’s virtualenv has had a fish integration for a long time now.
  • If I need to run a POSIX shell command real quick, I can always just run bash or zsh
  • I’ve gotten much better over the years at translating simple commands to fish syntax when I need to

My biggest day-to-day to annoyance is probably that for whatever reason I’m still not used to fish’s syntax for setting environment variables, I get confused about set vs set -x.

another downside: fish_add_path

fish has a function called fish_add_path that you can run to add a directory to your PATH like this:

fish_add_path /some/directory

I love the idea of it and I used to use it all the time, but I’ve stopped using it for two reasons:

  1. Sometimes fish_add_path will update the PATH for every session in the future (with a “universal variable”) and sometimes it will update the PATH just for the current session. It’s hard for me to tell which one it will do: in theory the docs explain this but I could not understand them.
  2. If you ever need to remove the directory from your PATH a few weeks or months later because maybe you made a mistake, that’s also kind of hard to do (there are instructions in this comments of this github issue though).

Instead I just update my PATH like this, similarly to how I’d do it in bash:

set PATH $PATH /some/directory/bin

on POSIX compatibility

When I started using fish, you couldn’t do things like cmd1 && cmd2 – it would complain “no, you need to run cmd1; and cmd2” instead.

It seems like over the years fish has started accepting a little more POSIX-style syntax than it used to, like:

  • cmd1 && cmd2
  • export a=b to set an environment variable (though this seems a bit limited, you can’t do export PATH=$PATH:/whatever so I think it’s probably better to learn set instead)

on fish as a default shell

Changing my default shell to fish is always a little annoying, I occasionally get myself into a situation where

  1. I install fish somewhere like maybe /home/bork/.nix-stuff/bin/fish
  2. I add the new fish location to /etc/shells as an allowed shell
  3. I change my shell with chsh
  4. at some point months/years later I reinstall fish in a different location for some reason and remove the old one
  5. oh no!!! I have no valid shell! I can’t open a new terminal tab anymore!

This has never been a major issue because I always have a terminal open somewhere where I can fix the problem and rescue myself, but it’s a bit alarming.

If you don’t want to use chsh to change your shell to fish (which is very reasonable, maybe I shouldn’t be doing that), the Arch wiki page has a couple of good suggestions – either configure your terminal emulator to run fish or add an exec fish to your .bashrc.

I’ve never really learned the scripting language

Other than occasionally writing a for loop interactively on the command line, I’ve never really learned the fish scripting language. I still do all of my shell scripting in bash.

I don’t think I’ve ever written a fish function or if statement.

I ran a highly unscientific poll on Mastodon asking people what shell they use interactively. The results were (of 2600 responses):

  • 46% bash
  • 49% zsh
  • 16% fish
  • 5% other

I think 16% for fish is pretty remarkable, since (as far as I know) there isn’t any system where fish is the default shell, and my sense is that it’s very common to just stick to whatever your system’s default shell is.

It feels like a big achievement for the fish project, even if maybe my Mastodon followers are more likely than the average shell user to use fish for some reason.

who might fish be right for?

Fish definitely isn’t for everyone. I think I like it because:

  1. I really dislike configuring my shell (and honestly my dev environment in general), I want things to “just work” with the default settings
  2. fish’s defaults feel good to me
  3. I don’t spend that much time logged into random servers using other shells so there’s not too much context switching
  4. I liked its features so much that I was willing to relearn how to do a few “basic” shell things, like using parentheses (seq 1 10) to run a command instead of backticks or using set instead of export

Maybe you’re also a person who would like fish! I hope a few more of the people who fish is for can find it, because I spend so much of my time in the terminal and it’s made that time much more pleasant.

2024-08-31T18:36:50-07:00 Fullscreen Open in Tab
Thoughts on the Resiliency of Web Projects

I just did a massive spring cleaning of one of my servers, trying to clean up what has become quite the mess of clutter. For every website on the server, I either:

  • Documented what it is, who is using it, and what version of language and framework it uses
  • Archived it as static HTML flat files
  • Moved the source code from GitHub to a private git server
  • Deleted the files

It feels good to get rid of old code, and to turn previously dynamic sites (with all of the risk they come with) into plain HTML.

This is also making me seriously reconsider the value of spinning up any new projects. Several of these are now 10 years old, still churning along fine, but difficult to do any maintenance on because of versions and dependencies. For example:

  • indieauth.com - this has been on the chopping block for years, but I haven't managed to build a replacement yet, and is still used by a lot of people
  • webmention.io - this is a pretty popular service, and I don't want to shut it down, but there's a lot of problems with how it's currently built and no easy way to make changes
  • switchboard.p3k.io - this is a public WebSub (PubSubHubbub) hub, like Superfeedr, and has weirdly gained a lot of popularity in the podcast feed space in the last few years

One that I'm particularly happy with, despite it being an ugly pile of PHP, is oauth.net. I inherited this site in 2012, and it hasn't needed any framework upgrades since it's just using PHP templates. My ham radio website w7apk.com is similarly a small amount of templated PHP, and it is low stress to maintain, and actually fun to quickly jot some notes down when I want. I like not having to go through the whole ceremony of setting up a dev environment, installing dependencies, upgrading things to the latest version, checking for backwards incompatible changes, git commit, deploy, etc. I can just sftp some changes up to the server and they're live.

Some questions for myself for the future, before starting a new project:

  • Could this actually just be a tag page on my website, like #100DaysOfMusic or #BikeTheEclipse?
  • If it really needs to be a new project, then:
  • Can I create it in PHP without using any frameworks or libraries? Plain PHP ages far better than pulling in any dependencies which inevitably stop working with a version 2-3 EOL cycles back, so every library brought in means signing up for annual maintenance of the whole project. Frameworks can save time in the short term, but have a huge cost in the long term.
  • Is it possible to avoid using a database? Databases aren't inherently bad, but using one does make the project slightly more fragile, since it requires plans for migrations and backups, and 
  • If a database is required, is it possible to create it in a way that does not result in ever-growing storage needs?
  • Is this going to store data or be a service that other people are going to use? If so, plan on a registration form so that I have a way to contact people eventually when I need to change it or shut it down.
  • If I've got this far with the questions, am I really ready to commit to supporting this code base for the next 10 years?

One project I've been committed to maintaining and doing regular (ok fine, "semi-regular") updates for is Meetable, the open source events website that I run on a few domains:

I started this project in October 2019, excited for all the IndieWebCamps we were going to run in 2020. Somehow that is already 5 years ago now. Well that didn't exactly pan out, but I did quickly pivot it to add a bunch of features that are helpful for virtual events, so it worked out ok in the end. We've continued to use it for posting IndieWeb events, and I also run an instance for two IETF working groups. I'd love to see more instances pop up, I've only encountered one or two other ones in the wild. I even spent a significant amount of time on the onboarding flow so that it's relatively easy to install and configure. I even added passkeys for the admin login so you don't need any external dependencies on auth providers. It's a cool project if I may say so myself.

Anyway, this is not a particularly well thought out blog post, I just wanted to get my thoughts down after spending all day combing through the filesystem of my web server and uncovering a lot of ancient history.

2024-08-29T12:59:53-07:00 Fullscreen Open in Tab
OAuth Oh Yeah!

The first law of OAuth states that
the total number of authorized access tokens
in an isolated system
must remain constant over time. Over time.

In the world of OAuth, where the sun always shines,
Tokens like treasures, in digital lines.
Security's a breeze, with every law so fine,
OAuth, oh yeah, tonight we dance online!

The second law of OAuth states that
the overall security of the system
must always remain constant over time.
Over time. Over time. Over time.

In the world of OAuth, where the sun always shines,
Tokens like treasures, in digital lines.
Security's a breeze, with every law so fine,
OAuth, oh yeah, tonight we dance online!

The third law of OAuth states that
as the security of the system approaches absolute,
the ability to grant authorized access approaches zero. Zero!

In the world of OAuth, where the sun always shines,
Tokens like treasures, in digital lines.
Security's a breeze, with every law so fine,
OAuth, oh yeah, tonight we dance online!

Tonight we dance online!
OAuth, oh yeah!
Lyrics and music by AI, prompted and edited by Aaron Parecki
2024-08-19T08:15:28+00:00 Fullscreen Open in Tab
Migrating Mess With DNS to use PowerDNS

About 3 years ago, I announced Mess With DNS in this blog post, a playground where you can learn how DNS works by messing around and creating records.

I wasn’t very careful with the DNS implementation though (to quote the release blog post: “following the DNS RFCs? not exactly”), and people started reporting problems that eventually I decided that I wanted to fix.

the problems

Some of the problems people have reported were:

  • domain names with underscores weren’t allowed, even though they should be
  • If there was a CNAME record for a domain name, it allowed you to create other records for that domain name, even if it shouldn’t
  • you could create 2 different CNAME records for the same domain name, which shouldn’t be allowed
  • no support for the SVCB or HTTPS record types, which seemed a little complex to implement
  • no support for upgrading from UDP to TCP for big responses

And there are certainly more issues that nobody got around to reporting, for example that if you added an NS record for a subdomain to delegate it, Mess With DNS wouldn’t handle the delegation properly.

the solution: PowerDNS

I wasn’t sure how to fix these problems for a long time – technically I could have started addressing them individually, but it felt like there were a million edge cases and I’d never get there.

But then one day I was chatting with someone else who was working on a DNS server and they said they were using PowerDNS: an open source DNS server with an HTTP API!

This seemed like an obvious solution to my problems – I could just swap out my own crappy DNS implementation for PowerDNS.

There were a couple of challenges I ran into when setting up PowerDNS that I’ll talk about here. I really don’t do a lot of web development and I think I’ve never built a website that depends on a relatively complex API before, so it was a bit of a learning experience.

challenge 1: getting every query made to the DNS server

One of the main things Mess With DNS does is give you a live view of every DNS query it receives for your subdomain, using a websocket. To make this work, it needs to intercept every DNS query before they it gets sent to the PowerDNS DNS server:

There were 2 options I could think of for how to intercept the DNS queries:

  1. dnstap: dnsdist (a DNS load balancer from the PowerDNS project) has support for logging all DNS queries it receives using dnstap, so I could put dnsdist in front of PowerDNS and then log queries that way
  2. Have my Go server listen on port 53 and proxy the queries myself

I originally implemented option #1, but for some reason there was a 1 second delay before every query got logged. I couldn’t figure out why, so I implemented my own very simple proxy instead.

challenge 2: should the frontend have direct access to the PowerDNS API?

The frontend used to have a lot of DNS logic in it – it converted emoji domain names to ASCII using punycode, had a lookup table to convert numeric DNS query types (like 1) to their human-readable names (like A), did a little bit of validation, and more.

Originally I considered keeping this pattern and just giving the frontend (more or less) direct access to the PowerDNS API to create and delete, but writing even more complex code in Javascript didn’t feel that appealing to me – I don’t really know how to write tests in Javascript and it seemed like it wouldn’t end well.

So I decided to take all of the DNS logic out of the frontend and write a new DNS API for managing records, shaped something like this:

  • GET /records
  • DELETE /records/<ID>
  • DELETE /records/ (delete all records for a user)
  • POST /records/ (create record)
  • POST /records/<ID> (update record)

This meant that I could actually write tests for my code, since the backend is in Go and I do know how to write tests in Go.

what I learned: it’s okay for an API to duplicate information

I had this idea that APIs shouldn’t return duplicate information – for example if I get a DNS record, it should only include a given piece of information once.

But I ran into a problem with that idea when displaying MX records: an MX record has 2 fields, “preference”, and “mail server”. And I needed to display that information in 2 different ways on the frontend:

  1. In a form, where “Preference” and “Mail Server” are 2 different form fields (like 10 and mail.example.com)
  2. In a summary view, where I wanted to just show the record (10 mail.example.com)

This is kind of a small problem, but it came up in a few different places.

I talked to my friend Marco Rogers about this, and based on some advice from him I realized that I could return the same information in the API in 2 different ways! Then the frontend just has to display it. So I started just returning duplicate information in the API, something like this:

{
  values: {'Preference': 10, 'Server': 'mail.example.com'},
  content: '10 mail.example.com',
  ...
}

I ended up using this pattern in a couple of other places where I needed to display the same information in 2 different ways and it was SO much easier.

I think what I learned from this is that if I’m making an API that isn’t intended for external use (there are no users of this API other than the frontend!), I can tailor it very specifically to the frontend’s needs and that’s okay.

challenge 3: what’s a record’s ID?

In Mess With DNS (and I think in most DNS user interfaces!), you create, add, and delete records.

But that’s not how the PowerDNS API works. In PowerDNS, you create a zone, which is made of record sets. Records don’t have any ID in the API at all.

I ended up solving this by generate a fake ID for each records which is made of:

  • its name
  • its type
  • and its content (base64-encoded)

For example one record’s ID is brooch225.messwithdns.com.|NS|bnMxLm1lc3N3aXRoZG5zLmNvbS4=

Then I can search through the zone and find the appropriate record to update it.

This means that if you update a record then its ID will change which isn’t usually what I want in an ID, but that seems fine.

challenge 4: making clear error messages

I think the error messages that the PowerDNS API returns aren’t really intended to be shown to end users, for example:

  • Name 'new\032site.island358.messwithdns.com.' contains unsupported characters (this error encodes the space as \032, which is a bit disorienting if you don’t know that the space character is 32 in ASCII)
  • RRset test.pear5.messwithdns.com. IN CNAME: Conflicts with pre-existing RRset (this talks about RRsets, which aren’t a concept that the Mess With DNS UI has at all)
  • Record orange.beryl5.messwithdns.com./A '1.2.3.4$': Parsing record content (try 'pdnsutil check-zone'): unable to parse IP address, strange character: $ (mentions “pdnsutil”, a utility which Mess With DNS’s users don’t have access to in this context)

I ended up handling this in two ways:

  1. Do some initial basic validation of values that users enter (like IP addresses), so I can just return errors like Invalid IPv4 address: "1.2.3.4$
  2. If that goes well, send the request to PowerDNS and if we get an error back, then do some hacky translation of those messages to make them clearer.

Sometimes users will still get errors from PowerDNS directly, but I added some logging of all the errors that users see, so hopefully I can review them and add extra translations if there are other common errors that come up.

I think what I learned from this is that if I’m building a user-facing application on top of an API, I need to be pretty thoughtful about how I resurface those errors to users.

challenge 5: setting up SQLite

Previously Mess With DNS was using a Postgres database. This was problematic because I only gave the Postgres machine 256MB of RAM, which meant that the database got OOM killed almost every single day. I never really worked out exactly why it got OOM killed every day, but that’s how it was. I spent some time trying to tune Postgres’ memory usage by setting the max connections / work-mem / maintenance-work-mem and it helped a bit but didn’t solve the problem.

So for this refactor I decided to use SQLite instead, because the website doesn’t really get that much traffic. There are some choices involved with using SQLite, and I decided to:

  1. Run db.SetMaxOpenConns(1) to make sure that we only open 1 connection to the database at a time, to prevent SQLITE_BUSY errors from two threads trying to access the database at the same time (just setting WAL mode didn’t work)
  2. Use separate databases for each of the 3 tables (users, records, and requests) to reduce contention. This maybe isn’t really necessary, but there was no reason I needed the tables to be in the same database so I figured I’d set up separate databases to be safe.
  3. Use the cgo-free modernc.org/sqlite, which translates SQLite’s source code to Go. I might switch to a more “normal” sqlite implementation instead at some point and use cgo though. I think the main reason I prefer to avoid cgo is that cgo has landed me with difficult-to-debug errors in the past.
  4. use WAL mode

I still haven’t set up backups, though I don’t think my Postgres database had backups either. I think I’m unlikely to use litestream for backups – Mess With DNS is very far from a critical application, and I think daily backups that I could recover from in case of a disaster are more than good enough.

challenge 6: upgrading Vue & managing forms

This has nothing to do with PowerDNS but I decided to upgrade Vue.js from version 2 to 3 as part of this refresh. The main problem with that is that the form validation library I was using (FormKit) completely changed its API between Vue 2 and Vue 3, so I decided to just stop using it instead of learning the new API.

I ended up switching to some form validation tools that are built into the browser like required and oninvalid (here’s the code). I think it could use some of improvement, I still don’t understand forms very well.

challenge 7: managing state in the frontend

This also has nothing to do with PowerDNS, but when modifying the frontend I realized that my state management in the frontend was a mess – in every place where I made an API request to the backend, I had to try to remember to add a “refresh records” call after that in every place that I’d modified the state and I wasn’t always consistent about it.

With some more advice from Marco, I ended up implementing a single global state management store which stores all the state for the application, and which lets me create/update/delete records.

Then my components can just call store.createRecord(record), and the store will automatically resynchronize all of the state as needed.

challenge 8: sequencing the project

This project ended up having several steps because I reworked the whole integration between the frontend and the backend. I ended up splitting it into a few different phases:

  1. Upgrade Vue from v2 to v3
  2. Make the state management store
  3. Implement a different backend API, move a lot of DNS logic out of the frontend, and add tests for the backend
  4. Integrate PowerDNS

I made sure that the website was (more or less) 100% working and then deployed it in between phases, so that the amount of changes I was managing at a time stayed somewhat under control.

the new website is up now!

I released the upgraded website a few days ago and it seems to work! The PowerDNS API has been great to work on top of, and I’m relieved that there’s a whole class of problems that I now don’t have to think about at all, other than potentially trying to make the error messages from PowerDNS a little clearer. Using PowerDNS has fixed a lot of the DNS issues that folks have reported in the last few years and it feels great.

If you run into problems with the new Mess With DNS I’d love to hear about them here.

2024-08-06T08:38:35+00:00 Fullscreen Open in Tab
Go structs are copied on assignment (and other things about Go I'd missed)

I’ve been writing Go pretty casually for years – the backends for all of my playgrounds (nginx, dns, memory, more DNS) are written in Go, but many of those projects are just a few hundred lines and I don’t come back to those codebases much.

I thought I more or less understood the basics of the language, but this week I’ve been writing a lot more Go than usual while working on some upgrades to Mess with DNS, and ran into a bug that revealed I was missing a very basic concept!

Then I posted about this on Mastodon and someone linked me to this very cool site (and book) called 100 Go Mistakes and How To Avoid Them by Teiva Harsanyi. It just came out in 2022 so it’s relatively new.

I decided to read through the site to see what else I was missing, and found a couple of other misconceptions I had about Go. I’ll talk about some of the mistakes that jumped out to me the most, but really the whole 100 Go Mistakes site is great and I’d recommend reading it.

Here’s the initial mistake that started me on this journey:

mistake 1: not understanding that structs are copied on assignment

Let’s say we have a struct:

type Thing struct {
    Name string
}

and this code:

thing := Thing{"record"}
other_thing := thing
other_thing.Name = "banana"
fmt.Println(thing)

This prints “record” and not “banana” (play.go.dev link), because thing is copied when you assign it to other_thing.

the problem this caused me: ranges

The bug I spent 2 hours of my life debugging last week was effectively this code (play.go.dev link):

type Thing struct {
  Name string
}
func findThing(things []Thing, name string) *Thing {
  for _, thing := range things {
    if thing.Name == name {
      return &thing
    }
  }
  return nil
}

func main() {
  things := []Thing{Thing{"record"}, Thing{"banana"}}
  thing := findThing(things, "record")
  thing.Name = "gramaphone"
  fmt.Println(things)
}

This prints out [{record} {banana}] – because findThing returned a copy, we didn’t change the name in the original array.

This mistake is #30 in 100 Go Mistakes.

I fixed the bug by changing it to something like this (play.go.dev link), which returns a reference to the item in the array we’re looking for instead of a copy.

func findThing(things []Thing, name string) *Thing {
  for i := range things {
    if things[i].Name == name {
      return &things[i]
    }
  }
  return nil
}

why didn’t I realize this?

When I learned that I was mistaken about how assignment worked in Go I was really taken aback, like – it’s such a basic fact about the language works! If I was wrong about that then what ELSE am I wrong about in Go????

My best guess for what happened is:

  1. I’ve heard for my whole life that when you define a function, you need to think about whether its arguments are passed by reference or by value
  2. So I’d thought about this in Go, and I knew that if you pass a struct as a value to a function, it gets copied – if you want to pass a reference then you have to pass a pointer
  3. But somehow it never occurred to me that you need to think about the same thing for assignments, perhaps because in most of the other languages I use (Python, JS, Java) I think everything is a reference anyway. Except for in Rust, where you do have values that you make copies of but I think most of the time I had to run .clone() explicitly. (though apparently structs will be automatically copied on assignment if the struct implements the Copy trait)
  4. Also obviously I just don’t write that much Go so I guess it’s never come up.

mistake 2: side effects appending slices (#25)

When you subset a slice with x[2:3], the original slice and the sub-slice share the same backing array, so if you append to the new slice, it can unintentionally change the old slice:

For example, this code prints [1 2 3 555 5] (code on play.go.dev)

x := []int{1, 2, 3, 4, 5}
y := x[2:3]
y = append(y, 555)
fmt.Println(x)

I don’t think this has ever actually happened to me, but it’s alarming and I’m very happy to know about it.

Apparently you can avoid this problem by changing y := x[2:3] to y := x[2:3:3], which restricts the new slice’s capacity so that appending to it will re-allocate a new slice. Here’s some code on play.go.dev that does that.

mistake 3: not understanding the different types of method receivers (#42)

This one isn’t a “mistake” exactly, but it’s been a source of confusion for me and it’s pretty simple so I’m glad to have it cleared up.

In Go you can declare methods in 2 different ways:

  1. func (t Thing) Function() (a “value receiver”)
  2. func (t *Thing) Function() (a “pointer receiver”)

My understanding now is that basically:

  • If you want the method to mutate the struct t, you need a pointer receiver.
  • If you want to make sure the method doesn’t mutate the struct t, use a value receiver.

Explanation #42 has a bunch of other interesting details though. There’s definitely still something I’m missing about value vs pointer receivers (I got a compile error related to them a couple of times in the last week that I still don’t understand), but hopefully I’ll run into that error again soon and I can figure it out.

more interesting things I noticed

Some more notes from 100 Go Mistakes:

Also there are some things that have tripped me up in the past, like:

this “100 common mistakes” format is great

I really appreciated this “100 common mistakes” format – it made it really easy for me to skim through the mistakes and very quickly mentally classify them into:

  1. yep, I know that
  2. not interested in that one right now
  3. WOW WAIT I DID NOT KNOW THAT, THAT IS VERY USEFUL!!!!

It looks like “100 Common Mistakes” is a series of books from Manning and they also have “100 Java Mistakes” and an upcoming “100 SQL Server Mistakes”.

Also I enjoyed what I’ve read of Effective Python by Brett Slatkin, which has a similar “here are a bunch of short Python style tips” structure where you can quickly skim it and take what’s useful to you. There’s also Effective C++, Effective Java, and probably more.

some other Go resources

other resources I’ve appreciated:

2024-07-21T12:54:40-07:00 Fullscreen Open in Tab
My IETF 120 Agenda

Here's where you can find me at IETF 120 in Vancouver!

Monday

  • 9:30 - 11:30 • alldispatch • Regency C/D
  • 13:00 - 15:00 • oauth • Plaza B
  • 18:30 - 19:30 • Hackdemo Happy Hour • Regency Hallway

Tuesday

  • 15:30 - 17:00 • oauth • Georgia A
  • 17:30 - 18:30 • oauth • Plaza B

Wednesday

  • 9:30 - 11:30 • wimse • Georgia A
  • 11:45 - 12:45 • Chairs Forum • Regency C/D
  • 17:30 - 19:30 • IETF Plenary • Regency A/B/C/D

Thursday

  • 17:00 - 18:00 • spice • Regency A/B
  • 18:30 - 19:30 • spice • Regency A/B

Friday

  • 13:00 - 15:00 • oauth • Regency A/B

My Current Drafts

2024-07-08T13:00:15+00:00 Fullscreen Open in Tab
Entering text in the terminal is complicated

The other day I asked what folks on Mastodon find confusing about working in the terminal, and one thing that stood out to me was “editing a command you already typed in”.

This really resonated with me: even though entering some text and editing it is a very “basic” task, it took me maybe 15 years of using the terminal every single day to get used to using Ctrl+A to go to the beginning of the line (or Ctrl+E for the end – I think I used Home/End instead).

So let’s talk about why entering text might be hard! I’ll also share a few tips that I wish I’d learned earlier.

it’s very inconsistent between programs

A big part of what makes entering text in the terminal hard is the inconsistency between how different programs handle entering text. For example:

  1. some programs (cat, nc, git commit --interactive, etc) don’t support using arrow keys at all: if you press arrow keys, you’ll just see ^[[D^[[D^[[C^[[C^
  2. many programs (like irb, python3 on a Linux machine and many many more) use the readline library, which gives you a lot of basic functionality (history, arrow keys, etc)
  3. some programs (like /usr/bin/python3 on my Mac) do support very basic features like arrow keys, but not other features like Ctrl+left or reverse searching with Ctrl+R
  4. some programs (like the fish shell or ipython3 or micro or vim) have their own fancy system for accepting input which is totally custom

So there’s a lot of variation! Let’s talk about each of those a little more.

mode 1: the baseline

First, there’s “the baseline” – what happens if a program just accepts text by calling fgets() or whatever and doing absolutely nothing else to provide a nicer experience. Here’s what using these tools typically looks for me – If I start the version of dash installed on my machine (a pretty minimal shell) press the left arrow keys, it just prints ^[[D to the terminal.

$ ls l-^[[D^[[D^[[D

At first it doesn’t seem like all of these “baseline” tools have much in common, but there are actually a few features that you get for free just from your terminal, without the program needing to do anything special at all.

The things you get for free are:

  1. typing in text, obviously
  2. backspace
  3. Ctrl+W, to delete the previous word
  4. Ctrl+U, to delete the whole line
  5. a few other things unrelated to text editing (like Ctrl+C to interrupt the process, Ctrl+Z to suspend, etc)

This is not great, but it means that if you want to delete a word you generally can do it with Ctrl+W instead of pressing backspace 15 times, even if you’re in an environment which is offering you absolutely zero features.

You can get a list of all the ctrl codes that your terminal supports with stty -a.

mode 2: tools that use readline

The next group is tools that use readline! Readline is a GNU library to make entering text more pleasant, and it’s very widely used.

My favourite readline keyboard shortcuts are:

  1. Ctrl+E (or End) to go to the end of the line
  2. Ctrl+A (or Home) to go to the beginning of the line
  3. Ctrl+left/right arrow to go back/forward 1 word
  4. up arrow to go back to the previous command
  5. Ctrl+R to search your history

And you can use Ctrl+W / Ctrl+U from the “baseline” list, though Ctrl+U deletes from the cursor to the beginning of the line instead of deleting the whole line. I think Ctrl+W might also have a slightly different definition of what a “word” is.

There are a lot more (here’s a full list), but those are the only ones that I personally use.

The bash shell is probably the most famous readline user (when you use Ctrl+R to search your history in bash, that feature actually comes from readline), but there are TONS of programs that use it – for example psql, irb, python3, etc.

tip: you can make ANYTHING use readline with rlwrap

One of my absolute favourite things is that if you have a program like nc without readline support, you can just run rlwrap nc to turn it into a program with readline support!

This is incredible and makes a lot of tools that are borderline unusable MUCH more pleasant to use. You can even apparently set up rlwrap to include your own custom autocompletions, though I’ve never tried that.

some reasons tools might not use readline

I think reasons tools might not use readline might include:

  • the program is very simple (like cat or nc) and maybe the maintainers don’t want to bring in a relatively large dependency
  • license reasons, if the program’s license is not GPL-compatible – readline is GPL-licensed, not LGPL
  • only a very small part of the program is interactive, and maybe readline support isn’t seen as important. For example git has a few interactive features (like git add -p), but not very many, and usually you’re just typing a single character like y or n – most of the time you need to really type something significant in git, it’ll drop you into a text editor instead.

For example idris2 says they don’t use readline to keep dependencies minimal and suggest using rlwrap to get better interactive features.

how to know if you’re using readline

The simplest test I can think of is to press Ctrl+R, and if you see:

(reverse-i-search)`':

then you’re probably using readline. This obviously isn’t a guarantee (some other library could use the term reverse-i-search too!), but I don’t know of another system that uses that specific term to refer to searching history.

the readline keybindings come from Emacs

Because I’m a vim user, It took me a very long time to understand where these keybindings come from (why Ctrl+A to go to the beginning of a line??? so weird!)

My understanding is these keybindings actually come from Emacs – Ctrl+A and Ctrl+E do the same thing in Emacs as they do in Readline and I assume the other keyboard shortcuts mostly do as well, though I tried out Ctrl+W and Ctrl+U in Emacs and they don’t do the same thing as they do in the terminal so I guess there are some differences.

There’s some more history of the Readline project here.

mode 3: another input library (like libedit)

On my Mac laptop, /usr/bin/python3 is in a weird middle ground where it supports some readline features (for example the arrow keys), but not the other ones. For example when I press Ctrl+left arrow, it prints out ;5D, like this:

$ python3
>>> importt subprocess;5D

Folks on Mastodon helped me figure out that this is because in the default Python install on Mac OS, the Python readline module is actually backed by libedit, which is a similar library which has fewer features, presumably because Readline is GPL licensed.

Here’s how I was eventually able to figure out that Python was using libedit on my system:

$ python3 -c "import readline; print(readline.__doc__)"
Importing this module enables command line editing using libedit readline.

Generally Python uses readline though if you install it on Linux or through Homebrew. It’s just that the specific version that Apple includes on their systems doesn’t have readline. Also Python 3.13 is going to remove the readline dependency in favour of a custom library, so “Python uses readline” won’t be true in the future.

I assume that there are more programs on my Mac that use libedit but I haven’t looked into it.

mode 4: something custom

The last group of programs is programs that have their own custom (and sometimes much fancier!) system for editing text. This includes:

  • most terminal text editors (nano, micro, vim, emacs, etc)
  • some shells (like fish), for example it seems like fish supports Ctrl+Z for undo when typing in a command. Zsh’s line editor is called zle.
  • some REPLs (like ipython), for example IPython uses the prompt_toolkit library instead of readline
  • lots of other programs (like atuin)

Some features you might see are:

  • better autocomplete which is more customized to the tool
  • nicer history management (for example with syntax highlighting) than the default you get from readline
  • more keyboard shortcuts

custom input systems are often readline-inspired

I went looking at how Atuin (a wonderful tool for searching your shell history that I started using recently) handles text input. Looking at the code and some of the discussion around it, their implementation is custom but it’s inspired by readline, which makes sense to me – a lot of users are used to those keybindings, and it’s convenient for them to work even though atuin doesn’t use readline.

prompt_toolkit (the library IPython uses) is similar – it actually supports a lot of options (including vi-like keybindings), but the default is to support the readline-style keybindings.

This is like how you see a lot of programs which support very basic vim keybindings (like j for down and k for up). For example Fastmail supports j and k even though most of its other keybindings don’t have much relationship to vim.

I assume that most “readline-inspired” custom input systems have various subtle incompatibilities with readline, but this doesn’t really bother me at all personally because I’m extremely ignorant of most of readline’s features. I only use maybe 5 keyboard shortcuts, so as long as they support the 5 basic commands I know (which they always do!) I feel pretty comfortable. And usually these custom systems have much better autocomplete than you’d get from just using readline, so generally I prefer them over readline.

lots of shells support vi keybindings

Bash, zsh, and fish all have a “vi mode” for entering text. In a very unscientific poll I ran on Mastodon, 12% of people said they use it, so it seems pretty popular.

Readline also has a “vi mode” (which is how Bash’s support for it works), so by extension lots of other programs have it too.

I’ve always thought that vi mode seems really cool, but for some reason even though I’m a vim user it’s never stuck for me.

understanding what situation you’re in really helps

I’ve spent a lot of my life being confused about why a command line application I was using wasn’t behaving the way I wanted, and it feels good to be able to more or less understand what’s going on.

I think this is roughly my mental flowchart when I’m entering text at a command line prompt:

  1. Do the arrow keys not work? Probably there’s no input system at all, but at least I can use Ctrl+W and Ctrl+U, and I can rlwrap the tool if I want more features.
  2. Does Ctrl+R print reverse-i-search? Probably it’s readline, so I can use all of the readline shortcuts I’m used to, and I know I can get some basic history and press up arrow to get the previous command.
  3. Does Ctrl+R do something else? This is probably some custom input library: it’ll probably act more or less like readline, and I can check the documentation if I really want to know how it works.

Being able to diagnose what’s going on like this makes the command line feel a more predictable and less chaotic.

some things this post left out

There are lots more complications related to entering text that we didn’t talk about at all here, like:

  • issues related to ssh / tmux / etc
  • the TERM environment variable
  • how different terminals (gnome terminal, iTerm, xterm, etc) have different kinds of support for copying/pasting text
  • unicode
  • probably a lot more