10 interesting stories served every morning and every evening.

LLMs are eroding my software engineering career and I don't know what to do

human-in-the-loop.bearblog.dev

06 Jun, 2026

I’m a soft­ware en­gi­neer, com­plet­ing 10 years of pro­fes­sional ex­pe­ri­ence this year. I started my ca­reer as a web fron­tend en­gi­neer (it was eas­ier for me to de­bug fron­tend code back then, so I chose that path), but shortly tran­si­tioned to (web) back­end and never looked back.

Through a se­ries of co­in­ci­dences, once I stepped into back­end de­vel­op­ment, I ended up work­ing in soft­ware de­vel­op­ment roles in the do­mains of fi­nance, book­keep­ing and pay­ment pro­cess­ing, where I had great au­ton­omy and a close and can­did re­la­tion­ship with Product Managers and stake­hold­ers.

I learnt a lot about the do­main and how to ef­fec­tively write pro­grams for it: PCI com­pli­ance, dou­ble-en­try ledgers, es­crows, rec­on­cil­i­a­tion, pay­ment life­cy­cles, bank trans­fer idem­po­tency, etc.

It was, then, ob­vi­ous that I should fo­cus my ca­reer on be­com­ing an ex­pert on that do­main to stand out as a pro­fes­sional and dif­fer­en­ti­ate my­self in a field that showed signs of an in­creas­ing need for do­main spe­cial­ists.

The first pil­lar to erode: do­main-spe­cific knowl­edge

Last year, I got hired by a com­pany in the fi­nance work­space. So far, I had worked on com­pa­nies that do have a strong pay­ment and fi­nance com­po­nent to their op­er­a­tions/​of­fer­ings, but that were not solely fi­nance-fo­cused com­pa­nies.

That com­pany also em­braced AI whole­heart­edly, so I got ChatGPT and Claude Enterprise ac­counts from day one and was en­cour­aged to use them for my re­search, ex­plo­ration, and even cod­ing, al­beit with a warn­ing that I should still re­view and own every sin­gle line that made it into pro­duc­tion.

One of my first pro­jects in­volved re­work­ing the legacy on­line pay­ment sys­tem, which was a mess. They hired me for (among other things) my pre­vi­ous ex­pe­ri­ence in build­ing that and trusted me with the task.

Different from the other com­pa­nies I had worked for so far, they wanted the Design Docs” I write be­fore cod­ing to be read­able by both en­gi­neers and prod­uct man­agers - so they should­n’t be a tech­ni­cal deep dive and more of an ar­chi­tec­tural view. I wrote my first one with min­i­mal AI as­sis­tance - I even called LLMs stochastic par­rots” at the time, a view I no longer hold - and de­liv­ered it.

I val­ued my knowl­edge and thought no LLMs could re­place it.

Then my man­ager reached out to me: even though you’re de­liv­er­ing code at a good pace, you’re tak­ing too long to de­liver those Design Docs. Are you us­ing AI? You should use more AI.

No way this will work”, I thought in my head, but agreed. The mod­els at that time were not as good as the ones we have now, but they did pro­vide a good speed-up on my writ­ing and even the de­ci­sion-mak­ing.

And then I started re­al­iz­ing: all the knowl­edge I have ac­cu­mu­lated over the years: the trade-offs be­tween im­ple­men­ta­tions, how ac­quir­ing works, how to struc­ture idem­po­tency to pre­vent dou­ble-charges, every­thing, was be­com­ing use­less. Even though the mod­els still needed some steer­ing, they could con­nect the dots on how to struc­ture such sys­tems, which was the hard­est part that only de­vel­ops in your brain af­ter years of hands-on ex­pe­ri­ence. That was my first shock.

But sure, I thought, they can do that be­cause there’s plenty of ar­ti­cles on the web on how that shit works along with all the tech­ni­cal doc­u­men­ta­tion, and we have blog posts ex­plain­ing how to ap­ply the tech­ni­cal tools to the do­main. For hu­mans, it may take a long time to learn all that, but that’s train­ing data so the mod­els can pick it up.

What the mod­els will never be good at, and that’s where hu­mans will shine, is de­bug­ging! I had ac­cu­mu­lated a good ex­pe­ri­ence de­bug­ging race con­di­tions and dis­trib­uted sys­tems in pro­duc­tion. That was my ticket to long-term em­ploy­a­bil­ity.

The sec­ond pil­lar to erode: de­bug­ging and dis­trib­uted sys­tems

So, af­ter LLMs started get­ting good at writ­ing docs and help­ing plan the ac­tual im­ple­men­ta­tions, they be­came good at cod­ing. It started in the sec­ond half of 2025 with the Claude Code hype, then Codex came and so on. Although I was us­ing LLMs for writ­ing unit tests every day be­fore that, I was­n’t trust­ing them to write the full im­ple­men­ta­tion yet.

The nat­ural next step was to in­tro­duce more AI into writ­ing code. And hon­estly, I liked it. I like ship­ping things to pro­duc­tion and see­ing users happy as much as I like cod­ing, so I was trad­ing one thing that I like for an­other one that I also like, it was fair.

LLMs were be­com­ing good at cod­ing, but it still could­n’t de­bug the mess left be­hind (by then or by the hu­mans), so I still had a role that was big­ger than steer­ing the ro­bot - a ticket to em­ploy­a­bil­ity.

Everything seemed fine.

Then came the MCPs, the agen­tic work­flows and Claude 4.5 and the sky started to fall.

Claude 4.5, to be hon­est, was­n’t that good. It solved like 60% of the bugs given a stack trace and some con­text (a Sentry link with Sentry MCP en­abled was all it took in most cases). Sometimes it gave a so­lu­tion that sounded plau­si­ble but was to­tally wrong.

This time, how­ever, I stopped doubt­ing the ma­chines. I saw bugs that in the past would eas­ily take 1 day of full-time de­bug­ging be­ing one-shot­ted by Claude Code. Of course, not all of them yet, but the pat­tern was clear.

Then came 4.6, 4.7, GPT 5.5, Opus 4.8 and the DataDog MCP… Now I have CLIs that one-shots bugs across dis­trib­uted sys­tems for me. Bugs that I could­n’t solve in the past. Bugs that would take 2 days of full-time de­bug­ging. Bugs across dis­trib­uted sys­tems that lack dis­trib­uted ob­serv­abil­ity. 90% of the bugs are one-shot­ted now, in­clud­ing bizarre race con­di­tions, un­ex­pected cor­ner-cases, third-party in­te­gra­tion is­sues, un­doc­u­mented API edge cases, every­thing. I hardly have to in­ter­vene.

Of course, I’m still em­ploy­able be­cause some­one has to re­view the code and steer the ro­bot. But I’m just an­other off-the-shelf en­gi­neer now. I have no do­main ex­per­tise that an­other Sr. en­gi­neer steer­ing an LLM can­not match. All my fi­nance and pay­ment do­main ex­per­tise, all the de­bug­ging in­tu­ition and dis­trib­uted sys­tem knowl­edge earned through hours of sweat and tears, is now prompt­able.

We were taught that gen­er­al­ists and spe­cial­ists will al­ways have their roles. But now the mar­ket is shap­ing every­one into be­com­ing a gen­er­al­ist. That’s not a bad thing per se, un­til you look un­der the eco­nom­ics of sup­ply and de­mand: if every­one is a gen­er­al­ist, the price of a gen­er­al­ist falls if there’s no de­mand to match. And we all know the de­mand is dry­ing up.

The third pil­lar, the one that has­n’t eroded yet: code qual­ity and ar­chi­tec­ture

I still have one pil­lar stand­ing, though: code qual­ity and soft­ware ar­chi­tec­ture - what’s now be­ing re­duced to be­ing called taste” 1.

Along the course of my ca­reer, I al­ways liked to refac­tor, al­ways prized good code, and ne­go­ti­ated time in the sprint for it. DDD, Hexagonal, Clean Architecture, you know all the buzz­words. I like this topic, I like to dis­cuss the trade-offs and dif­fer­ent ideas on how to shape code­bases. I re­ally like it.

This is the last pil­lar stand­ing. Except that no­body cares any­more.

Agents do a re­ally bad job at keep­ing code­bases or­ga­nized. If you don’t steer them, they’ll hit a cir­cu­lar de­pen­dency is­sue sooner than you think. Will du­pli­cate code. Add un­nec­es­sary com­ments. Mix up pure func­tions and side-ef­fects. Disregard the prin­ci­ples of SOLID.

That should keep hu­mans em­ployed, ex­cept that this skill is now be­ing re­duced to the word taste”. But it’s not just a re­nam­ing, the in­dus­try is mov­ing to a world where code or­ga­ni­za­tion is less im­por­tant.

Sure, hu­mans should steer the agent to pre­vent spaghetti code­bases with cir­cu­lar de­pen­dency graphs. We don’t want F-rated code­bases that are im­pos­si­ble to touch with­out break­ing some­thing. But a C or D? It’s now fine. Nobody needs A or B-grade code­bases any­more be­cause they’re be­ing made for LLMs, not for hu­mans to read.

I don’t want to ar­gue if this is in­her­ently good or bad. If the source code is now writ­ten for ma­chines to read and not hu­mans, it may be ac­tu­ally ok to tar­get them.

But that’s an­other pil­lar of my ex­per­tise that’s erod­ing. A good chunk of the knowl­edge I ac­cu­mu­lated on that topic is not that valu­able any­more. All the time I spent on it - read­ing books, do­ing real-world ex­er­cises, dis­cussing with other en­gi­neers, writ­ing ADRs - is be­com­ing use­less.

What now?

I’m still em­ployed and I see my­self em­ployed (at least in that com­pany) for a fore­see­able fu­ture. But I don’t know what to think about the long-term.

I spent 10 years (even more when you ac­count for non-pro­fes­sion ex­pe­ri­ence) get­ting good at things that are be­com­ing less and less valu­able. My last pil­lar of ex­per­tise is now re­duced to a taste” and will prob­a­bly won’t last long.

And I know that’s not just me. About 8 months ago there was a lay­off at my cur­rent com­pany (not re­lated to AI, ac­cord­ing to them). Some bril­liant ex-cowork­ers were laid off and are still look­ing for jobs. Most of them suf­fer from the same prob­lem I out­lined here: their do­main ex­per­tise is not enough to stand out any­more.

The com­pany is now hir­ing again for a few roles and do­main fa­mil­iar­ity is not a strong dif­fer­en­tia­tor any­more. We used to list Software Engineer - Area”. Now it’s just Software Engineer” and the team as­sign­ment comes af­ter the of­fer is ac­cepted.

Of course, this is good for bril­liant en­gi­neers that never had the chance to get deep into the do­main and now have bet­ter chances at get­ting a job, but it’s also sad to think that other bril­liant en­gi­neers that spent their lives col­lect­ing do­main knowl­edge are now com­pet­ing on the same lane.

The only way out for keep­ing my em­ploy­a­bil­ity in the long-term now seems to be shift­ing my do­main ex­per­tise to some­thing LLMs will not get good at so eas­ily. But what’s left?

I thought about go­ing back to col­lege, learn­ing Math, Statistics, ad­vanced Machine Learning and ap­ply­ing for re­search role at a fron­tier lab. Except that there are no fron­tier labs in my coun­try, the few ones that ex­ist are flood­ing with ap­pli­ca­tions and I have fam­ily mat­ters that makes mov­ing to an­other coun­try dif­fi­cult. By the time I can af­ford to make that jump, RSI may have made re­searchers ob­so­lete.

Maybe I should con­sider trans­form­ing my wood­work­ing hobby into a pro­fes­sion…

Update (Jun 7): this post went vi­ral. I wrote an­other post re­ply­ing to some com­ments from so­cial me­dia and ex­pand­ing some of my ar­gu­ments. You can read it here.

See this, this and this for ref­er­ence. Don’t take this as an en­dorse­ment of the con­tent in­side any of these posts.↩

See this, this and this for ref­er­ence. Don’t take this as an en­dorse­ment of the con­tent in­side any of these posts.↩

#ai

#llm

#software en­gi­neer­ing

Building from Zero After Addiction, Prison, and a Felony

gavinray97.github.io

Building from Zero After Addiction, Prison, and a Felony

I spent ages 14 – 16 in a max­i­mum-se­cu­rity ju­ve­nile prison, be­came a felon at 19, lost al­most every­thing to ad­dic­tion, and later re­built my life through soft­ware, open source, and a few peo­ple who took a chance on me.

I’ve wanted to write this for a while, but kept find­ing rea­sons not to. It felt too per­sonal, too risky, and too easy to mis­read.

Recently, I de­cided on two things:

After see­ing Preston Thorpe speak pub­licly about his own back­ground, I won­dered how many oth­ers like us were silently lurk­ing in tech

I’m far enough in my ca­reer with enough con­tri­bu­tions to OSS and com­mu­nity in­volve­ment, that I think I’ll prob­a­bly be al­right

I wrote this for any­one qui­etly won­der­ing whether they have no chance at a fu­ture.

Below is the much-con­densed life story of my strug­gles with ad­dic­tion, poverty, and in­car­cer­a­tion + life af­ter be­ing a felon. My hope is that it serves as en­cour­age­ment to oth­ers who are in sim­i­lar cir­cum­stances that things CAN get bet­ter.

Amphetamine Addict and Prison at 14

I was a model stu­dent up un­til around pu­berty and mid­dle school. Then, I think a com­bi­na­tion of be­ing bul­lied for be­ing over­weight and teenage hor­mones, led me to be just the wrong com­bi­na­tion of re­sent­ful, an­gry, un­happy, and re­bel­lious.

I started get­ting in fist­fights with peo­ple that made fun of me, be­ing a huge ass­hole to teach­ers, stopped do­ing school­work, and started ex­per­i­ment­ing with drugs.

The be­gin­ning of the end: The day I bought an Adderall from a class­mate. When that am­phet­a­mine feel­ing kicked-in, it was as if life was per­fect for the first time. I was happy, con­fi­dent, felt I could do any­thing. I wanted to feel this way every wak­ing mo­ment for the rest of my life.

Being 14, I had no job, and I do not come from money. So, log­i­cally, I did the thing one must do if one wishes to sus­tain a drug habit: Devise a way to make money.

The eas­i­est way to make money at 14 turned out to be deal­ing drugs, so I started sell­ing var­i­ous pre­scrip­tion med­ica­tions on a buy-low-sell-high” ba­sis from other stu­dents at school.

This was short-lived, as I had the huge mouth of a re­bel­lious I’m in­vin­ci­ble” 14-year old boy, and I was shortly ar­rested and charged with 17 counts of Possession with Intent to Manufacture or Distribute a Scheduled II Controlled Substance.

I wound up spend­ing 2 years, from 14 – 16 at a max­i­mum se­cu­rity ju­ve­nile prison (Lookout Mountain YSC, Golden CO).

Freedom - Shortly Lived

In prison, I got my GED, and af­ter re­lease briefly en­rolled in com­mu­nity col­lege. I was work­ing as a land­scaper do­ing man­ual la­bor for $8/hr and then rid­ing a bus 1hr each way to night classes. Not to say this sort of thing can’t be done (people do it all the time), but I did­n’t have the tenac­ity or mo­ti­va­tion to keep it up, so I dropped out.

I stayed sober for a brief pe­riod be­tween 16 – 17. Not hav­ing learned my les­son, I again started sell­ing drugs. I had learned about The Silk Road and the Darknet and was or­der­ing (what was then) a le­gal Research Chemical” with ef­fects sim­i­lar to MDMA (Methylone/bk-MDMA) shipped to my par­ents house. Eventually, my dad got home early from work and in­ter­cepted a pack­age. Asking me what it was be­fore I left for work, I told him I don’t know, never heard of the re­turn ad­dress name”. My fa­ther was not an id­iot; he told me he was go­ing to open it while I was at work, so I con­fessed it’s drugs.”

Cue huge ar­gu­ment, him in­sist­ing he was go­ing to re­move every­thing from my room ex­cept my clothes and bed (most of which I paid for my­self) and I would not be al­lowed to leave ex­cept for work. This was not an agree­able cir­cum­stance to me, so I re­fused — at which point my dad said then you won’t be liv­ing here any­more!”.

It’s im­por­tant to note that in Colorado (at the time, at least), eman­ci­pa­tion of a mi­nor was not a sta­tus one could file for, but in­stead purely a court sta­tus to be rec­og­nized dur­ing le­gal pro­ceed­ings. That meant there was tech­ni­cally no av­enue for me to legally move out be­fore 18 with proper le­gal sta­tus.

So this, to me, sounded like sweet free­dom & re­lease, rather than a pun­ish­ment. You re­ally won’t call the po­lice if I leave?” Nope.” I packed my back­pack with my lap­top and cash sav­ings, and a suit­case with my clothes, and left. I had no plan but that was a bridge to be crossed.

It turned out that the par­ents of a friend had an un­used bed­room in their trailer they would rent to me un­der-the-table for $300/mo. I jumped at that and slept on the floor of a trailer for 6 months.

I worked as a land­scaper, at a lum­ber mill, and as a cashier at Walgreens, con­tin­u­ing to sell drugs on the side.

Inevitably, I wound up be­ing ar­rested again on drug-re­lated charges, and spent 18 – 19 in county jail. It was then that I be­came a con­victed felon with a low-class felony.

A Serendipitous News Article & a Software Job

While I was in county jail, one day the news­pa­per had a small ar­ti­cle in it: Tech com­pany of­fers in­tern­ships to at-risk & un­der­priv­i­leged youth”

I had spent my child­hood on the com­puter, play­ing videogames and even­tu­ally teach­ing my­self to pro­gram to make game mods. I knew from a young age I wanted to be a pro­gram­mer (I thought I wanted to make videogames, as most young chil­dren do).

This to me, seemed like a for­tu­itous op­por­tu­nity. I cut the ar­ti­cle out and put it in a doc­u­ments folder.

Eventually, I was moved from reg­u­lar jail pop­u­la­tion into the Work-Release jail pro­gram, where they let you out dur­ing the day for work. You had 1 week to find a job, and if you could­n’t se­cure em­ploy­ment you were sent back per­ma­nently to fin­ish out your sen­tence.

The first day out, I walked into the of­fices of the com­pany from the ar­ti­cle and asked to speak to some­one. I ex­plained that I was fresh out of jail and had seen their ar­ti­cle while in­side.

They in­ter­viewed me, de­cided to hire me, and I was now an in­tern Full-Stack Web Developer! I knew noth­ing of web dev, and did­n’t even par­tic­u­larly have an in­ter­est in it orig­i­nally, but the job was al­ready be­yond my hopes. I had as­sumed I was go­ing to spend the rest of my life work­ing con­struc­tion or sim­i­lar, be­cause of my felony.

The same news re­porter that had done the orig­i­nal ar­ti­cle later came to visit, and af­ter in­ter­view­ing me, did a whole writeup on it!

https://​www.dai­ly­cam­era.com/​2017/​05/​12/​boul­der-tech-acad­e­mies-swamped-as-they-race-to-re­train-work­ers/

Working at Techtonic was the best pos­si­ble early-ca­reer ex­pe­ri­ence I think any­one could have had. They did con­tract de­vel­op­ment, a lot of which was green­field Saas MVP launches, across var­i­ous tech stacks. There was not a lot of time for men­tor­ship so it a very trial-by-fire” ex­pe­ri­ence — ei­ther fig­ure things out and ship stuff, or get the boot.

I learned fron­tend, back­end, and dev-ops while there and worked across sev­eral lan­guages + DBs. This was around the time Ruby on Rails + MongoDB was the hip thing. ES6 JS was still fresh and new, and it was there that our CTO did a com­pany meet­ing on this new thing called React” that we were to start learn­ing to re­place jQuery.

It’s also where I met my now-wife, who I pulled into my drug use and un­sta­ble life.

Drugs, Part 2: Electric Boogaloo

Being pos­si­bly the most hard­headed in­di­vid­ual in the uni­verse, I fell back into drug use shortly there­after. I man­aged to re­main mostly-func­tional, un­til the man­ager at Techtonic (who did not like me) lied to the owner that I was show­ing up hours late every day.

She fired me (and my now-wife), and I was later re­deemed when they found the truth in his Slack mes­sage his­tory af­ter fir­ing him many moons later. But oh whale, them’s the breaks”, as they say.

Not hav­ing a job, I spi­raled harder into ad­dic­tion, and even­tu­ally ran out of money to pay my rent and bills. We moved in with my bi­o­log­i­cal fa­ther in Florida. He was also an ad­dict, and in­stead of sta­bil­ity, the sit­u­a­tion be­came en­abling and de­struc­tive. It ex­ploded in short or­der.

From Zero

After the liv­ing sit­u­a­tion with my fa­ther ex­ploded, I was for­tu­nate enough to have a friend who had a spare room in the house and agreed to let me and my (now wife) stay with them for some tiny sum of money, but only tem­porar­ily un­til we could find work + save enough to move out and get back on our feet.

It was at this point we had noth­ing: A few dol­lars to our name, no ve­hi­cle, some cloth­ing and a sin­gle lap­top.

I had lost every­thing. And I had dragged this poor woman into it with me who had lost every­thing, too.

It was at this point my so­bri­ety be­gan. I had hit what we ad­dicts call a bot­tom”. Not the first one, but the one that was fi­nally grim and bleak enough to make me look at my­self and go What the fuck are you do­ing?” The one that fi­nally knocked it into my skull that I did­n’t want to live like this any­more.

I started wash­ing dishes at a restau­rant, and my wife took a job de­liv­er­ing and in­stalling large ap­pli­ances (ovens, fridges, etc) at the same ware­house where the friend worked. Having no ve­hi­cle, she had to bor­row the friend’s bi­cy­cle and ride 30 min­utes in the dark be­fore work, and 30 min­utes in the swel­ter­ing heat af­ter work home. The hours were very long, be­cause it was of­ten on-site in­stal­la­tion paid by the ap­pli­ance, so many days she would work 10 – 12 hours + 1 hour bike ride, and come home so ex­hausted all she could do was sob a lit­tle be­fore get­ting just enough sleep to do it all over again the next day.

Eventually, she told me that it made more sense for me to quit my job while she worked, so that I could spend all of my free time try­ing to get an­other tech job. So she alone car­ried us for sev­eral months. I sent out hun­dreds of ap­pli­ca­tions. I went through fi­nal-round in­ter­views and re­ceived of­fer let­ters from 8 com­pa­nies, only to have them re­scinded each time due to cor­po­rate No Felons” HR poli­cies. It was like hav­ing the car­rot dan­gled right in front of my face, to be snatched away each time.

Finally, I got an in­ter­view with a tiny startup in Miami. I passed their phone screen, and drove 4 hours each way to do the in-per­son.

They of­fered me the job, and helped pay for us to re­lo­cate and tem­porar­ily stay in Airbnb’s. It paid $50k, with the promise of a sig­nif­i­cant raise in 1 year when the com­pany had more rev­enue. I was over­joyed with the of­fer and im­me­di­ately ac­cepted.

Hasura, Open Source, and the Door That Stayed Open

The sys­tem at work was an age­ing Rails app that had ac­crued sig­nif­i­cant tech debt and was the re­sult of an amal­gam of out­sourced de­vel­op­ment shops. One of them was clearly quite pro­fi­cient, and the oth­ers… not so much. Part of my job was de­sign­ing and im­ple­ment­ing a V2 rewrite. While eval­u­at­ing tech­nolo­gies for this, I stum­bled upon Hasura

https://​github.com/​ha­sura/​graphql-en­gine

Put sim­ply, it au­to­mated the work of gen­er­at­ing CRUD for Postgres apps, and was de­signed by peo­ple who clearly had hit the lim­i­ta­tions of tra­di­tional Backend-as-a-Service type plat­forms. Only core CRUD was au­to­mated, and you in­te­grated the rest of your app through wiring up your own API end­points and im­ple­ment­ing your own AuthN + AuthZ.

The first time I plugged in our lo­cal­host Postgres URL for dev, and had a full work­ing CRUD API, I was hooked. Coming from a back­ground of rapidly churn­ing out SaaS MVPs, this was solv­ing a very real prob­lem for me, and it was PERFORMANT.

I be­came heav­ily in­volved in the Discord server, an­swer­ing other peo­ple’s ques­tions, and also started send­ing PRs to im­ple­ment fea­tures I felt were miss­ing.

When my 1 year an­niver­sary came around at work, the founders un­for­tu­nately still were not in a po­si­tion to pay me much more. I knew the fi­nan­cials of the busi­ness and they weren’t ly­ing, but it was still some­what of a dis­ap­point­ment. One of the Hasura em­ploy­ees had re­cently made a joke that I should just ap­ply to work there. I fig­ured that it could­n’t hurt to at least get more info.

I went through the in­ter­view rounds more as a for­mal­ity and was given an of­fer let­ter. I was of­fered slightly more than dou­ble my cur­rent salary! I gen­uinely loved work­ing with the founders at my cur­rent job and felt ter­ri­ble about leav­ing, but I did ac­cept the of­fer and stay on for an­other month to fin­ish up cur­rent work and make sure there was some­one to hand off to.

The com­pany was so small back then that there was no back­ground check done dur­ing the in­ter­view process. After work­ing there a while, I even­tu­ally dis­closed to the Hasura founders that I had a low-grade felony, and, thank the stars, they were cool with it.

I had my dream job: Working on a de­vel­oper-fac­ing tool I gen­uinely loved and was a power-user of, that was also part of the Postgres ecosys­tem. I could not have con­ceived of such a per­fectly-fit po­si­tion. I have been work­ing at Hasura (now PromptQL) since 2020, and I plan on rid­ing this one all the way to it’s end: ei­ther fired, bank­rupt, or bought-out. (Hopefully bought-out).

Conclusion

I don’t tell this story be­cause I think it is clean, heroic, or uni­ver­sally ap­plic­a­ble — It is­n’t. I made TERRIBLE choices. I hurt peo­ple who loved me. I wasted chances that other peo­ple would have killed for. And even when I fi­nally started do­ing the right things, I still needed luck, help, tim­ing, for­give­ness, and peo­ple will­ing to judge me by what I could do next in­stead of only by what I had done be­fore.

But that is ex­actly why I wanted to write this.

If you are read­ing this from the mid­dle of ad­dic­tion, poverty, a crim­i­nal record, or some other hole that feels per­ma­nent: I won’t in­sult you by claim­ing it’s easy. It may be un­fair for a long time. You may have to hear no” from peo­ple who never even look at your work. You may have to re­build with less room for mis­take than every­one around you.

But you are not nec­es­sar­ily fin­ished.

And if you are in a po­si­tion to hire, men­tor, re­view pull re­quests, or let some­one into a room they nor­mally would not be al­lowed into: please re­mem­ber that tal­ent is not evenly dis­trib­uted by back­ground check. Sometimes the per­son who looks risky on pa­per is also the per­son who will spend years try­ing to be­come wor­thy of the chance they were given.

I am alive, sober, mar­ried, em­ployed, and work­ing on soft­ware I care about be­cause a hand­ful of peo­ple took that risk on me.

I wake up grate­ful for that every day. And I hope, over time, to be­come the kind of per­son who gives that same chance to some­one else.

AI Use Disclaimer: claude code was used to gen­er­ate the OpenGraph SVG im­age.

No part of the prose was ma­chine-gen­er­ated. You will not find ma­chine-writ­ten prose on this blog. I con­sider it deeply dis­re­spect­ful.

[FEATURE] Official Claude Desktop build for Linux (Ubuntu LTS / Debian)

github.com

Preflight Checklist

I have searched ex­ist­ing re­quests and this fea­ture has­n’t been re­quested yet

This is a sin­gle fea­ture re­quest (not mul­ti­ple fea­tures)

Problem Statement

Preflight note. The clos­est open is­sue is #40347. Related: #47316 (closed), #38276 (closed as out of scope for this repo), #36011 (stale). I am fil­ing this as a con­sol­i­da­tion and ex­ten­sion of #40347 with cor­rected tech­ni­cal fram­ing (Claude Code plu­gin de­vel­op­ment against Desktop ex­ten­sions), named pri­mary sourc­ing for the Cowork Linux-VM ar­chi­tec­ture, and cur­rent mar­ket data. Happy to merge into #40347 if main­tain­ers pre­fer; please route rather than close if a dif­fer­ent venue is cor­rect.

On scope: this is­sue con­cerns Claude Code in two con­crete ways. (1) Claude Code plu­g­ins are de­vel­oped and tested against Claude Desktop ex­ten­sions, which has no Linux build, so plu­gin work cur­rently re­quires switch­ing OS. (2) Cowork in­vokes the Claude Code bi­nary in­side a Linux VM on ma­cOS, so the Linux ex­e­cu­tion path al­ready ex­ists in­side the Claude Code prod­uct and is the prac­ti­cal thing miss­ing as a pub­lished tar­get.

What this is­sue is ask­ing for A pub­lic Anthropic po­si­tion on Linux desk­top sup­port, and ide­ally a first-party build. A rea­soned not on the cur­rent roadmap, and here is why” would re­solve most of what this is­sue is about. There is, to my knowl­edge, no pub­lic state­ment on Linux desk­top sup­port; the ab­sence is it­self part of the prob­lem.

Current state

Anthropic dis­trib­utes Claude Desktop for ma­cOS and Windows only. The of­fi­cial down­load page states Not avail­able for Linux”. Claude Code (the CLI) runs na­tively on Linux but is a ter­mi­nal tool, not a sub­sti­tute for the desk­top GUI. Desktop ex­ten­sions (the sur­face Claude Code plu­g­ins are tested against), com­puter use, desk­top dic­ta­tion and Cowork are avail­able only in Claude Desktop. Linux users there­fore have no of­fi­cially sup­ported graph­i­cal path to these ca­pa­bil­i­ties, and in par­tic­u­lar no way to de­velop and test Claude Code plu­g­ins as desk­top ex­ten­sions with­out switch­ing to ma­cOS or Windows.

Why this is struc­turally hard to jus­tify Anthropic al­ready builds, signs and dis­trib­utes Linux soft­ware. Per code.claude.com/​docs/​en/​setup, Claude Code ships signed apt, dnf and apk repos­i­to­ries and per-ar­chi­tec­ture bi­na­ries (linux-x64, linux-ar­m64, musl vari­ants). The pipeline ex­ists.

The Cowork agent al­ready de­pends on Linux in­side the prod­uct. Independent re­verse-en­gi­neer­ing by Simon Willison on launch day (12/01/2026), cor­rob­o­rated by Pluto Security and pvieito (“Inside Claude Cowork”), found that on ma­cOS Cowork boots a cus­tom Ubuntu 22.04 VM via Apple’s Virtualization Framework (VZVirtualMachine) and runs the Claude Code bi­nary in­side it un­der bub­blewrap and sec­comp. Anthropic’s own doc­u­men­ta­tion con­firms the hy­per­vi­sor split: Apple Virtualization.framework on ma­cOS, Hyper-V on Windows. The com­mu­nity pro­ject johnz­fitch/​claude-cowork-linux demon­strates the same Cowork mode run­ning na­tively on Linux x86_64 by stub­bing the ma­cOS na­tive mod­ules and skip­ping the VM en­tirely. The Linux ca­pa­bil­ity al­ready ex­ists in­side the prod­uct; what’s miss­ing is a pub­lished Linux tar­get.

Why it mat­ters that it is miss­ing Claude Desktop han­dles OAuth to­kens, API keys, and ex­ten­sion con­fig­u­ra­tions. It is a cre­den­tial-han­dling ap­pli­ca­tion run­ning on de­vel­oper work­sta­tions.

Linux users cur­rently ob­tain it via third-party repack­ages of the Windows Electron build. The lead­ing pro­ject, aad­drick/​claude-desk­top-de­bian (roughly 4.5k stars), is gen­uinely high qual­ity: signed apt and dnf repos­i­to­ries, .deb/.rpm/AppImage/AUR/Nix builds, CI-tested, a –doctor di­ag­nos­tic, and up­stream track­ing within days (latest re­lease 05/06/2026, track­ing Claude Desktop 1.11187.1). It is also, by de­f­i­n­i­tion, not ven­dor-signed and not ven­dor-au­dited. A non-triv­ial num­ber of Claude users en­trust their cre­den­tials and lo­cal filesys­tem ac­cess to a third-party repack­age be­cause Anthropic ships noth­ing of­fi­cial. The struc­tural risk is not about the cur­rent main­tain­ers; it is the prece­dent on a plat­form Anthropic’s own agent run­time de­pends on.

Linux is not a fringe de­vel­oper plat­form. Stack Overflow 2025 (49,000+ re­spon­dents, 177 coun­tries): Ubuntu pri­mary OS for 27.7% of pro­fes­sional de­vel­op­ers. StatCounter: India desk­top Linux 16.21% (July 2024); US crossed 5% in June 2025.

Proposed Solution

Publish an of­fi­cial Claude Desktop build for Linux, tar­get­ing the two cur­rent Ubuntu LTS re­leases (and Debian) as a signed .deb via an Anthropic-operated apt repos­i­tory, us­ing the same dis­tri­b­u­tion pipeline Claude Code al­ready uses for Linux.

Alternative Solutions

Claude Code CLI: of­fi­cial and runs na­tively on Linux with signed apt/​dnf/​apk repos­i­to­ries. Excellent for ter­mi­nal work­flows and runs lo­cal MCP servers fine. Not a sub­sti­tute for the desk­top GUI: no sur­face for test­ing Claude Code plu­g­ins as desk­top ex­ten­sions, no com­puter use, no Cowork.

Web client (claude.ai): sup­ports re­mote MCP con­nec­tors but no desk­top ex­ten­sions, no com­puter use, no Cowork. Loses con­ver­sa­tion state on browser crash; higher RAM and bat­tery cost than a na­tive client.

Community repack­ages (aaddrick/claude-desktop-debian, johnz­fitch/​claude-cowork-linux, Snap wrap­pers, k3d3 NixOS flake): func­tional and what I cur­rently use. Unofficial, not ven­dor-signed, not ven­dor-au­dited.

Windows build un­der Wine: clip­board and font in­te­gra­tion break, MCP sub­process han­dling is un­re­li­able, no first-party se­cu­rity up­dates.

Switching to ma­cOS or Windows to test plu­g­ins: cur­rent workaround. Friction on every it­er­a­tion; not a real fix.

Priority

High - Significant im­pact on pro­duc­tiv­ity

Feature Category

Developer tools/​SDK

Use Case Example

I run Ubuntu LTS as my pri­mary de­vel­op­ment en­vi­ron­ment. Per the Stack Overflow 2025 Developer Survey, this is the case for 27.7% of pro­fes­sional de­vel­op­ers.

I run Ubuntu LTS as my pri­mary de­vel­op­ment en­vi­ron­ment. Per the Stack Overflow 2025 Developer Survey, this is the case for 27.7% of pro­fes­sional de­vel­op­ers.

I de­velop Claude Code plu­g­ins. Plugins are tested and it­er­ated on as Claude Desktop ex­ten­sions, which re­quires Claude Desktop. There is no Linux build.

I de­velop Claude Code plu­g­ins. Plugins are tested and it­er­ated on as Claude Desktop ex­ten­sions, which re­quires Claude Desktop. There is no Linux build.

The cur­rent workaround is to switch to ma­cOS every time I need to test a plu­gin as an ex­ten­sion. This is fric­tion on every it­er­a­tion of a plu­gin I am build­ing on Linux, and a suf­fi­ciently bad er­gonomic that it dis­cour­ages plu­gin de­vel­op­ment from Linux en­tirely.

The cur­rent workaround is to switch to ma­cOS every time I need to test a plu­gin as an ex­ten­sion. This is fric­tion on every it­er­a­tion of a plu­gin I am build­ing on Linux, and a suf­fi­ciently bad er­gonomic that it dis­cour­ages plu­gin de­vel­op­ment from Linux en­tirely.

With an of­fi­cial Linux build I would in­stall via apt from an Anthropic-signed repos­i­tory and de­velop, test and it­er­ate on Claude Code plu­g­ins as desk­top ex­ten­sions on the same ma­chine I write them on.

With an of­fi­cial Linux build I would in­stall via apt from an Anthropic-signed repos­i­tory and de­velop, test and it­er­ate on Claude Code plu­g­ins as desk­top ex­ten­sions on the same ma­chine I write them on.

Additional Context

Sources for the load-bear­ing claims, named pri­mary where pos­si­ble.

Platform sup­port ma­trix

claude.com/​down­load: Not avail­able for Linux”.

code.claude.com/​docs/​en/​desk­top: desk­top app avail­able for ma­cOS and Windows.

Claude Code al­ready on Linux

code.claude.com/​docs/​en/​setup: signed apt, dnf and apk repos­i­to­ries; per-plat­form bi­na­ries (linux-x64, linux-ar­m64, linux-x64-musl, linux-ar­m64-musl); Ubuntu 20.04+/Debian 10+.

Cowork Linux-VM ar­chi­tec­ture

Simon Willison, First im­pres­sions of Claude Cowork”, 12/01/2026 (simonwillison.net): VZVirtualMachine via Apple’s Virtualization Framework boot­ing a cus­tom Linux root filesys­tem.

Simon Willison, First im­pres­sions of Claude Cowork”, 12/01/2026 (simonwillison.net): VZVirtualMachine via Apple’s Virtualization Framework boot­ing a cus­tom Linux root filesys­tem.

Pluto Security: cor­rob­o­rat­ing re­verse-en­gi­neer­ing deep dive, Ubuntu 22.04 in­side the VM.

Pluto Security: cor­rob­o­rat­ing re­verse-en­gi­neer­ing deep dive, Ubuntu 22.04 in­side the VM.

pvieito, Inside Claude Cowork”: ma­cOS host → Apple Virtualization Framework → Ubuntu 22.04 VM → bub­blewrap → sec­comp → Claude Code at /usr/local/bin/claude.

pvieito, Inside Claude Cowork”: ma­cOS host → Apple Virtualization Framework → Ubuntu 22.04 VM → bub­blewrap → sec­comp → Claude Code at /usr/local/bin/claude.

Anthropic doc­u­men­ta­tion con­firms the hy­per­vi­sor split (Apple Virtualization.framework on ma­cOS, Hyper-V on Windows) with­out con­firm­ing the re­verse-en­gi­neered in­ter­nals.

Anthropic doc­u­men­ta­tion con­firms the hy­per­vi­sor split (Apple Virtualization.framework on ma­cOS, Hyper-V on Windows) with­out con­firm­ing the re­verse-en­gi­neered in­ter­nals.

johnz­fitch/​claude-cowork-linux: work­ing com­mu­nity port that stubs the ma­cOS na­tive mod­ules and runs Cowork di­rectly on Linux x86_64 with no VM.

johnz­fitch/​claude-cowork-linux: work­ing com­mu­nity port that stubs the ma­cOS na­tive mod­ules and runs Cowork di­rectly on Linux x86_64 with no VM.

Community pack­ag­ing

aad­drick/​claude-desk­top-de­bian: roughly 4.5k stars; .deb, .rpm, AppImage, AUR, Nix; signed apt and dnf repos­i­to­ries at pkg.claude-desk­top-de­bian.dev; lat­est re­lease v2.0.18+claude1.11187.1 dated 05/06/2026; –doctor di­ag­nos­tic; CI-tested; ex­per­i­men­tal Cowork on Linux.

aad­drick/​claude-desk­top-de­bian: roughly 4.5k stars; .deb, .rpm, AppImage, AUR, Nix; signed apt and dnf repos­i­to­ries at pkg.claude-desk­top-de­bian.dev; lat­est re­lease v2.0.18+claude1.11187.1 dated 05/06/2026; –doctor di­ag­nos­tic; CI-tested; ex­per­i­men­tal Cowork on Linux.

Related: aad­drick/​claude-desk­top-arch, emsi/​claude-desk­top, k3d3/​claude-desk­top-linux-flake.

Related: aad­drick/​claude-desk­top-arch, emsi/​claude-desk­top, k3d3/​claude-desk­top-linux-flake.

Demand

StatCounter: India desk­top Linux 16.21% (July 2024); US crossed 5% in June 2025; global ap­prox­i­mately 4.7% in 2025.

If a first-party build is not on the roadmap

A lower-cost fall­back that would ad­dress most of the trust and se­cu­rity con­cerns: a pub­lic state­ment on the in­stall doc­u­men­ta­tion that Linux is not cur­rently planned (with rough hori­zon if any), ac­knowl­edge­ment of a rec­om­mended com­mu­nity pro­ject, a one-off se­cu­rity re­view sum­mary of that pro­ject, and ex­plicit se­cu­rity guid­ance for Linux users on cre­den­tial han­dling and MCP server con­fig­u­ra­tion.

Steelmanned counter-case

The strongest in­ter­nal not now”, so this is­sue in­vites a real con­ver­sa­tion rather than a po­lite close.

Volume does not jus­tify the en­gi­neer­ing tax. Cowork par­ity, Windows hard­en­ing and agent ca­pa­bil­ity work all plau­si­bly out­rank a third desk­top plat­form.

Volume does not jus­tify the en­gi­neer­ing tax. Cowork par­ity, Windows hard­en­ing and agent ca­pa­bil­ity work all plau­si­bly out­rank a third desk­top plat­form.

Linux frag­men­ta­tion cre­ates a dis­pro­por­tion­ate sup­port tax: dis­tros, dis­play servers, sand­box­ing mod­els, graph­ics stacks. The com­mu­nity pro­jec­t’s com­mit log shows the sur­face (AppArmor userns blocks, KDE Plasma SNI races, Wayland HiDPI, eCryptfs path-length fail­ures).

Linux frag­men­ta­tion cre­ates a dis­pro­por­tion­ate sup­port tax: dis­tros, dis­play servers, sand­box­ing mod­els, graph­ics stacks. The com­mu­nity pro­jec­t’s com­mit log shows the sur­face (AppArmor userns blocks, KDE Plasma SNI races, Wayland HiDPI, eCryptfs path-length fail­ures).

Enterprise Linux de­vel­op­ers are largely served by re­mote de­vel­op­ment and the CLI. A desk­top GUI may not un­lock en­ter­prise rev­enue pro­por­tion­ate to its cost.

Enterprise Linux de­vel­op­ers are largely served by re­mote de­vel­op­ment and the CLI. A desk­top GUI may not un­lock en­ter­prise rev­enue pro­por­tion­ate to its cost.

Opportunity cost. Every en­gi­neer-quar­ter on Linux desk­top is a quar­ter not on agent qual­ity, MCP ecosys­tem, Cowork hard­en­ing, or en­ter­prise con­trol planes.

Opportunity cost. Every en­gi­neer-quar­ter on Linux desk­top is a quar­ter not on agent qual­ity, MCP ecosys­tem, Cowork hard­en­ing, or en­ter­prise con­trol planes.

Distribution is non-triv­ial. Signed re­pos, GPG keys, AppImage sign­ing, Snap, AUR, Nix.

Distribution is non-triv­ial. Signed re­pos, GPG keys, AppImage sign­ing, Snap, AUR, Nix.

A rea­son­able se­nior de­ci­sion could weigh these and con­clude not on the cur­rent roadmap”. I would un­der­stand that. What I do not un­der­stand is the ab­sence of any pub­lic po­si­tion at all, and the struc­tural se­cu­rity cost of that si­lence to cur­rent Linux users.

Note on the triage bot

I am aware this is­sue is processed by an au­to­mated triage sys­tem. I have writ­ten it as a sin­gle con­sol­i­dated re­quest with a clear pri­mary ask and a lower-cost fall­back (the good no” path in Additional Context). Please route rather than close if a dif­fer­ent venue is cor­rect; please re­spond rather than close as not planned” with­out a stated ra­tio­nale, be­cause the ab­sence of a stated ra­tio­nale is part of what this is­sue is ask­ing to fix.

Happy to con­tribute and help main­tain.

How's Linear so fast? A technical breakdown

performance.dev

Dennis BrotzkyMay 3, 2026

A few mil­lisec­onds is all it takes to up­date an is­sue in Linear. A tra­di­tional CRUD app do­ing the same thing takes about 300ms. How do they do it? There’s no se­cret sil­ver bul­let to per­for­mance. The re­al­ity is that it’s built from the ground up on the right foun­da­tion, then im­proved by count­less de­ci­sions. My goal is to walk through some of the tech­niques that make Linear feel the way it does and help you im­ple­ment the same.

What I’ll cover

Database in the browser

Database in the browser

Making the first load feel in­stant

Making the first load feel in­stant

The sync en­gine

The sync en­gine

Designed for speed

Designed for speed

Animations

Animations

A quick dis­claimer: I’ve never worked at Linear and have never seen their code. Everything I share comes from my per­sonal ex­pe­ri­ence, study­ing their app, read­ing their blog posts, or watch­ing their con­fer­ence talks. I sim­ply love build­ing web apps and have been us­ing Linear since their beta launch. Also, the ar­ti­cle’s hero im­age comes from a video by Meg Wayne, whose work for Linear is phe­nom­e­nal.

Database in the browser

Most web apps live in­side the same loop. The user clicks. The browser fires an HTTP re­quest. A server queries a data­base and sends it back. The browser re­paints. The end re­sult is a spin­ner, a skele­ton, or a frozen UI for a few hun­dred mil­lisec­onds while the app waits on the net­work.

Linear in­verts the tra­di­tional re­la­tion­ship. The ac­tual data­base the UI reads from is in the browser, in IndexedDB. Mutations ap­ply lo­cally first, then asyn­chro­nously push to the server, which broad­casts deltas back to other clients via WebSocket.

In my opin­ion, this is the most crit­i­cal piece to Linear’s per­for­mance. When your goal is to build a fast web app the biggest bot­tle­neck you will fight is the net­work. Any data sent be­tween the client and server costs hun­dreds of mil­lisec­onds. The best ap­proach is to elim­i­nate the need for a net­work re­quest en­tirely: which is ex­actly what Linear does.

I’ll be re­peat­ing this a lot, but the se­cret to build­ing in­cred­i­ble web apps is by hid­ing all the net­work re­quests from the user. The more load­ing states you can avoid the bet­ter.

Here’s an ex­am­ple of how sim­ple Linear’s re­quests are:

// A tra­di­tional web app up­dat­ing the server async func­tion up­dateIs­sue({ is­sue }) { showSpin­ner(); const re­sponse = await fetch(`/​api/​is­sues/${​is­sue.id}`, { method: PATCH, body: JSON.stringify({ ti­tle: is­sue.ti­tle }), }); const up­dated = await re­sponse.json(); setIs­sue(up­dated) hideSpin­ner(); }

// vs Linear is­sue.ti­tle = Faster app launch”; is­sue.save();

The first line, is­sue.ti­tle = Faster app launch”, up­dates an in-mem­ory data­s­tore (MobX ob­serv­able in Linear’s case) . The sec­ond line, is­sue.save();, queues a trans­ac­tion that their sync en­gine batches and flushes to the server. The key here is that the UI re-ren­ders syn­chro­nously off the lo­cal, in-mem­ory, up­date. There are no spin­ners be­cause there is noth­ing to wait for be­cause the data is synced in the back­round. This is the magic of treat­ing the browser as the data­base for each user.

Tuomas, one of Linear’s co-founders, said this at a con­fer­ence in 2024: Literally the first lines of code that I wrote was the sync en­gine, which is very un­com­mon to what you usu­ally do when you’re a startup.’ From day one, Linear knew the ap­proach they wanted to take and the trade­offs it would take.

I know most peo­ple won’t build a cus­tom sync en­gine like Linear just to make their app feel fast and they don’t need to. For most use cases, li­braries like Tanstack Query and SWR can get sur­pris­ingly close with op­ti­mistic up­dates. Most web apps feel slow be­cause the UI waits for each net­work re­quest to com­plete be­fore up­dat­ing state. For most use­cases the net­work re­quest will suc­ceed so you should take ad­van­tage of that and op­ti­misti­cally up­date your state.

// op­ti­mistic mu­ta­tion with SWR mu­tate( `/api/issues/${issue.id}`, { …issue, ti­tle: Faster app launch” }, false );

// vs Linear is­sue.ti­tle = Faster app launch”; is­sue.save();

The key idea is sim­ple: UI re­spon­sive­ness should not de­pend on net­work la­tency. Users per­ceive speed based on how quickly the in­ter­face re­acts, not how quickly the server re­sponds.

Optmistic re­quests is one of the high­est lever­age im­prove­ments you can make:

elim­i­nate un­nec­es­sary spin­ners

elim­i­nate un­nec­es­sary spin­ners

up­date state im­me­di­ately

up­date state im­me­di­ately

val­i­date in the back­ground

val­i­date in the back­ground

roll­back only if needed

roll­back only if needed

Linear’s foun­da­tion is based on this ex­act prin­ci­pal and it makes the app feel na­tive and fast.

A peek into Linear’s stack

Linear is built on the sim­plest stacks you can find: React, TypeScript, MobX, Postgres, a CDN. There’s no edge data­base, no React Server Components, or no fancy frame­work.

Frontend React + re­act-dom (UI run­time) MobX (observable graph, gran­u­lar re-ren­ders) TypeScript (single lan­guage end-to-end) Rolldown-Vite + plu­gin-re­act-oxc(mid-2025; pre­vi­ously Rollup; pre­vi­ously Parcel) ProseMirror + y-pros­emir­ror (rich text ed­i­tor; Yjs CRDT for live col­lab) Radix UI prim­i­tives (popovers, menus, fo­cus traps) Emotion + StyleX (Emotion run­time + StyleX com­piled to atomic CSS) Comlink (Worker RPC) idb (IndexedDB wrap­per back­ing the lo­cal-first store) graphql-re­quest (GraphQL trans­port to the sync server) Sentry (error mon­i­tor­ing) Inter Variable (single woff2, font-dis­play: swap)

Backend Node.js + TypeScript (single lan­guage for all server code) PostgreSQL on Cloud SQL (issues table par­ti­tioned 300 ways) Memorystore Redis (event bus + cache + sync cur­sors) tur­bop­uffer (similar-issue de­tec­tion, vec­tor db) Kubernetes on GCP (one work­load per con­cern) Cloudflare Workers (multi-region edge proxy)

Other clients Desktop: Electron (same web JS, na­tive chrome) Mobile: Swift (iOS) + Kotlin (a sep­a­rate full reim­ple­men­ta­tion)

Marketing Next.js (static) styled-com­po­nents Inline SVG sprite

The biggest stand­out to me is their de­ci­sion to stick with client-side ren­der­ing. CSR of­ten gets crit­i­cized for slow ini­tial loads, but with the right ar­chi­tec­ture and de­sign it can feel in­stant.

I’m also a big fan of the sim­plic­ity it brings. Keeping the app en­tirely client-side cre­ates a much cleaner men­tal model and re­moves a lot of the com­plex­ity that comes with server-ren­dered apps. You don’t have to con­stantly think if you’re on the server or client. If win­dow ob­ject is ac­ces­si­ble or not. If you’re set­ting the right cache head­ers or not. There’s beauty in sim­plic­ity and the con­straints you’re forced into.

So how does Linear make their client side ren­dered app feel in­stant?

Making the first load feel in­stant

One thing I ob­sess over is the first load, and Linear clearly does as well. For pro­duc­tiv­ity tools es­pe­cially, the time it takes be­fore you can ac­tu­ally start work­ing is one of the most im­por­tant de­tails to con­sider. No one wants to be wait­ing for a new tab to load for mul­ti­ple sec­onds

First, you have to un­der­stand what makes ini­tial loads slow. For a client side app you have to re­quest the in­dex.html, then that re­quests all the JavaScript and CSS, which then runs some sort of au­then­ti­ca­tion, and fi­nally makes some API re­quests to show the app.

Linear’s bundler arc: Parcel, Rollup, Vite, Rolldown

The first step to mak­ing an app feel in­stant hap­pens long be­fore run­time. It starts at build time. Remember, the net­work is the bot­tle­neck, so ship­ping the least amount of JavaScript and CSS is crit­i­cal to fast load times.

From what I can gather Linear has rewrit­ten their build pipeline four times: Parcel → Rollup → Vite → Rolldown. Each mi­gra­tion was dri­ven by the same goal: re­duce the amount of JavaScript and CSS and im­prove the de­vel­oper ex­pe­ri­ence.

From their own blog posts they claim:

50% less code shipped.

50% less code shipped.

30% smaller af­ter com­pres­sion.

30% smaller af­ter com­pres­sion.

Cold-cache page loads got 10 to 30% faster.

Cold-cache page loads got 10 to 30% faster.

Time-to-first-paint of the ac­tive-is­sues view dropped 59% (on Safari).

Time-to-first-paint of the ac­tive-is­sues view dropped 59% (on Safari).

Memory us­age dropped 70 to 80%

Memory us­age dropped 70 to 80%

Most of that came from a com­bi­na­tion of de­ci­sions tar­get­ing only mod­ern browsers, bet­ter dead-code elim­i­na­tion, and ag­gres­sive code split­ting. Dropping legacy sup­port is the big win (no poly­fills, no ES5 tran­spi­la­tion, no nomod­ule fall­back) but the dead-code and chunk­ing work mat­ters just as much.

Even with all of these op­ti­miza­tions, Linear still ships a sub­stan­tial amount of code: roughly 21 MB of mini­fied JavaScript. The dif­fer­ence is that it’s ag­gres­sively code split into hun­dreds of route-level chunks that are fetched on de­mand.

// vite.con­fig.ts (reconstruction; matches ob­served chunk graph) ex­port de­fault de­fineCon­fig({ plu­g­ins: [react()], build: { tar­get: esnext”, // no legacy syn­tax, no poly­fills css­Minify: lightningcss”, mod­ulePre­load: { poly­fill: false }, rollupOp­tions: { out­put: { // One chunk per npm pack­age > ~3 KB. Cache in­val­i­da­tion // be­comes per-li­brary in­stead of per-app-re­vi­sion. man­u­alChunks(id) { if (id.includes(“node_modules”)) { const pkg = id.match(/​node_­mod­ules\/([^/]+)/)?.[​1]; if (pkg) re­turn `vendor-${pkg}`; } }, }, }, }, });

The les­son is­n’t which bundler to pick but the im­por­tance of drop­ping legacy browsers, go­ing na­tive ESM, and code split­ting like crazy. Each step is small. Stacked, they cut Linear’s first-load JavaScript roughly in half and their build time by an or­der of mag­ni­tude.

So, the first se­cret to in­stant load times is re­duc­ing the amount of JavaScript and CSS needed to ren­der some­thing for the user.

Preloading af­ter ini­tial load

Once you’ve split your JavaScript into the small­est chunks pos­si­ble you can start do­ing work in the back­ground.

But hold on, split­ting the bun­dle into hun­dreds of chunks cre­ates a new prob­lem. Each chunk im­ports other chunks, and the browser does­n’t know what those are un­til it parses the en­try script. Without help, the load time­line be­comes a wa­ter­fall: fetch the en­try, parse it, fetch its im­ports, parse those, fetch their im­ports. Every level adds a net­work round-trip, which you want to avoid at all costs.

What Linear does is be­fore any JavaScript runs, the browser sees the en­tire list and fires off the re­quests in par­al­lel. By the time the en­try script reaches its first im­port, the chunks are al­ready in cache.

Here’s what it looks like in the <head /> if their in­dex.html

<script type=mod­ule crosso­ri­gin src=“https://​sta­tic.lin­ear.app/​client/​as­sets/​html.2_JBQs3Q.js></script> <link rel=mod­ulepre­load crosso­ri­gin href=“https://​sta­tic.lin­ear.app/​client/​as­sets/​ven­dor-mobx.Crhy2qQc.js> <link rel=mod­ulepre­load crosso­ri­gin href=“https://​sta­tic.lin­ear.app/​client/​as­sets/​SyncWeb­Socket.Djw6l_Op.js> <link rel=mod­ulepre­load crosso­ri­gin href=“https://​sta­tic.lin­ear.app/​client/​as­sets/​Data­base­M­an­ager.DKss­GAN8.js> <!– …around many more –>

The crosso­ri­gin at­tribute on each pre­load matches the crosso­ri­gin on the en­try script, so the browser reuses the cached fetch in­stead of treat­ing pre­load and im­port as sep­a­rate re­sources. Same trick as the font pre­load, ap­plied to every chunk on the crit­i­cal path.

The cold-load time­line col­lapses from a se­quen­tial wa­ter­fall into a sin­gle par­al­lel batch. The net­work still does the work. It just does it all at once. The beauty of this tech­nique is you’re able to do all this work in the back­ground when the user first hits the lo­gin page. In a few sec­onds the full app is stored in cache and served in­stantly.

It’s ex­tremely im­por­tant to un­der­stand how peo­ple will use your app. Once you have this un­der­stand­ing you can start us­ing it to your ad­van­tage, such as pre­load­ing scripts in the back­ground as Linear does.

The ser­vice worker for even more speed and of­fline ca­pa­bil­i­ties

The rest of the Linear, the route-level chunks for views the user has­n’t vis­ited yet, gets cached in the back­ground by a ser­vice worker. The worker has a pre­cache man­i­fest baked into its source, around 1,200 hashed as­sets cov­er­ing route chunks, icons, and fonts, and pulls them down lazily af­ter the first page load. Within a few sec­onds of hit­ting the lo­gin screen, the full app is sit­ting in cache.

This buys two things. Subsequent nav­i­ga­tions skip the net­work en­tirely; the ser­vice worker an­swers di­rectly from its cache with­out even go­ing through HTTP cache. And the app keeps work­ing when the net­work does­n’t. Combined with the lo­cal-first sync en­gine (which al­ready has the user’s data in IndexedDB), Linear is us­able of­fline. You can read is­sues, cre­ate new ones, edit ti­tles and de­scrip­tions, change sta­tuses. Everything queues in the lo­cal trans­ac­tion store and flushes the next time the con­nec­tion comes back.

Modulepreload is for what the app needs now, par­al­lel-fetched so the browser never blocks on a se­r­ial im­port chain. The ser­vice worker is for what the app needs next.

So, to get load times fast the steps for Linear is to elmi­nate as much code as pos­si­ble, split it into small pieces, and pre­cache it in the back­ground. Again, the goal of all this work is to make net­work re­quests as fast as pos­si­ble or, even bet­ter, elim­i­nate them com­pletely.

Vendor bun­dle com­po­si­tion

I found it in­ter­est­ing that every pack­age Linear uses gets its own chunk, cached in­de­pen­dently. A tra­di­tional ven­dor.js in­val­i­dates the en­tire de­pen­dency graph on any bump. Linear’s chunk­ing turns ven­dor caching from a sin­gle mas­sive file to fine-grained. Bumping a sin­gle de­pen­dency in­val­i­dates one chunk; the rest stay cached.

Seems like a no-brainer and yet an­other de­tail to en­sure fast load times.

Loading mas­sive font files

Font load­ing is one of those de­tails a lot of apps get wrong. The fail­ure modes are vis­i­ble: in­vis­i­ble text for half a sec­ond, lay­out shifts as the real font swaps in, dou­ble-fetched re­sources be­cause the pre­load did­n’t match. Linear’s setup avoids all three:

<!– in <head> of in­dex.html –> <link rel=“pre­load” href=“https://​sta­tic.lin­ear.app/​fonts/​In­ter­Vari­able.woff2?v=4.1 as=“font” type=“font/​woff2” crosso­ri­gin=“anony­mous”> <link rel=“pre­con­nect” href=“https://​sta­tic.lin­ear.app crosso­ri­gin>

@font-face { font-fam­ily: Inter Variable”; font-weight: 100 900; font-dis­play: swap; src: url(https://​sta­tic.lin­ear.app/​fonts/​In­ter­Vari­able.woff2?v=4.1) for­mat(“woff2”); } /* Italic and Berkeley Mono fol­low the same shape, sin­gle woff2 each. */

Variable fonts cover the full 100 – 900 weight axis in a sin­gle woff2, elim­i­nat­ing per-weight re­quests. font-dis­play: swap ren­ders the fall­back stack im­me­di­ately and swaps to Inter when it loads. The trick that’s easy to miss: crosso­ri­gin=“anony­mous” on the pre­load tag. Without it, the browser pre­loads the font, then fetches it again when CSS later ref­er­ences it, be­cause the two re­quests have dif­fer­ent CORS modes. crosso­ri­gin on the pre­load makes the browser reuse the cached one.

This all seems sim­ple, but I’m al­ways sur­prsied at how many apps load fonts in­cor­rectly. Linear is a great ex­am­ple of think­ing through the de­tails and en­sur­ing font load­ing is as fast and ac­cu­rate as pos­si­ble.

Inlined app shell

Another key tech­nique to make the first load feel fast: Inlined in <head/> is just enough CSS to paint the load­ing state with no ex­ter­nal stylesheet fetched. Remember, the net­work is the bot­tle­neck and what you’ll al­ways be fight­ing to make your app feel fast. In this case, Linear elmi­nates a net­work re­quest by in­lin­ing the crit­i­cal CSS re­quired to show the user an app shell.

<style> :root { –bg-color: #f5f5f5; –bg-base-color: #fcfcfd; –bg-border-color: #e0e0e0; –sidebar-width: 244px; } html { back­ground: var(–bg-color); height: 100%; } body { font-fam­ily: Inter Variable”, Arial, Helvetica, sans-serif; }

#appBorders { bor­der: 1px solid var(–bg-bor­der-color); back­ground: var(–bg-base-color); mar­gin: 8px 8px 8px var(–side­bar-width); bor­der-ra­dius: 12px; }

#logo { trans­form: trans­lateZ(0); }

@keyframes lo­goB­ack­ground­Pulse { 0% { opac­ity: 0; trans­form: scale(0.8); } 70% { opac­ity: 1; } 100% { opac­ity: 0; trans­form: scale(1.0); } } </style> <script>performance.mark(“appStart”);</script>

Beyond CSS there is also a bunch of in­lined JavaScript that’s crit­i­cal to load­ing the ini­tial ex­pe­ri­ence.

<script> // Electron con­text — lets CSS branch on na­tive chrome. if (navigator.userAgent.includes(“Electron”) && nav­i­ga­tor.user­A­gent.in­cludes(“Lin­ear”)) doc­u­ment.doc­u­mentEle­ment.classList.add(“elec­tron”);

Dopamine Fracking

igerman.cc

2026.04.13

8 min read

The act of pump­ing im­mense, dis­pro­por­tion­ate re­sources — money, crowd­sourced math, an­a­lyt­ics, op­ti­miza­tion, min-max­ing, pop­u­lar opin­ion ag­gre­ga­tion, etc. — into a pre­vi­ously ca­sual or com­plex, lay­ered ac­tiv­ity to force­fully ex­tract and squeeze out the purest, most con­cen­trated dopamine hit, with no re­gard for any­thing ex­cept dopamine.

Origin

One late evening while chat­ting on Discord, I coined the term dopamine frack­ing” to de­scribe a phe­nom­e­non that has be­come in­creas­ingly preva­lent in on­line cul­ture — a con­cept which I pre­vi­ously strug­gled to ex­press. It’s a metaphor, be­cause just like in ac­tual frack­ing, it is im­mensely harm­ful to the long-term health and sus­tain­abil­ity of any­thing it is ap­plied to, but in the short term, it can yield a very in­tense and con­cen­trated hit of dopamine (or oil).

I briefly called this sloptimization” — a term which was prob­a­bly coined by AI bros to de­scribe the process of op­ti­miz­ing AI mod­els to pass bench­marks, but it does­n’t quite cap­ture the de­struc­tive na­ture of the prac­tice. I guess you could say that a close al­ter­na­tive would be commodification,” over-consumption,” or industrialization” of the hu­man ex­pe­ri­ence, but… all of these words sound more like ster­ile eco­nomic terms and don’t ac­tu­ally sig­nify how ut­terly dev­as­tat­ing this has been to cul­ture, cre­ativ­ity, and con­nec­tion. I feel like dopamine frack­ing” cre­ates a much more gut­tural, vis­ceral, dis­gust­ing im­age of an oil rig in your brain, or worse, in things you love and cher­ish.

Commodifying the Human Experience

I was in­spired to come up with this af­ter watch­ing a few of Metta Beshay’s won­der­ful videos about drugs in the con­text of their orig­i­nal cul­tural sig­nif­i­cance. He cov­ers a lot of dif­fer­ent sub­stances and their his­to­ries, and I highly rec­om­mend go­ing to his chan­nel in­stead of lis­ten­ing to me (an id­iot) talk about it. In short, there’s a rea­son why cer­tain drugs were used in cer­tain cul­tures for thou­sands of years, but be­came much more ne­far­i­ous and de­struc­tive when they were taken out of that con­text. That rea­son is the in­dus­tri­al­iza­tion and cul­tural era­sure by the Enterprising Capitalist™️.

The same thing has been hap­pen­ing to so much of our cul­ture, hob­bies, and even re­la­tion­ships. For all in­tents and pur­poses, an enor­mous num­ber of peo­ple live on­line. The con­stant search for the next big thing, the next big hit of dopamine, has led to a cul­ture of over­con­sump­tion and ad­dic­tion. Whether it’s com­mu­ni­ties be­com­ing too pop­u­lar, mu­sic be­com­ing too cliché, videos be­com­ing too MrBeast-y,” movies be­com­ing too Marvel, web­sites be­com­ing too flat — all that mat­ters is the dopamine hit. And the long-term con­se­quences are ig­nored. Not out of mal­ice, but be­cause it feels as ad­dic­tive as a com­mod­i­fied drug, and peo­ple are sim­ply try­ing to get their next hit.

I’m not say­ing that the things I listed lack merit or ef­fort: an im­mense amount of work un­doubt­edly goes into any movie, song, or video if it’s made by a per­son or team and not by AI. But at a cer­tain thresh­old, if every­thing con­verges on a sin­gle point, there’s quite lit­er­ally no room for any­thing else in zero di­men­sions.

The Strawberry Example

Perhaps my takes are a lit­tle too on­line, so let’s look at a more re­lat­able ex­am­ple: straw­ber­ries. Strawberries are de­li­cious, and they have a very com­plex fla­vor pro­file. They have hun­dreds, if not thou­sands, of strains, and for every sin­gle in­di­vid­ual straw­berry, there are thou­sands of unique com­pounds that con­tribute to its fla­vor. There are white ones, red ones, some are white on the in­side, some are red, some are sour, some are sweet, some are a lit­tle bit­ter, some are very aro­matic, some are very juicy, some are very firm, some are very soft. Even if the dif­fer­ences within a sin­gle bushel of straw­ber­ries are nigh im­per­cep­ti­ble, the ex­pe­ri­ence of eat­ing one is com­plex and lay­ered. And each and every one of the straw­ber­ries you put in a cake, blend into a smoothie, or eat on its own is, in a way, a beau­ti­fully im­per­fect, unique, ana­log ex­pe­ri­ence. You might not no­tice it, you might not care, but it’s there, and it mat­ters — even if just that tiny bit.

But if you were to de­com­pose a straw­berry, ex­tract the aro­matic com­pound that smells most like a straw­berry, an­a­lyze its for­mula, de­vise a way to syn­the­size it, and make it com­mer­cially vi­able, you could put that in every food as a sub­sti­tute for the metic­u­lous work of col­lect­ing good straw­ber­ries and the com­plex palate one has. It would be much cheaper to man­u­fac­ture, and it would give you a very con­cen­trated hit of straw­berry fla­vor. Most peo­ple would­n’t be able to tell much of a dif­fer­ence, and it would prob­a­bly still be de­li­cious. If you’re not greedy.

In fact, this is ex­actly what hap­pens in the food in­dus­try. They ex­tract the com­pound that gives straw­ber­ries their fla­vor and put it in every­thing from cheap candy to ex­pen­sive desserts.

But it would also com­pletely erase every­thing else about the ex­pe­ri­ence of eat­ing a straw­berry. The tex­ture, the juici­ness, the com­plex­ity of the fla­vor, the im­per­fec­tions, the joy of find­ing a par­tic­u­larly good one, the cos­mic hor­ror of eat­ing a wormy one, the nos­tal­gia of hav­ing your grand­ma’s straw­berry jam with dozens of in­di­vid­u­ally unique straw­ber­ries in it. All of that is lost and con­densed into a sin­gle, pure hit of straw­berry fla­vor. Tasty? Maybe. But it’s not a straw­berry any­more. It’s just a chem­i­cal that kind of tastes like a straw­berry. Soon enough, you for­get what one ac­tu­ally tastes like. Or worse, you pre­fer the chem­i­cals. Or even worse, you can’t even find real straw­ber­ries any­more be­cause the mar­ket is flooded with syn­thetic re­place­ments. Or even worser, the real ones have long gone ex­tinct be­cause no one wanted to grow them any­more when the syn­thetic ver­sion was cheaper and more con­ve­nient. And whoop-dee-doo, you’ve erased about 500 in­di­vid­ual hu­man ex­pe­ri­ences and re­placed them with a sin­gle, shared one. And that’s just straw­ber­ries.

This is what dopamine frack­ing does to cul­ture, hob­bies, and even re­la­tion­ships, which are so much more com­plex be­cause they are so deeply ab­stract. It ex­tracts the most con­cen­trated hit of dopamine and puts it in every­thing, while eras­ing all the com­plex­ity, nu­ance, and beauty that made it spe­cial in the first place. And the more we do it, the more we for­get what the orig­i­nal ex­pe­ri­ence was like, and the more we pre­fer the syn­thetic ver­sion, and the worse off we are. It’s a vi­cious cy­cle that leads to a ho­mog­e­nized, com­mod­i­fied cul­ture that is de­void of mean­ing and con­nec­tion.

Remember that SpongeBob episode where they made Krabby Patties out of goo? Yeah. That.

Conclusion

The worst part about it? This was so in­cred­i­bly easy and con­ve­nient to ig­nore for such a long time. Optimization was seen as a good thing, and the idea of solving” some­thing was seen as a pos­i­tive. I def­i­nitely par­tic­i­pated in it, and I’m sure you or some­one you know has, too. After all, who does­n’t want to solve things? Who does­n’t want to op­ti­mize? But the more this hap­pens, the more we see just how de­struc­tive, dev­as­tat­ing, and un­sus­tain­able liv­ing like this is.

I’ve been grad­u­ally turn­ing off dopamine frack­ing in my life: delet­ing chan­nels and feeds that in­fu­ri­ate me or milk my trig­gers (positive or neg­a­tive), unin­stalling apps, and set­ting bound­aries on what I will and won’t en­gage with and con­sume. Becoming aware of this con­cept has made it eas­ier to nav­i­gate the world. And it’s be­com­ing eas­ier and eas­ier for me to sim­ply stop a video and close a tab when I sense that it’s just try­ing to give me a hit of dopamine. It’s so im­mensely lib­er­at­ing to be able to do that.

I don’t have any so­lu­tions. But aware­ness is the first step, and while it feels triv­ial com­pared to ac­tu­ally do­ing some­thing about it, it’s still a step in the right di­rec­tion. I hope that peo­ple can start talk­ing about this, even if not us­ing the term dopamine frack­ing” — I can rec­og­nize that it’s a lit­tle ec­cen­tric, but hey, we call short-form sludge brain rot,” so why not?

Written by a hu­man.

GitHub - devenjarvis/lathe: Generate hands-on, multi-part technical tutorials on demand, with LLM skills tuned to make content approachable. Then you work through them yourself, by hand ✋

github.com

An ex­per­i­ment in us­ing LLMs to teach you, rather than think for you.

Lathe gen­er­ates hands-on, multi-part tech­ni­cal tu­to­ri­als on de­mand, with skills tuned to make con­tent ap­proach­able. Then you work through them your­self, by hand, in a lo­cal UI built from the ground up for pleas­ant learn­ing. (Just like we did it in the stone age 😎)

What is it?

Generate hands-on tech­ni­cal tu­to­ri­als (single-part or a multi-part se­ries) from any prompt

Work through the tu­to­r­ial your­self in a pur­pose-built lo­cal UI

Use skills to ask ques­tions, ver­ify the tu­to­r­ial, and ex­tend it with a new part

Search, fil­ter, and man­age tu­to­ri­als from your li­brary

Every tu­to­r­ial doc­u­ments its sources, which model was used, and what prompt drove the voice” for the tu­to­r­ial

Quick start

Lathe is a com­bi­na­tion of LLM skills and a Golang CLI used to store, man­age, and view gen­er­ated tu­to­ri­als. After in­stall (below), you can gen­er­ate a tu­to­r­ial in­side any LLM ses­sion (Claude Code, Cursor, and Codex sup­ported) by prompt­ing some­thing like:

/lathe build a 3D Slicer in Erlang

Then open lathe from any ter­mi­nal:

lathe serve # starts the web server, opens the browser

Don’t worry, we also have dark mode:

Click the tu­to­r­ial you want to read and start learn­ing!

The CLI has a bunch of other com­mands, but hon­estly those were built to give the LLM a de­ter­min­is­tic way to man­age tu­to­ri­als. I ex­pect the above to be all you need (it’s all I ever use) for day-to-day. If you want to ask a ques­tion about a tu­to­r­ial, have the LLM ver­ify it, or ex­tend it with an ad­di­tional part, the UI has af­for­dances for each of these which will give you the ex­act skill/​prompt to give your LLM in or­der to trig­ger the ac­tion.

Install

Lathe is a sin­gle self-con­tained bi­nary. All you need is lathe on your $PATH; the skills run in an in­ter­ac­tive Claude Code, Cursor, or Codex ses­sion.

Homebrew (macOS, rec­om­mended):

brew in­stall de­ven­jarvis/​tap/​lathe

Distributed as a cask (a pre-built bi­nary), so it’s ma­cOS-only — on Linux use the in­stall script or go in­stall be­low.

Install script (curl | sh):

curl -sSf https://​raw.githubuser­con­tent.com/​de­ven­jarvis/​lathe/​main/​in­stall.sh | sh

Go (needs Go 1.25+):

go in­stall github.com/​de­ven­jarvis/​lathe@lat­est

From source:

git clone https://​github.com/​de­ven­jarvis/​lathe cd lathe go build -o lathe

Install the skills

The skills are bun­dled into the bi­nary. After in­stalling lathe, drop them into a pro­ject so Claude Code (or Cursor / Codex) can dis­cover them:

lathe skills in­stall # ./.claude/skills/<name>/SKILL.md (this pro­ject) lathe skills in­stall –user # ~/.claude/skills/<name>/SKILL.md (all pro­jects) lathe skills in­stall –agent cur­sor # ./.cursor/commands/<slug>.md (Cursor slash com­mands) lathe skills in­stall –agent codex # ./.agents/skills/<name>/SKILL.md (Codex Agent Skills) lathe skills in­stall –agent all # Claude Code, Cursor, and Codex lathe skills list # show the bun­dled skills

Codex uses the same SKILL.md for­mat as Claude Code, so its skills ship ver­ba­tim (and –user in­stalls to ~/.agents/skills/…). Cursor com­mands are slash-in­voked as /<slug> (e.g. /lathe); the in­ter­ac­tive hand­off model is doc­u­mented for Claude Code, so a few run­time de­tails dif­fer on Cursor and Codex.

Why does this ex­ist?

I learned how to pro­gram as a teen in the 2000s by build­ing home­brew games for my PSP (PlayStation Portable) in Lua, and then in C++. Lots of what I learned at the time was through the small PSP home­brew com­mu­nity I’m in­cred­i­bly grate­ful I got to be a part of, but I also owe much of that for­ma­tive learn­ing to free on­line re­sources and tu­to­ri­als avail­able on the in­ter­net (shoutout to 2007 cplus­plus.com - man does that site have a lot more ads now than it used to 😅). Eventually I be­came a pro­fes­sional soft­ware en­gi­neer and I spent the next decade upskilling” (though usu­ally to learn more in­ter­est­ing top­ics than needed for ) by find­ing and con­sum­ing a wealth of tech­ni­cal blogs, and more im­por­tantly for my learn­ing style - hands on tu­to­ri­als. Resources like the build-your-own-x repo, and Crafting Interpreters, and the 1,000 other one-off tu­to­ri­als that taught me every­thing from build­ing a ray­tracer, to a time­series data­base, to a lin­ear al­ge­bra ma­trix li­brary and every­thing in be­tween (seriously, I could­n’t even be­gin to list all the amaz­ing hands-on tu­to­ri­als out there that have in­flu­enced me).

Hands on learn­ing is how I’ve al­ways learned best. These tu­to­ri­als gave me the learn­ing curve I needed to go from zero-to-one in a brand new do­main, but even more im­por­tantly they gave me foot­ing and con­fi­dence to take it from one-to-two-to-ten on my own.

Fast for­ward to 2026, and now we’ve got LLMs. I’m not go­ing to go off topic about my com­pli­cated re­la­tion­ship with LLMs, but for writ­ing soft­ware they are in­ter­est­ing and in many cases they can be re­ally pro­duc­tive! But they do most of the work for you, and with that work gone they also take away the part that helped me learn a new con­cept or do­main. In some cases, that does­n’t mat­ter - we’ve got a prod­uct to ship and LLMs help us ship it faster - but for me and my joy in this field and hobby I still crave those ah ha!” mo­ments where some­thing fi­nally clicks and I have the con­fi­dence I need to be­gin shap­ing it into my own.

So lathe is an ex­per­i­ment in us­ing LLMs to teach me, rather than think for me. To recre­ate those mo­ments of hands-on learn­ing that taught me to love this work, and marry it with the po­ten­tial of a broad expert” LLM who can, in the­ory, teach me any­thing. I use lathe as a cat­a­lyst to get me started on pro­jects I would­n’t know how to start in, and can’t find any ex­ist­ing hu­man writ­ten re­sources to teach. For ex­am­ple I first came up with lathe be­cause I wanted to write a 3D Slicer Software from scratch (just find­ing doc­u­men­ta­tion on g-code was a pain, shoutout to reprap). At the time of writ­ing I’m div­ing into the world of em­bed­ded soft­ware de­vel­op­ment with Zig. Both of these cases lathe has been an ef­fec­tive tool in get­ting me from zero-to-one in ob­scure or ex­tremely young do­mains where the hu­man writ­ten re­sources just don’t ex­ist yet (and I won­der for how long hu­mans will still bother writ­ing tu­to­ri­als if only the LLMs read them…).

But what about hal­lu­ci­na­tions?

Are lathe tu­to­ri­als as good as ones writ­ten by hu­mans? Not in the slight­est. But what they lack in heart, per­son­al­ity, and ar­chi­tec­tural sound­ness, they make up for by hav­ing the tu­to­r­ial writer ready and wait­ing to an­swer all of your ques­tions, al­ways will­ing to fix or up­date their tu­to­r­ial when it is­n’t ex­actly what you wanted, and they ac­tu­ally com­plete writ­ing all 6 parts to that se­ries they started in 2018 (we’ve ALL been there 😁). Lathe is an LLM, and while I’ve built and tuned it to be as good as I know how to make it for this par­tic­u­lar task, it’s still go­ing to fail in the ways LLMs fail. I rec­om­mend us­ing the biggest thinking” model you have ac­cess to (Opus, GPT-5 Codex, etc) as these tasks are less about it­er­a­tive me­chan­i­cal ex­e­cu­tion you might op­ti­mize for when pro­gram­ming, and more about re­search­ing, de­sign­ing, and ex­plain­ing a tan­gi­ble con­cept from start to fin­ish.

Additionally, the risk for hal­lu­ci­na­tions in this con­text is, in my opin­ion, sig­nif­i­cantly lower. Lathe is built to help you do the think­ing, and is built around the ex­pec­ta­tion that you’re the one typ­ing this code out your­self. By read­ing through the guide and typ­ing it out, you are ac­tively en­gaged in the work and should be well po­si­tioned to nat­u­rally ask wait, does that make sense?” when you come across some­thing weird. At which point you can /lathe-ask (and some­times the LLM comes back with good rea­son­ing I did­n’t have be­cause it’s a for­eign do­main, and I learn some­thing) or just straight tell your LLM to up­date the tu­to­r­ial. While I have no ped­a­gog­i­cal cre­den­tials to back this up, I think I may be ac­tu­ally in­ter­nal­iz­ing con­cepts bet­ter by catch­ing and push­ing back on per­ceived slip-ups of the LLM. YMMV.

All of that said, if you can find a tu­to­r­ial writ­ten by a hu­man, I’d al­ways reach for that first. I hope more of­ten than not you do. But if you learn the same way I do and want to dive into a do­main that is light on teach­ing ma­te­ri­als, lathe is a pretty cool tool. Just re­mem­ber it is an LLM and not a hu­man. To help with this, I try to make it clear at all times what you are and are not get­ting. The lathe skills to write tu­to­ri­als will tell you when it is­n’t sure about some­thing it has writ­ten, and while I of­fer a more personal” voice, I’ve de­faulted to one that does­n’t pre­tend to be some­thing it is­n’t.

Be hon­est, did you vibecode this? Isn’t that con­tra­dic­tory to your the­sis?

Yep, lathe is vibecoded”. In this case, the scope and risk of lathe is low. It’s a liv­ing the­sis, for per­sonal learn­ing. That said, I’ve been us­ing it daily lately and it’s proven to be a use­ful and sta­ble tool in my tool­box. I’m learn­ing a lot by us­ing it, and at this point I think it’s good enough that oth­ers might ben­e­fit from it too. I ex­pect the next few point re­leases to be some in­ten­tional code/​ar­chi­tec­ture clean up to en­sure it re­mains sta­ble for oth­ers, and of course in­cor­po­rate any feed­back I get.

That said, for the sake of trans­parency, to­day I test lathe for my own use­cases - Using Claude Code on MacOS. If you are out­side of that setup, lathe should work, but I’ve not ver­i­fied it. If you’re will­ing to try it on a dif­fer­ent setup and it does work, or you end up hit­ting a bump in the road, I’d love an is­sue let­ting me know ei­ther way!

Alright then, how does it work?

LLM skills — gen­er­ate and work with tu­to­ri­als, all run in your in­ter­ac­tive LLM ses­sion: /lathe writes part-01.md, /lathe-extend adds the next part, /lathe-verify works through a tu­to­r­ial to con­firm it com­piles and runs, /lathe-ask an­swers ques­tions about a part you’re read­ing, and /lathe-tag adds search tags to ex­ist­ing tu­to­ri­als.

I moved to run­ning all of these in­ter­ac­tively, be­cause I am a Claude Code user and head­less claude -p is planned to be me­tered as of 2026 – 06-15. Maybe af­ter that change I’ll find that the cost is min­i­mal (generating tu­to­ri­als does not con­sume a lot of to­kens com­pared to vibecod­ing) and we can move some of these in­ter­ac­tions back into the UI. We’ll see!

I moved to run­ning all of these in­ter­ac­tively, be­cause I am a Claude Code user and head­less claude -p is planned to be me­tered as of 2026 – 06-15. Maybe af­ter that change I’ll find that the cost is min­i­mal (generating tu­to­ri­als does not con­sume a lot of to­kens com­pared to vibecod­ing) and we can move some of these in­ter­ac­tions back into the UI. We’ll see!

lathe CLI (Go) — copies tu­to­ri­als into ~/.lathe/tutorials/, serves the ren­dered out­put at http://​lo­cal­host:4242, and owns all durable state. It never calls an LLM it­self: the web but­tons and the lathe ver­ify/​lathe ex­tend com­mands just hand you the skill com­mand to paste into your ses­sion, and the skills call back into the CLI (lathe store, lathe ver­ify-re­sult, lathe ex­tend-start/​ex­tend-com­mit, lathe voice add) to record re­sults.

What’s up with the fancy UI?

I’m glad you asked! The lathe skills and CLI were built in tan­dem to of­fer (what I think is) a great read­ing and learn­ing ex­pe­ri­ence. A few key fea­tures that make us­ing lathe worth more than just prompt­ing Claude di­rectly (for me) are:

Full table of con­tents nav­i­ga­tion if you hover on the right side bar

Content is writ­ten with side-notes through­out to prompt me to think more deeply

Left-to-the-reader Exercises at the end of each tu­to­r­ial

Writing voices

Every tu­to­r­ial is writ­ten in a voice. A voice con­trols how the prose sounds but it does­n’t change ac­cu­racy, re­search, ci­ta­tion, ver­i­fi­ca­tion, or struc­ture, which are fixed. Two voices ship with lathe:

plain­spo­ken (the de­fault) — hon­est and pre­cise, with no in­vented per­sona or fab­ri­cated first-per­son war sto­ries. It’s writ­ten to avoid an­thro­po­mor­phiz­ing the LLM that pro­duced it.

com­pan­ion — an at­tempt at a warm, wry, first-per­son friend at the key­board”.

Pick one per run by nam­ing it in your /lathe in­vo­ca­tion (“…in the com­pan­ion voice”), or change the global de­fault:

lathe voice list # see what’s avail­able; * marks the de­fault lathe voice show com­pan­ion # print a voice’s full spec lathe voice set-de­fault com­pan­ion # change the de­fault for new tu­to­ri­als

Custom voices. If you don’t like the voices that come with lathe that’s cool, you do you. You can au­thor your own with /lathe-voice in an LLM ses­sion, and it’ll in­ter­view you about reg­is­ter, per­son, and hu­mor, draft a spec, and (on your ap­proval) save it via lathe voice add <name> –file - into ~/.lathe/voices/.

Custom voices are in­structed to not im­per­son­ate a real named per­son, fab­ri­cate cre­den­tials, or deny LLM au­thor­ship. /lathe-voice re­fuses those, and every voice is wrapped with a fixed pre­am­ble en­forc­ing the same at gen­er­a­tion time. The voice a tu­to­r­ial was writ­ten in is recorded on it (so /lathe-extend con­tin­ues in it) and is dis­closed in an au­thor­ship by­line at the top of every tu­to­r­ial: Generated by <Model> · voice <name> where the model is the spe­cific LLM used to gen­er­ate the tu­to­r­ial (e.g. Claude Opus 4.8”), and the voice name ex­pands to re­veal the full spec.

I fully rec­og­nize this is a cat and mouse game, and that any at­tempts at safety here can be cir­cum­vented. Unfortunately, whether I pub­lish lathe or not the bad ac­tors who want to flood the world with AI slop tu­to­ri­als are al­ready go­ing full steam ahead. I want to do my part though to make it clear that lathe is NOT in­tended for writ­ing con­tent out­side of your per­sonal use for your per­sonal learn­ing.

Finding tu­to­ri­als

As your li­brary grows, the web list page (lathe serve) has a search box and fil­ters to nar­row it down — all client-side, so it stays fast and of­fline:

Search matches a tu­to­ri­al’s ti­tle, topic, tags, repo, and tool ver­sions.

Sort by newest, old­est, or ti­tle (A–Z).

Filter by sta­tus, by type (single vs. se­ries), by tag, and by ver­sion.

Default port is 4242; over­ride with –port.

Storage lay­out

Tutorials live glob­ally in ~/.lathe/tutorials/, one di­rec­tory per slug:

~/.lathe/tutorials/ dig­i­tal-synth-zig/ meta­data.json part-01.md part-02.md part-03.md data­base-from-scratch-go/ meta­data.json in­dex.md

meta­data.json:

{ slug”: digital-synth-zig”, title”: Build a Digital Synth in Zig”, topic”: build a dig­i­tal synth in Zig”, created”: 2026 – 05-03T19:00:00Z”, status”: unverified”, tags”: [“zig”, audio”, dsp”], parts”: [“part-01.md”, part-02.md”, part-03.md”], tools”: [{ name”: zig”, version”: 0.13.0” }], sources”: [“https://​ziglang.org/​doc­u­men­ta­tion/​0.13.0/], voice”: plainspoken”, model”: Claude Opus 4.8″ }

Everything be­yond the core fields (slug/title/topic/created/status) is op­tional and omit­ted when empty: tools (the lan­guages/​tool­chains the tu­to­r­ial tar­gets, sur­faced as ver­sion chips and the Versions fil­ter), sources (the re­search trail — see be­low), voice and model (the by­line on the read­ing page), and repo/​re­po_branch when a tu­to­r­ial was writ­ten against a spe­cific git repos­i­tory.

Status is one of un­ver­i­fied (the de­fault af­ter lathe store; ren­ders no badge), ver­i­fy­ing, ver­i­fied, failed, skipped, or ex­tend­ing (set while /lathe-extend is writ­ing a new part). On fail­ure, a ver­ify-re­sult.json is writ­ten along­side with the failed part, step num­ber, and er­ror out­put; the web UI ren­ders it as a panel on the tu­to­r­ial page.

Sources & prove­nance

Every tu­to­r­ial keeps the re­search trail be­hind it — the URLs the gen­er­a­tion skill ac­tu­ally con­sulted while writ­ing. This is dis­tinct from the in­line ## Sources ci­ta­tions in­side a part’s mark­down: it’s a durable, tu­to­r­ial-level record stored in the sources field of meta­data.json and sur­faced in the UI as prove­nance, so you can san­ity-check where the ma­te­r­ial came from rather than tak­ing the prose on faith.

/lathe cap­tures them via lathe store –source <url> (repeatable), and /lathe-extend folds any newly-con­sulted URLs into the same trail (lathe ex­tend-com­mit –source), de-duped against what’s al­ready there.

On the list page, each card shows a · N sources count in its meta­data line.

On the read­ing page, a Researched against N sources” panel ex­pands to the full list of links.

Verification

Verification is opt-in and runs in your in­ter­ac­tive LLM ses­sion. Storing a tu­to­r­ial leaves it un­ver­i­fied and noth­ing runs un­til you ask. The lathe ver­ify <slug> com­mand, the –verify flag on lathe store, and the Verify this tu­to­r­ial but­ton in the web UI all just hand you the same com­mand to paste into your ses­sion:

/lathe-verify <slug>

The /lathe-verify skill works through every step in the tu­to­r­ial, cre­at­ing files in a fresh mk­temp -d scratch dir (never your repo), run­ning com­mands, ex­e­cut­ing each ## Checkpoint block and then calls lathe ver­ify-re­sult to record the out­come in the tu­to­ri­al’s meta­data.json. It marks the run ver­i­fy­ing when it starts and a ter­mi­nal ver­i­fied / failed / skipped when it fin­ishes.

Verification only makes sense where the tu­to­ri­al’s tool­chain is in­stalled. If a re­quired tool is miss­ing (e.g. no zig bi­nary), the run is re­ported as skipped (⚠️) rather than failed — couldn’t ver­ify here” is not the same as broken.”

Because ver­i­fi­ca­tion now runs in your own in­ter­ac­tive ses­sion, it ex­e­cutes un­der your nor­mal LLM per­mis­sion model, so you see and ap­prove the tool calls. The scratch-dir con­ven­tion keeps build ar­ti­facts out of your repo, but treat it as soft iso­la­tion at best, not a se­cu­rity bound­ary.

DeepSeek V4 Pro beats GPT-5.5 Pro on precision

runtimewire.com

The Absurdly Optimized Pancake: Leavening Chemistry, Acid-Base Stoichiometry, and an Interactive Calculator

www.absurdlyoptimized.com

Calculator out­put, plated. The lacy edges are Section IX; the rest is Sections I through VIII.

I have been mak­ing pan­cakes for twenty-five years. It was the first food I ever learned to cook, start­ing with Dorie Greenspan’s recipe from Pancakes: From Morning to Midnight. I made her recipe du­ti­fully for close to twenty years un­til some­one men­tioned Kenji López-Alt’s but­ter­milk pan­cakes, and I switched to mak­ing those du­ti­fully in­stead.

But I started to won­der whether I had ac­tu­ally found the op­ti­mal pan­cake, or just the most re­cently rec­om­mended one. And every time I made Kenji’s recipe I was an­noyed at two things: hav­ing to run out for but­ter­milk (or do men­tal sto­i­chiom­e­try to sub­sti­tute yo­gurt while in a pre-caf­feinated state), and the use of im­pre­cise cup mea­sure­ments rather than weights. I was also cu­ri­ous about com­pet­ing recipes that used sour cream, Greek yo­gurt, cot­tage cheese. Each one claimed to be the best. None of them showed their work.

So I did what any rea­son­able per­son would do. I de­rived the pan­cake from first prin­ci­ples.

Every recipe in every cook­book is a frozen snap­shot of one point in this pa­ra­me­ter space. This cal­cu­la­tor lets you ex­plore the space freely. Change what you have, change what you want, and the sto­i­chiom­e­try adapts.

1. What Actually Matters

A pan­cake has four axes of qual­ity, and most recipes op­ti­mize for at most one of them while ne­glect­ing the other three. In or­der of what you will ac­tu­ally no­tice while eat­ing:

Interior tex­ture. The in­side should be light and cus­tardy, not dense and bready. This is con­trolled by leav­en­ing (both chem­i­cal and me­chan­i­cal), pro­tein struc­ture, and hy­dra­tion ra­tio. A pan­cake that re­quires chew­ing has failed at its only job.

Tang. A flat-fla­vored pan­cake is a ve­hi­cle for maple syrup. A good pan­cake has its own acid bright­ness from resid­ual lac­tic and cit­ric acid that was in­ten­tion­ally left un-neu­tral­ized. This is a sto­i­chio­met­ric de­ci­sion: how much of your avail­able acid to con­sume with bak­ing soda (producing CO2) ver­sus how much to leave be­hind (producing fla­vor).

Rise and struc­ture. The pan­cake should be tall with­out be­ing cakey. This comes from three in­de­pen­dent CO2 sources (baking pow­der, bak­ing soda re­act­ing with acid, and steam from high-mois­ture in­gre­di­ents) plus one me­chan­i­cal source (whipped egg whites). The four sources op­er­ate on dif­fer­ent timescales, which is why they all con­tribute in­de­pen­dently.

Exterior crisp. A thin Maillard-browned shell that pro­vides tex­tural con­trast. Requires sur­face tem­per­a­ture above 140°C, re­duc­ing sug­ars, amino acids, and a mi­cro-fry­ing zone where clar­i­fied but­ter cre­ates rapid sur­face de­hy­dra­tion. The crisp here is built from that Maillard crust and the lacy ghee-fried edges, not from corn­starch: amy­lose gives a brit­tle, glassy shell, but past a small frac­tion it reads as an ar­ti­fi­cial fried-coat­ing crunch rather than a pan­cake crust, so the cal­cu­la­tor leaves it out (with a note for any­one who wants to ex­per­i­ment).

3. Background

Recipe de­vel­oped and kitchen-tested by a hu­man; AI helped with the back­ground re­search.

The old­est con­tin­u­ously pre­pared food

Pancakes are, in all prob­a­bil­ity, the old­est cooked food that mod­ern hu­mans would still rec­og­nize. Analysis of starch grains on 30,000-year-old grind­ing tools from sites in Italy (Bilancino II), Russia (Kostenki 16), and the Czech Republic (Pavlov VI) re­vealed flour made from cat­tails and ferns, likely mixed with wa­ter and cooked on hot stones (Revedin et al., 2010). This is not a pan­cake in the mod­ern sense, but it is a bat­ter cooked on a flat hot sur­face, which is the de­f­i­n­i­tion of one.

Otzi the Iceman (c. 3300 BCE) car­ried einkorn wheat with char­coal par­ti­cles con­sis­tent with flat­cake cook­ing (Maixner et al., 2018). By the 5th cen­tury BCE, the Greeks were mak­ing tegan­ites (from teganon, frying pan”): wheat flour, olive oil, honey, and cur­dled milk, served for break­fast (Athenaeus, c. 200 CE; Albala, 2008). The Roman Ova Sfongia Ex Lacte (“egg sponge with milk”) from Apicius calls for eggs, milk, and oil beaten into a bat­ter, fried, and served with honey and pep­per (Apicius, 4th cen­tury CE).

The word pancake” first ap­pears in Middle English in the 15th cen­tury (Austin, 1888). It be­came as­so­ci­ated with Shrove Tuesday be­cause house­holds needed to ex­haust their eggs, milk, but­ter, and fats be­fore the forty-day Lenten fast. Pancakes ef­fi­ciently com­bined all these per­ish­able in­gre­di­ents into a sin­gle prepa­ra­tion. The Olney Pancake Race in Buckinghamshire has been run since 1445, mak­ing it pos­si­bly the old­est con­tin­u­ously held sport­ing event mo­ti­vated en­tirely by break­fast (Albala, 2008).

The leav­en­ing rev­o­lu­tion

For most of hu­man his­tory, all pan­cakes were thin. A bat­ter of flour, eggs, and liq­uid, cooked on a hot sur­face, pro­duces a crepe. The thick fluffy pan­cake is a 19th-century in­ven­tion made pos­si­ble by chem­i­cal leav­en­ing.

The time­line: pearlash (potassium car­bon­ate, re­fined from wood ash) ap­peared in American kitchens in the 1780s and was the first chem­i­cal leav­ener (Simmons, 1796). Saleratus (sodium bi­car­bon­ate) re­placed it in the 1840s. In 1843, English chemist Alfred Bird cre­ated the first bak­ing pow­der by com­bin­ing bi­car­bon­ate of soda with tar­taric acid and starch, mo­ti­vated by his wife’s al­lergy to both eggs and yeast (Bird, 1843). In 1856, Harvard pro­fes­sor Eben Norton Horsford (a stu­dent of Justus von Liebig) patented mono­cal­cium phos­phate as a bak­ing pow­der acid, elim­i­nat­ing ex­pen­sive im­ported cream of tar­tar and found­ing the Rumford Chemical Works (Horsford, 1856; ACS). Double-acting bak­ing pow­ders (which re­lease CO2 in two stages: once when wet, again when heated) ap­peared around 1890.

The con­se­quence was the thick American pan­cake. Before chem­i­cal leav­en­ing, pan­cakes were struc­turally lim­ited to the thin bat­ter that eggs and yeast could sup­port. Baking pow­der gave bat­ters an in­ter­nal gas source that did not de­pend on yeast fer­men­ta­tion or whipped eggs, en­abling the heavy, high-hy­dra­tion bat­ters that pro­duce a tall, fluffy disc. The first com­mer­cial pan­cake mix (1889, Pearl Milling Company, St. Joseph, Missouri) com­bined wheat flour, corn flour, lime phos­phate, and salt into what is widely con­sid­ered the first ready-mix food prod­uct in com­mer­cial his­tory (Pearl Milling Company, 1889).

What bak­ing soda ac­tu­ally does

Baking soda has a rep­u­ta­tion as a pure leav­ener, and recipe com­ment threads reg­u­larly ar­gue over whether it is there for rise, for brown­ing, or for cut­ting acid­ity. The hon­est an­swer is all three at once, be­cause they are the same re­ac­tion seen from three an­gles. Sodium bi­car­bon­ate re­acts with the bat­ter’s acid to re­lease CO2 (the rise); that same re­ac­tion con­sumes acid and raises the bat­ter’s pH (less tang); and the higher pH then ac­cel­er­ates brown­ing. The three ef­fects are not sep­a­ra­ble knobs. You can­not dial in one with­out mov­ing the other two.

The brown­ing claim is the con­tested one, so it is worth pin­ning down. The Maillard re­ac­tion is not merely catalyzed in both acidic and ba­sic con­di­tions” at some flat rate; its rate climbs steeply with pH. The first and rate-de­ter­min­ing step is a nu­cle­ophilic at­tack by an amino group on the car­bonyl of a re­duc­ing sugar, and an amino group is nu­cle­ophilic only when it is de­pro­to­nated. In an acidic bat­ter most amino groups sit as un­re­ac­tive pro­to­nated am­mo­nium ions, so brown­ing is slow; rais­ing the pH frees them, and the brown­ing rate climbs steeply with pH across the weakly acidic to neu­tral range, with very lit­tle brown­ing be­low pH 6 (Martins & van Boekel, 2005). J. Kenji López-Alt showed the same thing pho­to­graph­i­cally, step­ping up the soda in oth­er­wise iden­ti­cal bat­ters and get­ting vis­i­bly darker pan­cakes each time, un­til the ex­cess soda turned soapy (López-Alt, 2015). So soda does brown, the com­menter who said it was for loft, not brown­ing” had the wrong half, and the one who in­voked an al­ka­line en­vi­ron­ment was right about the chem­istry even if you need it” over­states the case (an acidic bat­ter still browns, just re­luc­tantly, given enough heat and time).

The catch is that the brown­ing and the tang are drawn from the same well. Every in­cre­ment of pH you spend on a darker crust is acid you have neu­tral­ized and tang you have lost, which is the cen­tral con­flict this cal­cu­la­tor is built around. The res­o­lu­tion, de­vel­oped in the method­ol­ogy, is to stop us­ing pH as the brown­ing lever at all and brown by other means (concentrated re­duc­ing sug­ars and ly­sine, a clar­i­fied fry­ing fat) so that the acid can be spent on fla­vor in­stead.

The ri­cotta pan­cake: Sydney, 1993

The ri­cotta pan­cake as a dis­tinct cat­e­gory was cre­ated by Bill Granger (1969 – 2023), who opened his first restau­rant, bills,” in Darlinghurst, Sydney in 1993. He was twenty-two, self-taught, and had stud­ied art. His sig­na­ture ri­cotta hot­cakes with hon­ey­comb but­ter ap­peared in bills Sydney Food (Murdoch Books, 2000) and be­came the defin­ing dish of Australian cafe cul­ture (Granger, 2000).

The in­no­va­tion was struc­tural: ri­cot­ta’s pre-de­na­tured whey pro­teins pro­vide body with­out flour, while sep­a­rated and whipped egg whites pro­vide me­chan­i­cal leav­en­ing. The re­sult is a pan­cake with dra­mat­i­cally less gluten de­vel­op­ment and dra­mat­i­cally more pro­tein struc­ture than any flour-for­ward recipe. The New Yorker cred­ited Granger as the restau­ra­teur most re­spon­si­ble for the Australian cafe’s global reach.” He opened restau­rants in Tokyo, Seoul, and London be­fore his death in December 2023 at age 54.

Global vari­ants and what they re­veal

Every cul­ture with ac­cess to grain and a flat hot sur­face in­vented pan­cakes in­de­pen­dently, and the vari­a­tions re­veal which pa­ra­me­ters each cul­ture op­ti­mized for:

Dutch pan­nenkoeken: Large (30cm), mod­er­ately thin, served as a full meal with sa­vory fill­ings. Optimized for size and ver­sa­til­ity. The ear­li­est men­tion in a Dutch man­u­script dates to 1183 (Albala, 2008).

Russian blini: Small buck­wheat pan­cakes pre­dat­ing Christianity, orig­i­nally pa­gan sun sym­bols. Optimized for rit­ual sig­nif­i­cance and nutty fla­vor from buck­wheat. The Maslenitsa fes­ti­val (Butter Week) main­tains the tra­di­tion (Moscow Times, 2023).

Ethiopian in­jera: Spongy fer­mented teff flat­bread, nat­u­rally leav­ened by 1 – 3 days of wild lac­tic acid bac­te­ria fer­men­ta­tion. Teff has been cul­ti­vated in the Ethiopian high­lands for at least 3,000 years (Mezber/Ona Adi ex­ca­va­tions, 2021). Optimized for serv­ing as both plate and uten­sil.

Japanese souf­fle pan­cakes: Extremely tall, jig­gly, steamed in ring molds. Codified by Gram Cafe (Osaka, 2014). Optimized for height and spec­ta­cle at the ex­pense of Maillard brown­ing (Honolulu Magazine).

Korean hot­teok: Filled with brown sugar, cin­na­mon, and peanuts. Originated from Chinese mer­chants in 1880s Korea. Optimized for tex­tural con­trast be­tween crispy shell and molten fill­ing.

4. Methodology

I. Leavening: four in­de­pen­dent CO2 sources

The four sources at work: in­te­rior crumb from a blue­berry batch. The voids were CO2 and steam; the walls around them are co­ag­u­lated egg pro­tein.

A pan­cake’s rise comes from gas cells ex­pand­ing dur­ing cook­ing. Unlike bread (which re­lies on a sin­gle source: yeast fer­men­ta­tion), an op­ti­mized pan­cake bat­ter uses four in­de­pen­dent gas sources op­er­at­ing on dif­fer­ent timescales:

Source 1: Baking soda + acid (immediate). The re­ac­tion is in­stan­ta­neous upon mix­ing:

\text{NaHCO}_3 + \text{H}^+ \rightarrow \text{Na}^+ + \text{H}_2\text{O} + \text{CO}_2 \uparrow

One mole of sodium bi­car­bon­ate (84 g/​mol) re­acts with one mole of hy­dro­gen ions to pro­duce ex­actly one mole of CO2 (44 g/​mol). At 100°C and 1 atm, one mole of CO2 oc­cu­pies 30.6 L (ideal gas law). This re­ac­tion is the pri­mary rea­son to in­clude acid in­gre­di­ents (buttermilk, lemon juice, yo­gurt): each acid source is si­mul­ta­ne­ously a fla­vor con­trib­u­tor and a CO2 feed­stock.

Source 2: Baking pow­der. A self-con­tained acid-base sys­tem: sodium bi­car­bon­ate plus a pow­dered acid, with corn­starch as a buffer (BAKERpedia). The acid is what mat­ters. Some pow­ders use sodium alu­minum sul­fate, which leaves the faintly metal­lic, bit­ter af­ter­taste peo­ple blame on too much bak­ing pow­der.” Use an alu­minum-free pow­der in­stead; a mono­cal­cium-phos­phate one such as Rumford (cornstarch, sodium bi­car­bon­ate, mono­cal­cium phos­phate; not Clabber Girl, which uses the alu­minum) is the clean­est-tast­ing. Monocalcium phos­phate re­leases its CO2 on wet­ting, so add the pow­der shortly be­fore cook­ing and cook promptly, which both modes do any­way.

Source 3: Steam (thermal). High-moisture in­gre­di­ents (ricotta at 70 – 80% wa­ter, but­ter­milk, eggs at 74% wa­ter) pro­vide a reser­voir of liq­uid that va­por­izes dur­ing cook­ing. Steam is not a chem­i­cal re­ac­tion; it is a phase tran­si­tion. But it ex­pands ex­ist­ing gas cells sig­nif­i­cantly, par­tic­u­larly in the high-mois­ture en­vi­ron­ment of a ri­cotta bat­ter.

Source 4: Whipped egg whites (mechanical). When egg whites are whipped, the me­chan­i­cal force de­na­tures oval­bu­min (the ma­jor pro­tein, 54% of egg white pro­tein mass), ex­pos­ing hy­dropho­bic residues that align at the air-wa­ter in­ter­face to form a sta­ble pro­tein film around each air bub­ble (McGee, 2004). These pre-formed air cells do not re­quire any chem­i­cal re­ac­tion; they are al­ready pre­sent in the bat­ter and ex­pand ther­mally dur­ing cook­ing. Ovalbumin co­ag­u­lates ir­re­versibly at 80°C (Weijers et al., 2003), per­ma­nently set­ting the foam struc­ture.

II. Acid-base sto­i­chiom­e­try: the tang equa­tion

The cen­tral op­ti­miza­tion prob­lem in pan­cake chem­istry is this: acid serves two com­pet­ing pur­poses. It re­acts with bak­ing soda to pro­duce CO2 (desirable for rise), but the un­re­acted resid­ual acid is what pro­vides tang (desirable for fla­vor). You can­not max­i­mize both si­mul­ta­ne­ously. The ques­tion is what frac­tion of avail­able acid to neu­tral­ize.

The avail­able acid sources, their con­cen­tra­tions, and their H+ con­tri­bu­tion at typ­i­cal recipe quan­ti­ties:

Cream of tar­tar is sur­pris­ingly po­tent. Potassium hy­dro­gen tar­trate (KHC4H4O6, MW 188) is a pure dry acid: its acid mass frac­tion is 1.0, mean­ing 100% of its weight par­tic­i­pates in the acid-base re­ac­tion. Compare this to ke­fir at 1.0% acid and 90% wa­ter, or ri­cotta at 0.2% acid and 74% wa­ter. A mere 1.5g of cream of tar­tar (1/4 tea­spoon) pro­vides ap­prox­i­mately 8 mmol H+, which is 57% of the acid tar­get at tang level 4. The in­tu­ition that a tiny pinch can­not mat­ter” is wrong by an or­der of mag­ni­tude. Cream of tar­tar also sta­bi­lizes egg white foam by low­er­ing pH to­ward oval­bu­min’s iso­elec­tric point (4.5), serv­ing dou­ble duty as both acid source and foam sta­bi­lizer.

Each dairy in­gre­di­ent serves up to three in­de­pen­dent roles (structure, hy­dra­tion, acid), and sub­sti­tut­ing 1 cup but­ter­milk for 1 cup yo­gurt” is di­men­sion­ally wrong. The cal­cu­la­tor solves for each role sep­a­rately: ri­cotta and cot­tage cheese are fixed by struc­tural need (pre-denatured whey pro­teins), acidic dairy (kefir, but­ter­milk, yo­gurt, sour cream) is com­puted from the acid tar­get, and milk fills any re­main­ing hy­dra­tion deficit.

Dairy acid sources are prefer­able on every axis ex­cept acid con­cen­tra­tion. They pro­vide lac­tic acid (which pro­duces the char­ac­ter­is­tic tangy pan­cake” fla­vor that cit­ric acid does not), lac­tose (a re­duc­ing sugar for Maillard brown­ing), and pro­tein (including ly­sine, the most Maillard-reactive amino acid). Citric acid in a cooked pan­cake does not pro­duce per­ceiv­able tang; with­out strong cit­rus aroma to con­tex­tu­al­ize it, resid­ual cit­ric acid reads as vaguely sour or goes un­no­ticed. Lemon zest pro­vides dis­tinc­tive cit­rus fla­vor via aro­matic ter­penes (limonene, cit­ral), but the juice’s only role is as a con­cen­trated acid for CO2 pro­duc­tion.

Lemon juice ap­pears in the cal­cu­la­tor only when dairy acid sources alone do not pro­vide enough H+ for the de­sired tang and CO2 bal­ance. With ri­cotta only (5.6 mmol H+), sup­ple­men­tal cit­ric acid is nec­es­sary at mod­er­ate to high tang set­tings. With ri­cotta plus but­ter­milk (15.9 mmol), or ri­cotta plus sour cream and yo­gurt (16.3 mmol), dairy acid is suf­fi­cient and lemon juice is omit­ted. When cit­rus is se­lected and juice is not needed, only the zest is in­cluded for fla­vor.

Citric acid is tripro­tic (three dis­so­cia­ble pro­tons), but the ef­fec­tive ra­tio is ap­prox­i­mately 2.5 rather than 3. The rea­son in­volves pKa val­ues: the third dis­so­ci­a­tion con­stant (pKa3 = 6.40) is nearly iden­ti­cal to the pKa of car­bonic acid (H2CO3, pKa = 6.35). At bat­ter pH (~6.4), by the Henderson-Hasselbalch equa­tion, only ap­prox­i­mately 50% of cit­rate mol­e­cules have sur­ren­dered their third pro­ton. The ef­fec­tive H+ con­tri­bu­tion is there­fore 2 + 0.5 = 2.5 moles per mole of cit­ric acid (PubChem, Citric acid).

The neu­tral­iza­tion cal­cu­la­tion:

n_{\text{soda}} = n_{\text{acid}} \times f_{\text{neu­tral­ize}}

where f_{\text{neu­tral­ize}} is the frac­tion of acid to con­sume (0.30 for max­i­mum tang, 0.80 for mild). The re­main­ing acid pro­vides resid­ual acid­ity:

\text{Residual acid­ity (\%)} = \frac{(n_{\text{acid}} - n_{\text{soda}}) \times M_{\text{lactic}}}{m_{\text{batter}}} \times 100

Perception thresh­old for acid­ity in bat­ter is ap­prox­i­mately 0.05% (lactic acid equiv­a­lent). Above 0.2%, the pan­cake reads as dis­tinctly tangy. For ref­er­ence, wheat sour­dough breads con­tain 0.45 – 0.73% lac­tic acid (Clement et al., 2020), and bread pH (which cor­re­lates with sour taste at = 0.97) ranges from 4.07 to 4.40. The cal­cu­la­tor’s tang slider spans from 0.03% (below per­cep­tion) to 0.51% (solidly in the sour­dough range).

Most recipes get this wrong. A typ­i­cal lemon ri­cotta pan­cake” recipe calls for 1.5 tea­spoons of bak­ing soda (~6.9g, 0.082 mol) with 57 mL of lemon juice (0.037 mol H+) and 227g ri­cotta (0.005 mol H+). The soda ex­ceeds the to­tal acid by a fac­tor of two. Every mol­e­cule of lemon acid is neu­tral­ized. Despite the recipe’s name, the lemon juice con­tributes zero per­ceiv­able tang to the fin­ished pan­cake; all lemon fla­vor comes from the zest. Worse, the ~0.04 mol of un­re­acted NaHCO3 ther­mally de­com­poses dur­ing cook­ing into sodium car­bon­ate (Na2CO3), which is al­ka­line, bit­ter, and soapy. This is the fa­mil­iar metal­lic off-fla­vor of too much bak­ing soda.” The cal­cu­la­tor pre­vents this by com­put­ing the ex­act sto­i­chiom­e­try: it adds only enough soda to neu­tral­ize the de­sired frac­tion of acid, never more.

The sponge af­ter its overnight cool-room fer­ment, pocked with CO2 from the yeast and the ke­fir’s cul­tures. They spent the night on fla­vor, not lift.

The overnight path: fer­ment for fla­vor, soda for rise. At max­i­mum tang the cal­cu­la­tor switches to an overnight fer­ment, but not be­cause the yeast does the leav­en­ing. The rise still comes from bak­ing soda and bak­ing pow­der; the long rest is there to deepen fla­vor and to let the cul­tures push tang past what the sto­i­chiom­e­try sets. This is how yeasted and sour­dough pan­cake recipes ac­tu­ally be­have: a slow fer­ment for char­ac­ter, chem­i­cal leav­en­ing for lift (King Arthur Baking).

Why the leav­ener goes in last. Gas and time do not mix in a loose bat­ter. A mono­cal­cium-phos­phate bak­ing pow­der re­leases its CO2 the in­stant it is wet­ted. Fold it in the night be­fore and that gas es­capes the thin, pourable bat­ter long be­fore morn­ing, leav­ing noth­ing to lift the pan­cake, ex­actly why an overnight sour­dough pan­cake adds its leav­ener fresh in the morn­ing rather than the night be­fore. So in overnight mode the bak­ing pow­der is held back, folded in just be­fore cook­ing, and cooked im­me­di­ately, so its gas goes into the pan­cake in­stead of es­cap­ing the bowl overnight.

Keeping the soda does not cost the tang. The in­tu­itive worry is that soda neu­tral­izes the acid you want for sour­ness, but it does not bite here, be­cause a cul­tured-dairy bat­ter car­ries far more acid than the soda can con­sume. The soda only ever neu­tral­izes the ex­cess above the resid­ual tar­get (the same cal­cu­la­tion the quick bat­ter runs); with sev­eral hun­dred grams of ke­fir in the bowl, what re­mains sits solidly in the sour­dough range. Reliable rise and ag­gres­sive tang at the same time, no fer­men­ta­tion gas re­quired.

The yeast is a fla­vor dose, and the sugar it eats is added back. The yeast in overnight mode is small, a frac­tion of a per­cent of the flour: enough to fer­ment and aer­ate over the long rest along­side the ke­fir’s own cul­tures, not enough to be the leav­ener. It still eats sugar as it works, and here a chem­i­cal-ver­sus-bi­o­log­i­cal asym­me­try mat­ters. Baking soda con­sumes acid, not sugar, so all added sugar sur­vives into a quick bat­ter; fer­men­ta­tion re­moves sugar. Crucially the yeast can­not touch the dairy sugar, be­cause S. cere­visiae lacks the en­zyme to split lac­tose, so it lives on the added su­crose, in­vert­ing it and fer­ment­ing four moles of CO2 per mole. How much it eats tracks the yeast’s gassing rate, which is Arrhenius in tem­per­a­ture (Chiotellis & Campbell, 2003; rate near op­ti­mum per Cauvain & Young, 2007), so a cool-room fer­ment eats more than a cold one. The cal­cu­la­tor adds that amount back on top of the in­tended sugar, leav­ing the same fin­ished sweet­ness and crust brown­ing a same-day bat­ter would have.

Cool room ver­sus fridge. With the rise handed to the morn­ing soda, the fer­ment tem­per­a­ture be­comes purely a fla­vor and con­ve­nience choice. A cool room (about 20°C) keeps the ke­fir cul­tures and yeast ac­tive, so they gen­er­ate ex­tra lac­tic and acetic acid overnight and push tang past the sto­i­chio­met­ric tar­get; the cost is a tighter win­dow, since past about 12 hours or in a warm kitchen the bat­ter turns sour and sol­vent-like. A fridge (about 4°C) nearly stalls the cul­tures, so the tang stays close to what you mixed in, but the tim­ing is for­giv­ing: 8 or 14 hours look alike. Because max­i­miz­ing tang is the whole point of the overnight mode, the cal­cu­la­tor de­faults to the cool room and of­fers the fridge to cooks who would rather have a wide tim­ing win­dow than the last in­cre­ment of sour­ness. The rise is iden­ti­cal ei­ther way, be­cause it comes from the morn­ing leav­en­ers, not the fer­ment.

The overnight fer­ment also de­vel­ops mod­er­ate gluten struc­ture via hy­dra­tion, pro­duc­ing a slight chew that is ab­sent in the quick-mixed chem­i­cal leav­en­ing ver­sion. This is a fea­ture for pan­cakes where some struc­tural pull is de­sir­able (as op­posed to the pure souf­fle tex­ture of whipped-egg-only leav­en­ing). The acid con­cen­tra­tion at tang level 5 (~0.5% lac­tic equiv­a­lent, com­pa­ra­ble to the lower end of sour­dough bread) is far too low to de­na­ture gluten pro­teins overnight, so the chew de­vel­ops with­out degra­da­tion. At the much higher acid­ity of a ma­ture sour­dough (roughly 0.8 – 1.2% lac­tic acid), the gluten net­work does weaken over a few hours, not by any di­rect at­tack on its disul­fide crosslinks but be­cause the low pH raises the pro­teins’ net pos­i­tive charge and sol­u­bil­ity, loos­en­ing their non-co­va­lent bonds, and switches on en­doge­nous ce­real pro­teases that hy­drolyze the glutenin (Thiele et al., 2004), but this thresh­old is never ap­proached in the cal­cu­la­tor’s acid range.

The morn­ing af­ter: a yolk goes into the sponge. The rise still comes from the soda and pow­der added now; the night was for tang.

Kefir’s live cul­tures dur­ing overnight fer­ment. Kefir is not a ster­ile acid source. It con­tains live Lactobacillus bac­te­ria and wild yeast strains that stay meta­bol­i­cally ac­tive in the bat­ter. In an overnight fer­ment, these or­gan­isms pro­duce ad­di­tional lac­tic acid be­yond what was pre­sent when the bat­ter was mixed, mak­ing the fin­ished pan­cake tang­ier than the sto­i­chiom­e­try alone pre­dicts. Like the yeast, their rate is tem­per­a­ture-de­pen­dent: at a cool room (~20°C) the self-sour­ing is mean­ing­ful, while at fridge tem­per­a­ture (bacterial me­tab­o­lism slows roughly 2 – 4x at 4°C com­pared to 25°C) it is small. This is the bi­o­log­i­cal half of why the cal­cu­la­tor de­faults to a cool-room fer­ment when tang is max­i­mized: the cul­tures fin­ish the job the sto­i­chiom­e­try starts. Buttermilk cul­tures be­have sim­i­larly but are less di­verse; ke­fir’s sym­bi­otic colony (the kefir grain”) con­tains dozens of bac­te­r­ial and yeast species com­pared to but­ter­milk’s 2 – 3.

The two modes leaven dif­fer­ently. The quick bat­ter uses bak­ing soda plus bak­ing pow­der, mixed in at the start: there is ex­cess acid above the (low) tang tar­get, and the soda turns it into free CO2. The overnight bat­ter, at max­i­mum tang, uses bak­ing pow­der alone, folded in the morn­ing, be­cause soda would con­sume the very acid the long fer­ment is build­ing into tang; the bak­ing pow­der car­ries the rise from its own acid with­out touch­ing the bat­ter’s. Whipped egg whites add me­chan­i­cal lift in both, but the de­pend­able rise is chem­i­cal.

III. Gluten in­hi­bi­tion: why less flour works

Wheat flour con­tains two stor­age pro­teins, glutenin and gliadin, which to­gether com­prise 80 – 85% of to­tal flour pro­tein. When hy­drated and me­chan­i­cally worked, they bond into gluten: a vis­coelas­tic net­work of cross-linked pro­tein sheets joined by disul­fide bridges, hy­dro­gen bonds, and hy­dropho­bic in­ter­ac­tions (McGee, 2004).

Gluten is de­sir­able in bread (where elas­tic­ity traps yeast-pro­duced CO2 over long fer­men­ta­tion) and un­de­sir­able in pan­cakes (where it makes the in­te­rior tough and chewy). Three strate­gies min­i­mize gluten de­vel­op­ment:

Minimal mix­ing. Mechanical ac­tion aligns glutenin strands into or­ga­nized sheets. Organized gluten re­sists CO2 ex­pan­sion. The stan­dard in­struc­tion (“fold 10 – 12 strokes un­til just com­bined; lumps are de­sir­able”) is not a sug­ges­tion about aes­thet­ics; it is a struc­tural pre­scrip­tion. Overmixed bat­ter can lose 30% or more of its po­ten­tial vol­ume (McGee, 2004).

Fat as phys­i­cal bar­rier. Fat mol­e­cules bond to hy­dropho­bic amino acids along gluten pro­tein chains, phys­i­cally pre­vent­ing those chains from bond­ing to each other (McGee, 2004). This is the lit­eral mean­ing of shortening”: fat cre­ates a shorter, weaker gluten net­work. In a ri­cotta pan­cake, both the ri­cotta fat (10 – 13%) and the melted but­ter serve this func­tion. The cal­cu­la­tor tar­gets roughly 9% fat by bat­ter weight, which lands squarely among ac­claimed rich pan­cakes: work­ing from the pub­lished quan­ti­ties, a but­ter­milk-and-sour-cream bat­ter runs about 10% (López-Alt, 2015), a sour­dough-sponge bat­ter about 8% (NYT Cooking), and a sour-cream bat­ter as high as 13% (Perelman, 2009). The role mat­ters more than the source: the last bat­ter reaches the top of that range on sour cream alone, with no added but­ter at all, which is why the cal­cu­la­tor sizes but­ter only to fill what­ever fat the cho­sen dairy leaves short of the tar­get.

Protein sub­sti­tu­tion. By re­plac­ing much of the flour (and its gluten-form­ing pro­teins) with ri­cotta (which pro­vides struc­ture via pre-de­na­tured whey pro­teins that do not form gluten), the to­tal avail­able glutenin and gliadin is re­duced. The recipe drops from 165g flour (standard) to 125g flour (with ri­cotta): a 24% re­duc­tion in po­ten­tial gluten.

IV. Ricotta: pre-de­na­tured whey pro­teins

Ricotta” is Italian for recooked” (Latin re­co­quere: re- again” + co­quere to cook”). The name de­scribes the pro­duc­tion method: whey left over from pri­mary cheese­mak­ing is heated a sec­ond time to 80 – 93°C, de­na­tur­ing and ag­gre­gat­ing the whey pro­teins (primarily beta-lac­toglob­u­lin and al­pha-lac­tal­bu­min) that ca­sein-based cheeses leave be­hind (Journal of Dairy Science, 1988).

This pre-de­nat­u­ra­tion is the key in­sight. Beta-lactoglobulin un­der­goes ir­re­versible un­fold­ing and ag­gre­ga­tion above 78°C (ScienceDirect, 2015). Because ri­cotta has al­ready been heated to 80 – 93°C dur­ing pro­duc­tion, its pro­teins are fully de­na­tured be­fore they en­ter the pan­cake bat­ter. When the bat­ter is cooked again (internal tem­per­a­ture reach­ing ap­prox­i­mately 100°C), the ri­cotta pro­teins do not un­dergo fur­ther struc­tural change. They pro­vide a soft, cus­tard-like ma­trix with­out the tight­en­ing that oc­curs when raw pro­teins (like egg white) are de­na­tured for the first time.

Additionally, ri­cot­ta’s high mois­ture con­tent (around 74% wa­ter, within the 82.5% ceil­ing the USDA sets for ri­cotta) serves as a steam reser­voir dur­ing cook­ing, con­tribut­ing to the char­ac­ter­is­tic light­ness of the fi­nal pan­cake (USDA AMS).

V. The Maillard re­ac­tion and ex­te­rior brown­ing

Maillard brown­ing from a milk-solid-free pan fat: even color, lacy edge. The in­te­rior never ex­ceeded 100°C; this side did.

The golden-brown ex­te­rior of a pan­cake is pro­duced by the Maillard re­ac­tion: a non-en­zy­matic brown­ing re­ac­tion be­tween re­duc­ing sug­ars and amino acids that pro­duces melanoidins (brown pig­ments) and hun­dreds of volatile fla­vor com­pounds. It was first de­scribed by Louis-Camille Maillard in 1912 (Maillard, 1912), largely ig­nored un­til 1941, and for­mally sys­tem­atized by John Hodge in the most-cited pa­per in food sci­ence his­tory (Hodge, 1953; Finot, 2005).

The re­quire­ments for Maillard brown­ing on a pan­cake sur­face:

Surface tem­per­a­ture above 140°C. The in­te­rior of a pan­cake never ex­ceeds 100°C (limited by wa­ter’s boil­ing point), but the sur­face in con­tact with but­tered pan reaches 175 – 200°C (ThermoWorks). This tem­per­a­ture dif­fer­en­tial is why pan­cakes are brown out­side and pale in­side.

Reducing sug­ars. Sucrose (table sugar) in the bat­ter pro­vides glu­cose and fruc­tose upon in­ver­sion. Lactose from milk and ri­cotta is also a re­duc­ing sugar. Fructose be­gins carameliz­ing at 110°C (vs. 160°C for su­crose), so re­plac­ing some white sugar with honey (38% fruc­tose, 31% glu­cose) pro­vides more re­ac­tive Maillard fuel.

Amino acids. Provided by egg pro­teins, milk ca­sein, and whey pro­teins.

Low wa­ter ac­tiv­ity at the sur­face. A wet sur­face can­not ex­ceed 100°C. Surface de­hy­dra­tion is the rate-lim­it­ing step for crisp for­ma­tion. Clarified but­ter (smoke point 230°C vs. whole but­ter at 177°C) en­ables higher pan tem­per­a­tures, and its pure fat cre­ates more ef­fec­tive mi­cro-fry­ing zones that de­hy­drate the sur­face faster (Modernist Cuisine).

Cornstarch pro­duces crispier sur­faces than wheat flour be­cause its higher amy­lose con­tent (25 – 28% vs. wheat’s 20 – 22%) forms a rigid, porous, brit­tle net­work af­ter de­hy­dra­tion. The amy­lose mol­e­cules cross-link dur­ing heat­ing, cre­at­ing a struc­ture that frac­tures cleanly rather than bend­ing, and re­sists mois­ture re-ab­sorp­tion (America’s Test Kitchen; Cho et al., 2019). Mohamed et al. con­firmed this di­rectly: in model bat­ter sys­tems, crisp­ness cor­re­lated pos­i­tively with amy­lose con­tent and in­versely with oil ab­sorp­tion (Mohamed et al., 1998). Altunakar et al. tested corn starch, amy­lo­maize (70% amy­lose), waxy maize (0% amy­lose), and prege­la­tinized tapi­oca in chicken nugget bat­ters: corn starch pro­duced the high­est poros­ity, and all high-amy­lose starches sig­nif­i­cantly out­per­formed waxy maize for crisp­ness (Altunakar et al., 2004).

The cal­cu­la­tor adds no corn­starch, but it re­mains avail­able as a man­ual ex­per­i­ment. Frying bat­ters lean on it heav­ily: Korean fried chicken bat­ters rou­tinely use 50% corn­starch, ATKs fried chicken recipe uses 1:1, and com­mer­cial bat­ter patents spec­ify 50 – 80% high-amy­lose flour in the dry mix. Shih and Daigle found that high-amy­lose rice flour bat­ters re­duced oil up­take by up to 62%, though pure-starch coat­ings be­came more brit­tle (Shih & Daigle, 1999). Primo-Martín et al. showed that cross-linked starch (resistant to gela­tiniza­tion) fur­ther im­proved crisp­ness mea­sured by acoustic emis­sion, re­duc­ing oil con­tent from 28% to 20 – 23% (Primo-Martín et al., 2012). Two things ar­gue against it in a pan­cake. First, struc­ture: those bat­ters are coat­ings cling­ing to a sub­strate, while a free-stand­ing pan­cake needs gluten in­tegrity to flip with­out tear­ing, which is why a home ex­per­i­ment should stay un­der about 30% of the flour by weight. Second, fla­vor and tex­ture: amy­lose crisp is brit­tle and fla­vor­less, and past a small frac­tion it reads as an ar­ti­fi­cial, fried-coat­ing crunch rather than a pan­cake crust. So the de­pend­able, fla­vor­ful crisp is left to the Maillard crust and the lacy ghee-fried edges (a dry, open-pan fin­ish on a gen­er­ous fat), which carry both tex­ture and taste.

However, corn­starch also re­duces Maillard brown­ing: it con­tains 0.26% pro­tein com­pared to flour’s 10.3%, re­mov­ing roughly 40x the avail­able amino acids per gram re­placed. A crispy but pale pan­cake is a half-solved prob­lem. The cal­cu­la­tor com­pen­sates on both sides of the Maillard equa­tion: the sugar side and the amino acid side.

Not all sug­ars par­tic­i­pate equally in the Maillard re­ac­tion. Reducing sug­ars (those with a free alde­hyde or ke­tone group) re­act di­rectly with amino acids; su­crose, a non-re­duc­ing dis­ac­cha­ride, must first hy­drolyze into glu­cose and fruc­tose be­fore it can par­tic­i­pate. The brown­ing rate at pH 6 varies dra­mat­i­cally by sugar type (Buera et al., 1987):

Common sweet­ener sub­sti­tutes vary enor­mously in their Maillard po­ten­tial. Honey (81% re­duc­ing sug­ars by weight) is the clear win­ner. Molasses (24.7% re­duc­ing sug­ars plus cat­alytic iron and cop­per) has gen­uine brown­ing ben­e­fit but its strong fla­vor lim­its it to small doses. Brown sugar (2.5% re­duc­ing sug­ars from its ~3.5% mo­lasses coat­ing) and maple syrup (2.1% re­duc­ing sug­ars, 97% of its sugar is su­crose) are nearly iden­ti­cal to white sugar for brown­ing pur­poses; their ap­peal is fla­vor, not chem­istry (USDA FoodData Central).

Honey (approximately 38% fruc­tose, 31% glu­cose, 17% wa­ter) de­liv­ers 69% im­me­di­ately avail­able re­duc­ing sug­ars by weight, com­pared to 0% from white sugar un­til heat and acid cleave the gly­co­sidic bond. This makes honey a sub­stan­tially more ef­fec­tive Maillard fuel per gram of sweet­ener. However, this same re­ac­tiv­ity is the rea­son the cal­cu­la­tor does not use it: honey browns too ag­gres­sively at the pan tem­per­a­tures needed for proper in­te­rior cook­ing, nar­row­ing the for­give­ness win­dow be­tween golden and burnt to the point where con­sis­tent re­sults re­quire a PID-controlled cook­top. The cal­cu­la­tor uses white sugar and com­pen­sates for re­duced brown­ing via milk pow­der (concentrated ly­sine and lac­tose) on the amino acid side of the Maillard equa­tion.

The other Maillard re­ac­tant, amino acids, also varies dra­mat­i­cally across bat­ter in­gre­di­ents. Lysine is the most re­ac­tive amino acid in the Maillard re­ac­tion be­cause its side chain pro­vides an ep­silon-amino group in ad­di­tion to the al­pha-amino group pre­sent in all amino acids (Hemmler et al., 2018). Wheat flour con­tains only 285 mg ly­sine per 100g (lysine is wheat’s first lim­it­ing amino acid), while non­fat dry milk pow­der con­tains 2,720 mg per 100g. Even 10 – 15g of milk pow­der (about one ta­ble­spoon) adds 270 – 400 mg of ly­sine to the bat­ter, plus con­cen­trated lac­tose as an ad­di­tional re­duc­ing sugar. At high crisp set­tings where corn­starch dis­places flour pro­tein (and its al­ready-lim­ited amino acids), milk pow­der com­pen­sates on the amino acid side of the Maillard equa­tion.

This has an in­ter­est­ing im­pli­ca­tion for acid source se­lec­tion. Dairy acid sources (buttermilk, sour cream, Greek yo­gurt, ri­cotta) are dual-pur­pose: they pro­vide lac­tic acid for tang and CO2 pro­duc­tion, but the car­rier also brings lac­tose (a re­duc­ing sugar) and pro­tein (including ly­sine) that par­tic­i­pate di­rectly in Maillard brown­ing. Citrus juice pro­vides acid and noth­ing else for brown­ing. Fifty-five grams of sour cream con­tributes roughly 2g of pro­tein (~110 mg ly­sine) along­side its lac­tic acid; 30 mL of lemon juice con­tributes ~0.1g pro­tein and no re­duc­ing sug­ars. For max­i­mum brown­ing, at least one dairy acid source should be pre­sent. Citrus is not wasted in this con­fig­u­ra­tion: lemon zest pro­vides aro­matic ter­penes (limonene, cit­ral) that dairy can­not repli­cate, while the juice pro­vides ad­di­tional acid for tang. The ideal com­bi­na­tion for both brown­ing and fla­vor com­plex­ity is dairy acids plus cit­rus, not one or the other.

Salt (NaCl) has a bipha­sic re­la­tion­ship with the Maillard re­ac­tion that is of­ten mis­un­der­stood. At mod­er­ate con­cen­tra­tions (around 0.5% Na+, or roughly 1.25% NaCl), sodium ions ac­tu­ally pro­mote brown­ing: Luo et al. mea­sured 8.2x higher brown­ing in­ten­sity at 140°C com­pared to un­salted con­trols (Luo et al., 2019). The pro­posed mech­a­nism is that Na+ ions sta­bi­lize the tran­si­tion state of the con­den­sa­tion re­ac­tion be­tween car­bonyl and amino groups (Zhang et al., 2022). Only at very high con­cen­tra­tions (above ~6% NaCl) does the in­hibitory ef­fect dom­i­nate (Kwak & Lim, 2004). Typical sea­son­ing con­cen­tra­tions in bat­ters and on meat sur­faces (1 – 2% NaCl) fall in the pro­mot­ing range. Yamaguchi et al. fur­ther showed that NaCl does not sig­nif­i­cantly af­fect the brown­ing rate of glu­cose with pep­tides and pro­teins, only with free amino acids (Yamaguchi et al., 2009). Since the amino groups in a pan­cake bat­ter are mostly bound in in­tact pro­teins (flour gluten, egg al­bu­min, whey), the di­rect chem­i­cal ef­fect of salt on brown­ing at nor­mal sea­son­ing lev­els is likely neg­li­gi­ble.

Alkaline con­di­tions ac­cel­er­ate the Maillard re­ac­tion be­cause amino groups (RNH3+ at low pH be­come RNH2 at high pH) have in­creased nu­cle­ophilic­ity, mak­ing them more re­ac­tive with car­bonyl groups. This is why ex­cess bak­ing soda causes rapid brown­ing. J. Kenji López-Alt demon­strated this di­rectly by pho­tograph­ing iden­ti­cal bat­ters with in­creas­ing amounts of bak­ing soda: each in­cre­ment pro­duced vis­i­bly more brown­ing, up to the point where ex­cess un-neu­tral­ized soda pro­duced a soapy off-fla­vor (López-Alt, 2015).

This cre­ates a gen­uine trade­off be­tween tang and crisp that can­not be en­gi­neered away. Baking soda and acid are in the same liq­uid; they re­act spon­ta­neously on con­tact. You can­not add soda just for brown­ing” with­out it also neu­tral­iz­ing some of the resid­ual acid that pro­vides tang. The brown­ing ac­cel­er­a­tion re­quires rais­ing bat­ter pH, which means con­sum­ing hy­dro­gen ions, which means less sour­ness. The two goals are in di­rect chem­i­cal con­flict.

The cal­cu­la­tor re­solves this by keep­ing brown­ing en­tirely non-al­ka­line at all crisp lev­els: corn­starch (whose amy­lose net­work crisps in­de­pen­dently of pH), milk pow­der (concentrated ly­sine and lac­tose for the amino acid side of the Maillard re­ac­tion), and clar­i­fied but­ter or ghee (whose higher smoke point en­ables faster sur­face de­hy­dra­tion). These path­ways do not touch the acid bal­ance. You get max­i­mum crisp and max­i­mum tang si­mul­ta­ne­ously, no trade­off re­quired.

The pan tem­per­a­ture is com­puted from a ther­mal model that bal­ances two com­pet­ing timescales: the time for the cen­ter of the risen pan­cake to reach 95°C (egg pro­tein full co­ag­u­la­tion), and the time for the sur­face to reach tar­get brown­ing. The cen­ter time comes from the 1D slab heat equa­tion with mea­sured bat­ter ther­mal dif­fu­siv­ity (Baik et al., 1999; α = 1.3 × 10−7 m2/​s). The sur­face brown­ing rate fol­lows Arrhenius ki­net­ics with Ea = 64 kJ/​mol, mea­sured di­rectly on bread crust at 140 – 250°C (Zanoni et al., 1995). Honey mul­ti­plies the ef­fec­tive brown­ing rate by ap­prox­i­mately 1.7× (derived from the bak­ing guide­line of re­duc­ing oven tem­per­a­ture by 25°F for honey-sweet­ened goods, com­bined with the Zanoni ac­ti­va­tion en­ergy). Milk pow­der adds a fur­ther in­cre­ment via con­cen­trated ly­sine (27.2 mg/​g vs. 2.85 mg/​g in flour), the most Maillard-reactive amino acid. The cal­cu­la­tor solves for the pan tem­per­a­ture at which the sur­face reaches tar­get brown­ing ex­actly when the cen­ter is done, en­sur­ing even cook­ing with­out burn­ing.

Making Peace With Your Unlived Dreams

nik.art

I will never be a great snow­boarder. For var­i­ous ge­netic and non-ge­netic rea­sons, my knees are barely ca­pa­ble of sur­viv­ing a three-hour hike, let alone the land­ing af­ter a 1080.

In fact, I’ll prob­a­bly never be a snow­boarder at all, given my or­tho­pe­dist told me to stay away from any­thing that’s heavy on the knees, like ten­nis, ski­ing, or, say, snow­board­ing,” as long as 15 years ago. It sucks. I’d love to take snow­board­ing lessons. Alas, all I can do is watch videos of peo­ple do­ing sick stunts, liv­ing vic­ar­i­ously through GoPro’s Youtube chan­nel.

When I first found out, for a good while, I was re­ally up­set about this. How dare life take that from me!” I of­ten imag­ined what would hap­pen if I went big on snow­board­ing any­way. That there must be a way for me to fix my knees enough to suc­ceed, and, to be fair, there prob­a­bly is. But at some point, I re­al­ized that life is big but also short.

When asked What’s one ex­pe­ri­ence you hope we’ll share in the fu­ture?” ex-Bach­e­lor star Sharleen Joynt tells her hus­band: It’s hard. I want to do every­thing with you. There’s not enough time.”

You know what else I’d like to do be­sides be­com­ing a great snow­boarder? I want to learn kung fu. I’d also love to be a lot bet­ter at video games, get my Yu-Gi-Oh! hobby back on, and be­come at least flu­ent enough for every­day con­ver­sa­tion in oh, I don’t know, eight more lan­guages.

Meanwhile, back down on earth, I’m self-em­ployed. I spend most of my time work­ing, and when I don’t work, I try to be with my girl­friend, or fam­ily, or friends. It ebbs and flows, of course, but over the last few weeks, I’ve barely man­aged to make time to read, let alone pur­sue other, sec­ond-tier hob­bies.

Even if I won the lot­tery to­mor­row, how­ever, I doubt there’d be enough time. There’s never enough time. If Death ex­cused me for a few hun­dred years, I’d def­i­nitely take it.

And yet, some­how, the more years go by, the more rarely I watch snow­board­ing videos. My imag­i­na­tion runs wild less of­ten, and when it does, it comes with smiles more so than bit­ter­ness. It’s okay. Leave the snow­board­ing to oth­ers. You are a writer. You have things to do where you are, and that is enough.”

Use your imag­i­na­tion. Sometimes, dreams can just be dreams. They need­n’t all come true to feel sat­is­fy­ing. Watch videos. Read books. Spend time with the he­roes you’ll never meet. Whatever you do, don’t get an­gry at your un­lived dreams. Extend a hand. Make peace.

We only get to sam­ple a small taste of every­thing life has to of­fer, but in choos­ing de­lib­er­ately, we are do­ing the most im­por­tant job we were brought here to do.

APC–2

teenage.engineering

a pro­fes­sional record cut­ter built and de­signed for pro­duc­ing

orig­i­nal play­back discs with su­pe­rior sound qual­ity in real time. avail­able

ex­clu­sively via our col­lab­o­ra­tive part­ners and mas­ters of ana­log me­dia,

SUPERSENSE. our shared vi­sion is to en­able ac­cess to any­one who wants

their mu­sic or sound on a phys­i­cal record. so far, only a lim­ited set of

ma­chines has been built. email us to find out how to get this ma­chine.vari­able pitch con­trol

au­toma­tion pos­si­ble di­rectly

from DAW al­lows locked grooves

and other spe­cialty cutsa pro­fes­sional record cut­ter built and de­signed for pro­duc­ing orig­i­nal play­back discs with su­pe­rior sound qual­ity in real time. avail­able ex­clu­sively via our col­lab­o­ra­tive part­ners and mas­ters of ana­log me­dia,

SUPERSENSE. our shared vi­sion is to en­able ac­cess to any­one who wants their mu­sic or sound on a phys­i­cal record. so far, only a lim­ited set of ma­chines has been built. email us to find out how to get this ma­chine.vari­able pitch con­trol

au­toma­tion pos­si­ble di­rectly from DAW al­lows locked grooves and other spe­cialty cuts

To add this web app to your iOS home screen tap the share button and select "Add to the Home Screen".

10HN is also available as an iOS App

If you visit 10HN only rarely, check out the the best articles from the past week.

Visit pancik.com for more.