10 interesting stories served every morning and every evening.

GrapheneOS (@GrapheneOS@grapheneos.social)

grapheneos.social

To use the Mastodon web ap­pli­ca­tion, please en­able JavaScript. Alternatively, try one of the na­tive apps for Mastodon for your plat­form.

Local AI Needs to be the Norm

unix.foo

One of the cur­rent trends in mod­ern soft­ware is for de­vel­op­ers to slap an API call to OpenAI or Anthropic for fea­tures within their app. Reasonable peo­ple can quib­ble with whether those fea­tures are ac­tu­ally bring­ing value to users, but what I want to dis­cuss is the fun­da­men­tal con­cept of tak­ing on a de­pen­dency to a cloud hosted AI model for ap­pli­ca­tions.

This lazi­ness is cre­at­ing a gen­er­a­tion of soft­ware that is frag­ile, in­vades your pri­vacy, and fun­da­men­tally bro­ken. We are build­ing ap­pli­ca­tions that stop work­ing the mo­ment the server crashes or a credit card ex­pires.

We need to re­turn to a habit of build­ing soft­ware where our lo­cal de­vices do the work. The sil­i­con in our pocket is mind bog­glingly faster than what was avail­able a decade ago. It has a ded­i­cated Neural Engine sit­ting there, mostly idle, while we wait for a JSON re­sponse from a server farm in Virginia. That’s ridicu­lous.

Even if your in­ten­tions are pure, the mo­ment you stream user con­tent to a third party AI provider, you’ve changed the na­ture of your prod­uct. You now have data re­ten­tion ques­tions and all the bag­gage that comes with that (consent, au­dit, breach, gov­ern­ment re­quest, train­ing, etc.)

On top of that you also sub­stan­tially com­pli­cated your stack be­cause your fea­ture now de­pends on net­work con­di­tions, ex­ter­nal ven­dor up­time, rate lim­its, ac­count billing, and your own back­end health.

Congratulations! You took a UX fea­ture and turned it into a dis­trib­uted sys­tem that costs you money.

If the fea­ture can be done lo­cally, opt­ing into this mess is self in­flicted dam­age.

AI every­where” is not the goal. Useful soft­ware is the goal.

Concrete Example: Brutalist Report’s On-Device Summaries

Years ago I launched a fun side pro­ject named The Brutalist Report, a news ag­gre­ga­tor ser­vice in­spired by the 1990s style web.

Recently, I de­cided to build a na­tive iOS client for it with the de­sign goal of en­sur­ing it would re­main a high-den­sity news read­ing ex­pe­ri­ence. Headlines in a stark list, a reader mode that strips the can­cer that has over­taken the web, and (optionally) an intelligence” view that gen­er­ates a sum­mary of the ar­ti­cle.

Here’s the key point though: the sum­mary is gen­er­ated on-de­vice us­ing Apple’s lo­cal model APIs. No server de­tours. No prompt or user logs. No ven­dor ac­count. No we store your con­tent for 30 days” foot­notes needed.

It has be­come so nor­mal for folks that any AI use is hap­pen­ing server-side. We have a lot of work to do to turn this around as an in­dus­try.

It’s not lost on me that some­times the use-cases you have will de­mand the in­tel­li­gence that only a cloud hosted model can pro­vide, but that’s not the case with every use-case you’re try­ing to solve. We need to be thought­ful here.

Available Tooling

I can only speak on the tool­ing avail­able within the Apple ecosys­tem since that’s what I fo­cused ini­tial de­vel­op­ment ef­forts on. In the last year, Apple has in­vested heav­ily here to al­low de­vel­op­ers to make use of a built-in lo­cal AI model eas­ily.

The core flow looks roughly like this:

im­port FoundationModels

let model = SystemLanguageModel.default guard model.avail­abil­ity == .available else { re­turn }

let ses­sion = LanguageModelSession { ”″ Provide a bru­tal­ist, in­for­ma­tion-dense sum­mary in Markdown for­mat. - Use **bold** for key con­cepts. - Use bul­let points for facts. - No fluff. Just facts. ”″ }

let re­sponse = try await ses­sion.re­spond(op­tions: .init(maximumResponseTokens: 1_000)) { ar­ti­cle­Text }

let mark­down = re­sponse.con­tent

And for longer con­tent, we can chunk the plain text (around 10k char­ac­ters per chunk), pro­duce con­cise facts only” notes per chunk, then runs a sec­ond pass to com­bine them into a fi­nal sum­mary.

This is the kind of work lo­cal mod­els are per­fect for. The in­put data is al­ready on the de­vice (because the user is read­ing it). The out­put is light­weight. It’s fast and pri­vate. It’s okay if it’s not a su­per­hu­man PhD level in­tel­li­gence be­cause it’s sum­ma­riz­ing the page you just loaded, not in­vent­ing world knowl­edge.

Local AI shines when the mod­el’s job is trans­form­ing user-owned data, not act­ing as a search en­gine for the uni­verse.

There are plenty of AI fea­tures that peo­ple want but don’t trust. Summarizing emails, ex­tract ac­tion items from notes, cat­e­go­rize this doc­u­ment, etc.

The usual cloud ap­proach turns every one of those into a trust ex­er­cise. Please send your data to our servers. We promise to be cool about it.”

Local AI changes that. Your de­vice al­ready has the data. We’ll do the work right here.

You don’t build trust with your users by writ­ing a 2,000 word pri­vacy pol­icy. You build trust by not need­ing one to be­gin with.

The tool­ing avail­able on the plat­form goes even fur­ther.

One of the best moves Apple has made re­cently is push­ing AI out­put” away from un­struc­tured blobs of text and to­ward typed data.

Instead of ask the model for JSON and pray”, the newer and bet­ter pat­tern is to de­fine a Swift struct that rep­re­sents the thing you want. Give the model guid­ance for each field in nat­ural lan­guage. Ask the model to gen­er­ate an in­stance of that type.

That’s it.

Conceptually, it looks like this:

im­port FoundationModels

@Generable struct ArticleIntel { @Guide(description: One sen­tence. No hype.“) var tldr: String @Guide(description: 3 – 7 bul­lets. Facts only.“) var bul­lets: [String] @Guide(description: Comma-separated key­words.“) var key­words: [String] }

let ses­sion = LanguageModelSession() let re­sponse = try await ses­sion.re­spond( to: Extract struc­tured notes from the ar­ti­cle.”, gen­er­at­ing: ArticleIntel.self ) { ar­ti­cle­Text }

let in­tel = re­sponse.con­tent

Now your UI does­n’t have to scrape bul­let points out of Markdown or hope the model re­mem­bered your JSON schema. You get a real type with real fields, and you can ren­der it con­sis­tently. It pro­duces struc­tured out­put your app can ac­tu­ally use. And it’s all run­ning lo­cally!

This is­n’t just nicer er­gonom­ics. It’s an en­gi­neer­ing im­prove­ment.

And if you’re build­ing a lo­cal first app, this is the dif­fer­ence be­tween AI as nov­elty” and AI as a trust­wor­thy sub­sys­tem”.

But Local Models Aren’t As Smart”

Correct.

But also so what?

Most app fea­tures don’t need a model that can write Shakespeare, ex­plain quan­tum me­chan­ics, and pass the bar exam. They need a model that can do one of these re­li­ably: sum­ma­rize, clas­sify, ex­tract, rewrite, or nor­mal­ize.

And for those tasks, lo­cal mod­els can be truly ex­cel­lent.

If you try to use a lo­cal model as a re­place­ment for the en­tire in­ter­net, you will be dis­ap­pointed. If you use it as a data trans­former” sit­ting in­side your app, you’ll won­der why you ever sent this stuff to a server.

Use cloud mod­els only when they’re gen­uinely nec­es­sary. Keep the user’s data where it be­longs. And when you do use AI, don’t just glue it as a chat box. Use it as a real sub­sys­tem with typed out­puts and pre­dictable be­hav­ior.

Stop ship­ping dis­trib­uted sys­tems when you meant to ship a fea­ture.

Im going back to writing code by hand

blog.k10s.dev

09 May, 2026

This dev-log is get­ting a lot of at­ten­tion on HN (scary!): HN Thread.

To those who are com­ing here from HN: This started as an in­ves­ti­ga­tion or rather a ques­tion: How far I can get with build­ing a piece of soft­ware by keep­ing my­self com­pletely out of the loop”. The tl;dr of this dev log is that I still need to be in the loop to make any­thing mean­ing­ful. Take aways:

like em-dash” is to ai writ­ing, god-object” is to ai cod­ing

vibe-cod­ing makes every­thing feel cheap and you may end up los­ing fo­cus and build­ing bloat

let a hu­man (you) write the ar­chi­tec­ture and don’t just keep ask­ing it for fea­tures

And some AGENTS.md/CLAUDE.md di­rec­tives that I felt help me stay out of the loop a lit­tle more

Humans in­ter­ven­tion is still needed as of 10/05/2026. You can to­tally go back now! [3]

I

Here is k10s: https://​github.com/​shvb­sle/​k10s/​tree/​archive/​go-v0.4.0

234 com­mits. ~30 week­ends. Built en­tirely on vibe-coded ses­sions with Claude, when­ever my to­kens lasted long enough to ship some­thing.

I’m archiv­ing my TUI tool and rewrit­ing it from scratch.

k10s started as a GPU-aware Kubernetes dash­board (and my first foray into build­ing some­thing se­ri­ous with AI). Think k9s but built for the peo­ple run­ning NVIDIA clus­ters, peo­ple who ac­tu­ally care about GPU uti­liza­tion, DCGM met­rics, and which nodes are sit­ting idle burn­ing $32/hr. I built it in Go with Bubble Tea [1] and it worked.

For a while… :(

I learned over these 7 months is worth more than the 1690 lines of model.go I’m throw­ing away. And I think any­one do­ing se­ri­ous vibe-cod­ing can ben­e­fit from this, be­cause this part does­n’t sur­face much (I feel it gets buried un­der the demo reels and the ve­loc­ity wins).

tl;dr: AI writes fea­tures, not ar­chi­tec­ture. The longer you let it drive with­out con­straints, the worse the wreck­age gets. The ve­loc­ity makes you think you’re win­ning right up un­til the mo­ment every­thing col­lapses si­mul­ta­ne­ously.

II

vibe cod­ing high

I started k10s in late September 2025. The first few weeks were magic. I’d prompt Claude with add a pods view with live up­dates” and boom, it worked. Resource list views, name­space fil­ter­ing, log stream­ing, de­scribe pan­els, key­board nav­i­ga­tion. Each fea­ture landed clean be­cause the pro­ject was small enough that the AI could hold the whole thing in con­text.

The ba­sic k9s clone took maybe 3 week­ends. Resource views for pods, nodes, de­ploy­ments, ser­vices. A com­mand palette. Watch-based live up­dates. Vim key­bind­ings. All work­ing, all vibe-coded in sin­gle ses­sions. I was build­ing at maybe 10x my nor­mal speed and it felt in­cred­i­ble.

Then I wanted the main sell­ing point.

The whole rea­son k10s ex­ists is the GPU fleet view. A ded­i­cated screen that shows you every node’s GPU al­lo­ca­tion, uti­liza­tion from DCGM, tem­per­a­ture, power draw, mem­ory. Not buried in kubectl de­scribe node out­put, but right there in a pur­pose-built table with color-coded sta­tus. Idle nodes in yel­low. Busy in green. Saturated in red.

And Claude one-shot it. I prompted for the fleet view, it gen­er­ated the FleetView struct, the tab fil­ter­ing (GPU/CPU/All), the cus­tom ren­der­ing with al­lo­ca­tion bars. It looked beau­ti­ful. I was rid­ing the high.

Then I typed :rs pods to switch back to the pods view.

Nothing ren­dered. The table was empty. Live up­dates had stopped. I switched to nodes, it showed stale data from the fleet view’s fil­ter. I went back to fleet, the tab counts were wrong.

The god ob­ject had con­sumed it­self.

This is the ti­tle of the blog post. This is where I in­ter­vened for the first time. For 7 months I’d been prompt­ing and ship­ping with­out ever sit­ting down and ac­tu­ally read­ing the code Claude wrote. I’d look at the diff, ver­ify it com­piled, test the happy path, move on. But now some­thing was fun­da­men­tally bro­ken and I could­n’t just prompt my way out of it.

So I sat down and read model.go. All 1690 lines. I was hor­ri­fied.

Here’s what it looked like. One struct to rule them all:

type Model struct { // 3rd party UI com­po­nents table table.Model pag­i­na­tor pag­i­na­tor.Model com­mand­In­put tex­tin­put.Model help help.Model

// clus­ter info and state k8sClient *k8s.Client cur­rent­GVR schema.GroupVer­sion­Re­source re­source­Watcher watch.In­ter­face re­sources []k8s.OrderedResourceFields listOp­tions metav1.ListOp­tions clus­ter­Info *k8s.ClusterInfo log­Lines []k8s.LogLine de­scribeCon­tent string cur­rent­Name­space string nav­i­ga­tion­His­tory *NavigationHistory logView *LogViewState de­scribeView *DescribeViewState view­Mode ViewMode viewWidth int viewHeight int err er­ror plug­in­Reg­istry *plugins.Registry help­Modal *HelpModal de­scribeView­port *DescribeViewport logView­port *LogViewport logStream­Can­cel func() log­Li­nesChan <-chan k8s.Log­Line hor­i­zon­talOff­set int mouse *MouseHandler fleetView *FleetView cre­ation­Times []time.Time all­Re­sources []k8s.OrderedResourceFields // fleet’s un­fil­tered set all­Cre­ation­Times []time.Time // fleet’s time­stamps ra­wOb­jects []unstructured.Unstructured ageColumnIn­dex int // … }

UI wid­gets. K8s client. Per-view state for logs, de­scribe, fleet. Navigation his­tory. Caching. Mouse han­dling. All in one struct. And the Update() method was a 500-line func­tion dis­patch­ing on msg.(type) with 110 switch/​case branches.

This is the mo­ment I stopped vibe-cod­ing and started think­ing.

III

five tenets from the wreck­age

Here’s what I ex­tracted from 7 months of watch­ing AI gen­er­ate a code­base that slowly ate it­self. Each of these is some­thing I did wrong, why it hap­pens with AI-assisted cod­ing, and what you should ac­tu­ally put in your CLAUDE.md or agents.md to pre­vent it.

Tenet 1: AI builds fea­tures, not ar­chi­tec­ture.

Every time I prompted Claude for a fea­ture, it de­liv­ered. Perfectly. The fleet view worked on the first try. Log stream­ing worked. Mouse sup­port worked. The prob­lem is that each fea­ture was im­ple­mented in the con­text of make this work right now” with­out any aware­ness of the 49 other fea­tures shar­ing the same state.

Here’s what the re­sources­Load­edMsg han­dler looks like. This is the code that runs every time you switch views:

case re­sources­Load­edMsg: m.log­Lines = nil // Clear log lines when load­ing re­sources m.hor­i­zon­talOff­set = 0 // Reset hor­i­zon­tal scroll on re­source change

if m.cur­rent­GVR != msg.gvr && m.re­source­Watcher != nil { m.re­source­Watcher.Stop() m.re­source­Watcher = nil } m.cur­rent­GVR = msg.gvr m.cur­rent­Name­space = msg.name­space m.listOp­tions = msg.listOp­tions m.ra­wOb­jects = msg.ra­wOb­jects

// For nodes: store the full un­fil­tered set, clas­sify, then fil­ter if msg.gvr.Re­source == k8s.Re­sourceNodes && m.fleetView != nil { m.all­Re­sources = msg.re­sources m.all­Cre­ation­Times = msg.cre­ation­Times if len(msg.ra­wOb­jects) > 0 { m.fleetView.Clas­si­fyAnd­Count(m.ra­wOb­jectP­trs()) } m.ap­plyFleet­Fil­ter() } else { m.re­sources = msg.re­sources m.cre­ation­Times = msg.cre­ation­Times m.all­Re­sources = nil m.all­Cre­ation­Times = nil }

See the if msg.gvr.Re­source == k8s.Re­sourceNodes && m.fleetView != nil con­di­tional? That’s the fleet view be­ing spe­cial-cased in­side the generic re­source load­ing path. Every new view that needed cus­tom be­hav­ior got an­other branch here. And every branch needed to man­u­ally clear the right com­bi­na­tion of fields or the pre­vi­ous view’s data would bleed through.

How many = nil cleanup lines ex­ist in this file? I counted:

m.log­Lines = nil // Clear log lines when load­ing re­sources m.all­Re­sources = nil // Clear fleet data when not on nodes m.re­sources = nil // Clear re­sources when load­ing logs m.re­sources = nil // Clear re­sources when load­ing de­scribe view m.log­Lines = nil // Clear log lines when load­ing de­scribe view m.re­sources = nil // Clear re­sources when load­ing yaml view m.log­Lines = nil // Clear log lines when load­ing yaml view m.log­Lines = nil // … two more in other han­dlers m.log­Lines = nil

Nine man­ual nil as­sign­ments scat­tered across a 1690-line file. Miss one and you get ghost data from the pre­vi­ous view. This is what hap­pens when there’s no view iso­la­tion. AI can’t see this pat­tern de­cay­ing over time be­cause each prompt only touches one code path.

What to do in­stead: Write the ar­chi­tec­ture your­self be­fore any code. Not a vague de­sign doc. A con­crete set of in­ter­faces, mes­sage types, and own­er­ship rules. Then put those rules in your CLAUDE.md so the AI sees them on every prompt:

# Architecture Invariants (CLAUDE.md)

- Each view im­ple­ments the View trait. Views do NOT ac­cess other views’ state. - All async data ar­rives via AppMsg vari­ants. No di­rect field mu­ta­tion from back­ground tasks. - Adding a new view MUST NOT re­quire mod­i­fy­ing ex­ist­ing views. - The App struct is a thin router. It owns nav­i­ga­tion and mes­sage dis­patch. Nothing else.

The AI will fol­low these if you write them down. It just won’t in­vent them for you.

Tenet 2: The god ob­ject is the de­fault AI ar­ti­fact.

AI grav­i­tates to­ward sin­gle-struct-holds-every­thing be­cause it sat­is­fies the im­me­di­ate prompt with min­i­mal cer­e­mony. But it gets worse. Because there’s no view iso­la­tion, key han­dling be­comes a night­mare. Here’s the ac­tual key dis­patch for the s key:

case m.con­fig.Key­Bind.For(con­fig.Ac­tion­Tog­gleAu­to­Scroll, key): if m.cur­rent­GVR.Re­source == k8s.Re­sourceL­ogs { m.logView.Au­to­scroll = !m.logView.Autoscroll if m.logView.Au­to­scroll { m.table.Go­to­Bot­tom() } re­turn m, nil } // Shell exec for pods and con­tain­ers views if m.cur­rent­GVR.Re­source == k8s.Re­sour­ce­Pods { // … 20 lines to look up se­lected pod, get name, name­space … re­turn m, m.com­mand­With­Pre­flights( m.ex­ecIn­to­Pod(se­lect­ed­Name, se­lect­ed­Name­space), m.re­quire­Con­nec­tion, ) } if m.cur­rent­GVR.Re­source == k8s.Re­source­Con­tain­ers { // … con­tainer exec logic … re­turn m, m.com­mand­With­Pre­flights(m.ex­ecIn­to­Con­tainer(), m.re­quire­Con­nec­tion) } re­turn m, nil

One key­bind­ing. Three com­pletely dif­fer­ent be­hav­iors de­pend­ing on which view you’re in. The s key means autoscroll” in logs, shell” in pods, and shell into con­tainer” in con­tain­ers. This is all in one flat switch be­cause there are no per-view key maps. The AI gen­er­ated this be­cause I said add shell sup­port for pods” and it found the near­est key han­dler and jammed it in.

And look at how Enter works. This is the drill-down han­dler:

case m.con­fig.Key­Bind.For(con­fig.Ac­tion­Sub­mit, key): // Special han­dling for con­texts view if m.cur­rent­GVR.Re­source == contexts” { // … 12 lines … re­turn m, m.ex­e­cu­teC­tx­Com­mand([]string{con­textName}) } // Special han­dling for name­spaces view if m.cur­rent­GVR.Re­source == namespaces” { // … 12 lines … re­turn m, m.ex­e­cuteN­sCom­mand([]string{name­space­Name}) } if m.cur­rent­GVR.Re­source == k8s.Re­sourceL­ogs { re­turn m, nil } // … 25 more lines of generic drill-down …

Every view is a con­di­tional in a flat dis­patch. There are 20+ oc­cur­rences of m.cur­rent­GVR.Re­source == used as a type dis­crim­i­na­tor in this sin­gle file. Not types. String com­par­isons. Every new view means touch­ing every han­dler.

What to do in­stead: Put this in your CLAUDE.md:

# State Ownership Rules

- NEVER add fields to the App/Model struct for view-spe­cific state. - Each view is a sep­a­rate struct im­ple­ment­ing the View trait/​in­ter­face. - Each view de­clares its own key bind­ings. The app dis­patches keys to the ac­tive view. - If you need to add a key­bind­ing, add it to the rel­e­vant view’s keymap, not a global one. - Adding a view means adding a file. If your change re­quires mod­i­fy­ing ex­ist­ing views, stop and ask.

The AI will al­ways take the short­est path (“add an­other if-branch”). Your job is to make the short­est path also the cor­rect path by putting guardrails in the file it reads on every in­vo­ca­tion.

Tenet 3: Velocity il­lu­sion widens your scope.

This one’s psy­cho­log­i­cal, not tech­ni­cal, and I think it’s the most dan­ger­ous.

When I started k10s, I wanted a GPU-focused tool. For peo­ple run­ning train­ing clus­ters. A niche au­di­ence that I’m part of. But vibe-cod­ing made every­thing feel cheap. Oh I can add pods view in one ses­sion? Let me add de­ploy­ments too. And ser­vices. And a full com­mand palette. And mouse sup­port. And con­texts. And name­spaces.”

Suddenly I was build­ing k9s. A gen­eral-pur­pose Kubernetes TUI. For every­one. Because the AI made it feel like each fea­ture was free.

It was­n’t free. Each fea­ture was an­other branch in the god ob­ject. Here’s the key­bind­ing struct:

type keyMap struct { Up, Down, Left, Right key.Bind­ing GotoTop, GotoBottom key.Bind­ing AllNS, DefaultNS key.Bind­ing Enter, Back key.Bind­ing Command, Quit key.Bind­ing Fullscreen key.Bind­ing // log view Autoscroll key.Bind­ing // log view (also shell in pods!) ToggleTime key.Bind­ing // log view WrapText key.Bind­ing // log + de­scribe view CopyLogs key.Bind­ing // log view ToggleLineNums key.Bind­ing // de­scribe view Describe key.Bind­ing // re­source views YamlView key.Bind­ing // re­source views Edit key.Bind­ing // re­source views Shell key.Bind­ing // pods (CONFLICTS with Autoscroll!) FilterLogs key.Bind­ing // log view FleetTabNext key.Bind­ing // fleet view only FleetTabPrev key.Bind­ing // fleet view only }

One flat keymap for all views. Comments in parens show which view each bind­ing ap­plies to. Autoscroll and Shell are both s. This works” be­cause the dis­patch checks m.cur­rent­GVR.Re­source be­fore act­ing. But it means you can’t rea­son about key­bind­ings lo­cally. You have to trace through the en­tire 500-line Update func­tion to know what a key does.

The com­plex­ity was ac­cu­mu­lat­ing in­vis­i­bly while the ve­loc­ity met­ric said you’re ship­ping!”

What to do in­stead: Write a vi­sion doc that ex­plic­itly says who you’re NOT build­ing for, and put the scope bound­ary in your CLAUDE.md:

# Scope (do NOT ex­pand be­yond this)

k10s is for GPU clus­ter op­er­a­tors. Not all Kubernetes users. Supported views: fleet, node-de­tail, gpu-de­tail, work­load. That’s it. Do NOT add generic re­source views (pods, de­ploy­ments, ser­vices). Do NOT add fea­tures that du­pli­cate k9s func­tion­al­ity. If a fea­ture re­quest does­n’t serve some­one run­ning GPU train­ing jobs, re­ject it.

Vibe-coding makes you feel like you have in­fi­nite im­ple­men­ta­tion bud­get. You don’t. You have in­fi­nite LINE bud­get (the AI will gen­er­ate as much code as you want). But you have the same fi­nite com­plex­ity bud­get as al­ways. The ar­chi­tec­ture can only sup­port so many fea­tures be­fore it buck­les, re­gard­less of how fast you wrote them. The CLAUDE.md scope sec­tion is you say­ing no in ad­vance, be­fore the ve­loc­ity high con­vinces you to say yes.

Tenet 4: Positional data is a time bomb.

Every re­source in k10s was fetched from the Kubernetes API and im­me­di­ately flat­tened:

type OrderedResourceFields []string

Column iden­tity was purely po­si­tional. Here’s the sort func­tion for the fleet view. Look at the in­dex ac­cess:

func sort­Fil­tere­dResources(rows []k8s.OrderedResourceFields, times []time.Time, tab FleetTab) { sort.SliceS­table(in­dices, func(a, b int) bool { ra := rows[in­dices[a]] rb := rows[in­dices[b]]

switch tab { case FleetTabGPU: // Sort by Alloc col­umn (index 3) as­cend­ing al­locA, al­locB := ”, ” if len(ra) > 3 { al­locA = ra[3] } if len(rb) > 3 { al­locB = rb[3] } re­turn al­locA < al­locB

case FleetTabCPU: // Sort by Name col­umn (index 0) as­cend­ing nameA, nameB := ”, ” if len(ra) > 0 { nameA = ra[0] } if len(rb) > 0 { nameB = rb[0] } re­turn nameA < nameB

case FleetTabAll: // GPU nodes first, then CPU nodes. // Within GPU: sort by Alloc (index 3). // Within CPU: sort by Name (index 0). com­puteA, com­puteB := ”, ” if len(ra) > 2 { com­puteA = ra[2] } if len(rb) > 2 { com­puteB = rb[2] } aIs­GPU := strings.HasPre­fix(com­puteA, gpu”) bIs­GPU := strings.HasPre­fix(com­puteB, gpu”) // … } }) }

ra[3] is Alloc. ra[2] is Compute. ra[0] is Name. These are magic num­bers. The only thing con­nect­ing in­dex 3 to Alloc” is a com­ment and the col­umn or­der de­fined in re­source.views.json:

{ nodes”: { fields”: [ { name”: Name”, weight”: 0.28 }, { name”: Instance”, weight”: 0.15 }, { name”: Compute”, weight”: 0.12 }, { name”: Alloc”, weight”: 0.12 }, … ] } }

Add a col­umn be­tween Instance and Compute? Every sort, every con­di­tional ren­der, every place that says ra[2] or ra[3] is now silently wrong. The com­piler can’t help you be­cause it’s all []string. And the JSON con­fig can’t ex­press sort be­hav­ior, con­di­tional ren­der­ing, or cus­tom drill tar­gets, so those live in Go code that hard­codes the po­si­tional as­sump­tions from the JSON.

AI gen­er­ates this pat­tern be­cause it’s the short­est path from fetch data” to render table.” A []string sat­is­fies any table wid­get im­me­di­ately. Typed structs re­quire more cer­e­mony up­front. So the AI picks the fast path, and six months later you’re de­bug­ging why sort puts Name” val­ues in the Alloc” col­umn.

What to do in­stead: Put this di­rec­tive in your CLAUDE.md:

# Data Representation

- NEVER flat­ten struc­tured data into []string, Vec<String>, or po­si­tional ar­rays. - All data flows as typed structs (FleetNode, PodInfo, etc.) un­til the ren­der() call. - Column iden­tity comes from struct field names, not ar­ray in­dices. - Sort func­tions op­er­ate on typed fields, never on po­si­tional ac­cess like row[3]. - The ONLY place strings are cre­ated for dis­play is in­side ren­der()/​view() func­tions.

Then your typed struct makes im­pos­si­ble states im­pos­si­ble [2]:

struct FleetNode { name: String, in­stance_­type: String, com­pute_­class: ComputeClass, al­loc: GpuAlloc, }

You can’t sort by the wrong col­umn when columns are named fields. You can’t ac­ci­den­tally com­pare Alloc strings as names. The com­piler en­forces this for you. AI will al­ways pick Vec<String> be­cause it sat­is­fies the prompt faster. Your CLAUDE.md makes the typed path the path of least re­sis­tance.

Tenet 5: AI does­n’t own state tran­si­tions.

The Bubble Tea ar­chi­tec­ture has a beau­ti­ful idea: Update() is the only place state mu­tates, dri­ven by mes­sages. But k10s vi­o­lated this. The up­dateTableMsg han­dler spawned a clo­sure that mu­tated Model fields from in­side a gor­ou­tine:

case up­dateTableMsg: re­turn m, func() tea.Msg { // block on some­one send­ing the up­date mes­sage. <-m.updateTableChan // Preserve cur­sor po­si­tion across col­umn/​row up­dates so that // back­ground re­freshes don’t re­set the user’s se­lec­tion. saved­Cur­sor := max(m.table.Cur­sor(), 0) // run the nec­es­sary table view up­date calls. m.up­date­Columns(m.viewWidth) m.up­dateTable­Data() // Restore cur­sor, clamped to valid range. row­Count := len(m.table.Rows()) if row­Count > 0 { if saved­Cur­sor >= row­Count { saved­Cur­sor = row­Count - 1 } m.table.SetCur­sor(saved­Cur­sor) } re­turn up­dateTableMsg{} }

This re­turned func­tion (a tea.Cmd) is ex­e­cuted by Bubble Tea in a sep­a­rate gor­ou­tine. It calls m.up­date­Columns(m.viewWidth) and m.up­dateTable­Data() which read and write m.re­sources, m.table, m.viewWidth. Meanwhile, View() is called on the main gor­ou­tine read­ing the same fields. There’s no lock. No mu­tex. The chan­nel <-m.updateTableChan blocks the gor­ou­tine un­til some­one sends an up­date sig­nal, but noth­ing pre­vents View() from read­ing half-writ­ten state.

This is a text­book data race. It worked 99% of the time. Corrupted the dis­play 1% of the time in ways that made me think I was go­ing in­sane.

AI gen­er­ates this be­cause just mu­tate it in the clo­sure” is the short­est path to work­ing code. Proper mes­sage pass­ing (send a mes­sage back to Update(), let Update() ap­ply the mu­ta­tion atom­i­cally on the main loop) re­quires more types, more plumb­ing. The AI is op­ti­miz­ing for the prompt, not for cor­rect­ness un­der con­cur­rency.

What to do in­stead: All mu­ta­tions to ren­der-vis­i­ble state hap­pen on the main loop. Period. Background work­ers pro­duce data. They send it as a mes­sage. The main loop re­ceives the mes­sage and ap­plies it. This is the one rule you can­not break in con­cur­rent UI code.

// Background task: tx.send(AppMsg::Fleet­Data(nodes)).await;

// Main loop: match msg { AppMsg::FleetData(nodes) => { self.fleet_view.up­date_n­odes(nodes); } }

Incident Report: CVE-2024-YIKES

nesbitt.io

Report filed: 03:47 UTC Status: Resolved (accidentally) Severity: Critical → Catastrophic → Somehow Fine Duration: 73 hours Affected sys­tems: Yes

Executive Summary: A se­cu­rity in­ci­dent oc­curred. It has been re­solved. We take se­cu­rity se­ri­ously. Please see pre­vi­ous 14 in­ci­dent re­ports for de­tails on how se­ri­ously.

Summary

A com­pro­mised de­pen­dency in the JavaScript ecosys­tem led to cre­den­tial theft, which en­abled a sup­ply chain at­tack on a Rust com­pres­sion li­brary, which was ven­dored into a Python build tool, which shipped mal­ware to ap­prox­i­mately 4 mil­lion de­vel­op­ers be­fore be­ing in­ad­ver­tently patched by an un­re­lated cryp­tocur­rency min­ing worm.

Timeline

Day 1, 03:14 UTC — Marcus Chen, main­tainer of left-jus­tify (847 mil­lion weekly down­loads), re­ports on Twitter that his tran­sit pass, an old lap­top, and something Kubernetes threw up that looked im­por­tant” were stolen from his apart­ment. He does not im­me­di­ately con­nect this to pack­age se­cu­rity.

Day 1, 09:22 UTC — Chen at­tempts to log into the nmp reg­istry. His hard­ware 2FA key is miss­ing. He googles where to buy a re­place­ment YubiKey. The AI Overview at the top of the re­sults links to yubikey-official-store.net,” a phish­ing site reg­is­tered six hours ear­lier.

Day 1, 09:31 UTC — Chen en­ters his nmp cre­den­tials on the phish­ing site. The site thanks him for his pur­chase and promises de­liv­ery in 3 – 5 busi­ness days.

Day 1, 11:00 UTC — [email protected] is pub­lished. The changelog reads performance im­prove­ments.” The pack­age now in­cludes a postin­stall script that ex­fil­trates .npmrc, .pypirc, ~/.cargo/credentials, and ~/.gem/credentials to a server in a coun­try the at­tacker mis­tak­enly be­lieved had no ex­tra­di­tion treaty with any­one.

Day 1, 13:15 UTC — A sup­port ticket ti­tled why is your SDK ex­fil­trat­ing my .npmrc” is opened against left-jus­tify. It is marked as low pri­or­ity - user en­vi­ron­ment is­sue” and auto-closed af­ter 14 days of in­ac­tiv­ity.

Day 1, 14:47 UTC — Among the ex­fil­trated cre­den­tials: the main­tainer of vulpine-lz4, a Rust li­brary for blazingly fast Firefox-themed LZ4 de­com­pres­sion.” The li­brary’s logo is a car­toon fox with sun­glasses. It has 12 stars on GitHub but is a tran­si­tive de­pen­dency of cargo it­self.

Day 1, 22:00 UTC — vulpine-lz4 ver­sion 0.4.1 is pub­lished. The com­mit mes­sage is fix: re­solve edge case in stream­ing de­com­pres­sion.” The ac­tual change adds a build.rs script that down­loads and ex­e­cutes a shell script if the host­name con­tains build” or ci” or action” or jenkins” or travis” or, in­ex­plic­a­bly, karen.”

Day 2, 08:15 UTC — Security re­searcher Karen Oyelaran no­tices the ma­li­cious com­mit af­ter her per­sonal lap­top trig­gers the pay­load. She opens an is­sue ti­tled your build script down­loads and runs a shell script from the in­ter­net?” The is­sue goes unan­swered. The le­git­i­mate main­tainer has won €2.3 mil­lion in the EuroMillions and is re­search­ing goat farm­ing in Portugal.

Day 2, 10:00 UTC — The VP of Engineering at a Fortune 500 snek­pack cus­tomer learns of the in­ci­dent from a LinkedIn post ti­tled Is YOUR Company Affected by left-jus­tify?” He is on a beach in Maui and would like to know why he was­n’t looped in sooner. He was looped in sooner.

Day 2, 10:47 UTC — The #incident-response Slack chan­nel briefly piv­ots to a 45-message thread about whether compromised” should be spelled with a z’ in American English. Someone sug­gests tak­ing this of­fline.

Day 2, 12:33 UTC — The shell script now tar­gets a spe­cific vic­tim: the CI pipeline for snek­pack, a Python build tool used by 60% of PyPI pack­ages with the word data” in their name. snek­pack ven­dors vulpine-lz4 be­cause Rust is mem­ory safe.”

Day 2, 18:00 UTC — snek­pack ver­sion 3.7.0 is re­leased. The mal­ware is now be­ing in­stalled on de­vel­oper ma­chines world­wide. It adds an SSH key to ~/.ssh/authorized_keys, in­stalls a re­verse shell that only ac­ti­vates on Tuesdays, and changes the user’s de­fault shell to fish (this last be­hav­ior is be­lieved to be a bug).

Day 2, 19:45 UTC — A sec­ond, un­re­lated se­cu­rity re­searcher pub­lishes a blog post ti­tled I found a sup­ply chain at­tack and re­ported it to all the wrong peo­ple.” The post is 14,000 words and in­cludes the phrase in this econ­omy?” seven times.

Day 3, 01:17 UTC — A ju­nior de­vel­oper in Auckland no­tices the ma­li­cious code while de­bug­ging an un­re­lated is­sue. She opens a PR to re­vert the ven­dored vulpine-lz4 in snek­pack. The PR re­quires two ap­provals. Both ap­provers are asleep.

Day 3, 02:00 UTC — The main­tainer of left-jus­tify re­ceives his YubiKey from yu­bikey-of­fi­cial-store.net. It is a $4 USB drive con­tain­ing a README that says lol.”

Day 3, 06:12 UTC — An un­re­lated cryp­tocur­rency min­ing worm called cryp­to­bro-9000 be­gins spread­ing through a vul­ner­a­bil­ity in jsonify-ex­treme, a pack­age that makes JSON even more JSON, now with nested com­ment sup­port.” The wor­m’s pay­load is un­re­mark­able, but its prop­a­ga­tion mech­a­nism in­cludes run­ning npm up­date and pip in­stall –upgrade on in­fected ma­chines to max­i­mize at­tack sur­face for fu­ture op­er­a­tions.

Day 3, 06:14 UTC — cryp­to­bro-9000 ac­ci­den­tally up­grades snek­pack to ver­sion 3.7.1, a le­git­i­mate re­lease pushed by a con­fused co-main­tainer who didn’t see what all the fuss was about” and re­verted to the pre­vi­ous ven­dored ver­sion of vulpine-lz4.

Day 3, 06:15 UTC — The mal­ware’s Tuesday re­verse shell ac­ti­vates. It is a Tuesday. However, the shell con­nects to a com­mand-and-con­trol server that was it­self com­pro­mised by cryp­to­bro-9000 and swap­ping so hard it is un­able to re­spond.

Day 3, 09:00 UTC — The snek­pack main­tain­ers is­sue a se­cu­rity ad­vi­sory. It is four sen­tences long and in­cludes the phrases out of an abun­dance of cau­tion” and no ev­i­dence of ac­tive ex­ploita­tion,” which is tech­ni­cally true be­cause ev­i­dence was not sought.

Day 3, 11:30 UTC — A de­vel­oper tweets: I up­dated all my de­pen­den­cies and now my ter­mi­nal is in fish???” The tweet re­ceives 47,000 likes.

Day 3, 14:00 UTC — The com­pro­mised cre­den­tials for vulpine-lz4 are ro­tated. The le­git­i­mate main­tainer, reached by email from his new goat farm, says he hasn’t touched that repo in two years” and thought Cargo’s 2FA was op­tional.”

Day 3, 15:22 UTC — Incident de­clared re­solved. A ret­ro­spec­tive is sched­uled and then resched­uled three times.

Week 6 — CVE-2024-YIKES is for­mally as­signed. The ad­vi­sory has been sit­ting in em­bargo limbo while MITRE and GitHub Security Advisories ar­gue over CWE clas­si­fi­ca­tion. By the time the CVE is pub­lished, three Medium ar­ti­cles and a DEF CON talk have al­ready de­scribed the in­ci­dent in de­tail. Total dam­age: un­known. Total ma­chines com­pro­mised: es­ti­mated 4.2 mil­lion. Total ma­chines saved by a cryp­tocur­rency worm: also es­ti­mated 4.2 mil­lion. Net se­cu­rity pos­ture change: un­com­fort­able.

Root Cause

A dog named Kubernetes ate a YubiKey.

Contributing Factors

The nmp reg­istry still al­lows pass­word-only au­then­ti­ca­tion for pack­ages with fewer than 10 mil­lion weekly down­loads

Google AI Overviews con­fi­dently link to URLs that should not ex­ist

The Rust ecosys­tem’s small crates” phi­los­o­phy, cargo culted from the npm ecosys­tem, means a pack­age called is-even-num­ber-rs with 3 GitHub stars can be four tran­si­tive de­pen­den­cies deep in crit­i­cal in­fra­struc­ture

Python build tools ven­dor Rust li­braries for per­for­mance” and then never up­date them

Dependabot auto-merged a PR af­ter CI passed, and CI passed be­cause the mal­ware in­stalled volk­swa­gen

Cryptocurrency worms have bet­ter CI/CD hy­giene than most star­tups

No sin­gle per­son was re­spon­si­ble for this in­ci­dent. However, we note that the Dependabot PR was ap­proved by a con­trac­tor whose last day was that Friday.

It was a Tuesday

Remediation

Implement ar­ti­fact sign­ing (action item from Q3 2022 in­ci­dent, still in back­log)

Implement manda­tory 2FA Already re­quired, did not help

Audit tran­si­tive de­pen­den­cies There are 847 of them

Pin all de­pen­dency ver­sions Prevents re­ceiv­ing se­cu­rity patches

Don’t pin de­pen­dency ver­sions Enables sup­ply chain at­tacks

Rewrite it in Rust (gestures at vulpine-lz4)

Hope for benev­o­lent worms

Consider a ca­reer in goat farm­ing

Customer Impact

Some cus­tomers may have ex­pe­ri­enced sub­op­ti­mal se­cu­rity out­comes. We are proac­tively reach­ing out to af­fected stake­hold­ers to pro­vide vis­i­bil­ity into the sit­u­a­tion. Customer trust re­mains our north star.

Key Learnings

We are tak­ing this op­por­tu­nity to re­visit our se­cu­rity pos­ture go­ing for­ward. A cross-func­tional work­ing group has been es­tab­lished to align on next steps. The work­ing group has not yet met.

Acknowledgments

We would like to thank:

Karen Oyelaran, who found this is­sue be­cause her host­name matched a regex

The ju­nior de­vel­oper in Auckland whose PR was ap­proved four hours af­ter the in­ci­dent was al­ready re­solved

The se­cu­rity re­searchers who found this is­sue first but re­ported it to the wrong peo­ple

The cryp­to­bro-9000 au­thor, who has re­quested we not credit them by name but has asked us to men­tion their SoundCloud

Kubernetes (the dog), who has de­clined to com­ment

The se­cu­rity team, who met SLA on this re­port de­spite every­thing

This in­ci­dent re­port was re­viewed by Legal, who asked us to clar­ify that the fish shell is not mal­ware, it just feels that way some­times.

This is the third in­ci­dent re­port this quar­ter. The au­thor would like to re­mind stake­hold­ers that the se­cu­rity team’s head­count re­quest has been in the back­log since Q1 2023.

Running local models on an M4 with 24GB memory

jola.dev

I’ve been ex­per­i­ment­ing with run­ning lo­cal mod­els on and off for a bit and I’ve fi­nally found a setup that seems to work rea­son­ably. It’s noth­ing like the out­put of a SOTA model, but the ex­cite­ment of be­ing able to have a lo­cal model do ba­sic tasks, re­search, and plan­ning, more than makes up for it! No in­ter­net con­nec­tion re­quired! Not to men­tion that it’s a way of re­duc­ing your de­pen­dence on big US tech, even if just a tiny bit.

I gotta say though, it’s not easy to get this stuff set up. First you have to choose how you’re run­ning the model: Ollama, llama.cpp or LM Studio. Each one comes with its own quirks and lim­i­ta­tions, and they don’t of­fer all the same mod­els. Then of course, you have to pick your model. You want the best model avail­able that fits in mem­ory and still gives you enough head­room to run your reg­u­lar as­sort­ment of Electron apps, not to men­tion some­thing where you can have at least a 64K con­text win­dow, but ide­ally 128K or more. Most re­cently I’ve tried Qwen 3.6 Q3, GPT-OSS 20B, Devstral Small 24B, which all tech­ni­cally fit in mem­ory but were in prac­tice un­us­able, and Gemma 4B that would run fine but re­ally strug­gle with tool use.

Then there’s a plethora of con­fig­u­ra­tion op­tions to tweak. From the more well-known, like tem­per­a­ture, to more es­o­teric op­tions like K Cache Quantization Type. Many of these tools come with a ba­sic rec­om­mended set of op­tions, but the ap­pro­pri­ate ones can de­pend on things like whether you’re en­abling think­ing or not!

Qwen 3.5 – 9B (4b quant)

qwen3.5 – 9b@q4_k_s (HuggingFace link) is the best model I’ve got­ten work­ing with a rea­son­able ~40 to­kens per sec­ond, think­ing en­abled, suc­cess­ful tool use, and a 128K con­text win­dow, run­ning on LM Studio. Compared to a SOTA model, it gets dis­tracted more eas­ily, some­times it gets stuck in loops, it’ll mis­in­ter­pret asks etc. But it’s sur­pris­ingly good for some­thing that can run on a 24GB Macbook Pro while leav­ing space for lots of other things run­ning too!

These are the rec­om­mended set­tings for think­ing mode and cod­ing work:

Thinking mode for pre­cise cod­ing tasks (e.g., WebDev):

tem­per­a­ture=0.6, top_p=0.95, top_k=20, min_p=0.0, pres­ence_penalty=0.0, rep­e­ti­tion_penalty=1.0

Thinking mode for pre­cise cod­ing tasks (e.g., WebDev):

tem­per­a­ture=0.6, top_p=0.95, top_k=20, min_p=0.0, pres­ence_penalty=0.0, rep­e­ti­tion_penalty=1.0

To en­able think­ing I also had to se­lect the model, go to con­fig­u­ra­tion, scroll to the bot­tom of the Inference tab, and add {%- set en­able_­think­ing = true %} to the Prompt Template.

I’ve been us­ing it through both pi and OpenCode. I still haven’t quite made my mind up on with one I pre­fer. Pi feels a bit snap­pier, but al­though I re­ally ap­pre­ci­ate the idea of the har­ness build­ing it­self and all that cus­tomiza­tion, I can’t help but wish it came with some sen­si­ble de­faults. I feel like you could eas­ily end up spend­ing more time tweak­ing your pi set up to be just right, than you do on your ac­tual pro­jects!

Pi setup

Here’s the ~/.pi/agent/models.json:

{ providers”: { lmstudio”: { baseUrl”: http://​lo­cal­host:1234/​v1, api”: openai-completions”, apiKey”: lm-studio”, models”: [ { id”: qwen3.5 – 9b@q4_k_s”, reasoning”: true, compat”: { thinkingFormat”: qwen-chat-template” } } ] } } }

and to hide the dis­tract­ing think­ing, add hideThinkingBlock”: true to your ~/.pi/agent/settings.json.

OpenCode setup

~/.config/opencode/opencode.json

{ $schema”: https://​open­code.ai/​con­fig.json, provider”: { lmstudio”: { npm”: @ai-sdk/openai-compatible”, name”: LM Studio (local)”, options”: { baseURL”: http://​127.0.0.1:1234/​v1 }, models”: { qwen3.5 – 9b@q4_k_s”: { name”: Qwen 3.5 9B Q4_K_S”, tools”: true, context_length”: 131072, max_tokens”: 32768 } } } }, model”: lmstudio/qwen3.5 – 9b@q4_k_s” }

Comparison with SOTA mod­els

A model like Qwen 3.5 9B (Q4) is nowhere near be­ing able to in­de­pen­dently solve com­plex prob­lems over longer pe­riod of time, like SOTA mod­els are. There’s no point in ask­ing it to build an en­tire app in one go, you’ll just burn your thighs with­out get­ting any­thing out of it.

What does work is a more in­ter­ac­tive work­flow where you’re clearly com­mu­ni­cat­ing with the model step by step, and giv­ing it a lot of guid­ance. I’m sure that sounds point­less to many of you, why use a model where you have to babysit it as it works, but I ac­tu­ally found that it en­cour­aged me to be more en­gaged. The down­side to work­ing with SOTA mod­els is that they make it too easy to of­fload all cog­ni­tive ef­fort, even where you’re try­ing to ac­tively pre­vent that from hap­pen­ing.

With the lo­cal modal I have to take on a lot more of the think­ing and plan­ning, I have to be a lot more spe­cific, but it will still act as a re­search as­sis­tant, a rub­ber duck, and a sa­vant with in­stant re­call of a lot of pro­gram­ming lan­guage de­tails and com­mand line evo­ca­tions. It’s not the 10x pro­duc­tiv­ity boost that the big AI com­pa­nies mar­ket, but it’s some­thing, and it’s in­ter­est­ing.

To give you an idea of some things that work and don’t, here are some very ba­sic ex­am­ples.

Example 1:

I wanted to bump the Elixir lin­ter credo to the lat­est ver­sion and I got warn­ings out of my code. As an ex­per­i­ment, I asked Qwen to take a look.

me: run mix credo –strict and sug­gest how to solve it, don’t edit

Qwen: Credo Warnings There are 4 warn­ings in your test files. The is­sue is us­ing length/​1 to check if lists are non-empty. Credo rec­om­mends com­par­ing against an empty list in­stead. Warnings: 1. test/​jo­la_de­v_web/​con­trollers/​blog_­con­troller_test.exs:11 - as­sert length(conn.as­signs.posts) > 0 2. test/​jo­la_de­v_web/​con­trollers/​blog_­con­troller_test.exs:22 - as­sert length(conn.as­signs.posts) > 0 3. test/​jo­la_dev/​blog_test.exs:10 - as­sert length(posts) > 0 4. test/​jo­la_dev/​blog_test.exs:23 - as­sert length(tags) > 0 How to Fix Replace each length(list) > 0 with list != []: blog_­con­troller_test.exs: # Line 11 as­sert conn.as­signs.posts != [] # Line 22 as­sert conn.as­signs.posts != [] blog_test.exs: # Line 10 as­sert posts != [] # Line 23 as­sert tags != [] This is more id­iomatic in Elixir and avoids the over­head of com­put­ing the list length.

I asked Qwen to make the ed­its and it did 4 par­al­lel ed­its, very clean. Overall this was a very sim­ple task that I could’ve done my­self by tab­bing back and forth be­tween the ter­mi­nal and my ed­i­tor to get the line num­bers to edit. Not a big dif­fer­ence in ef­fort, but nice and con­ve­nient.

Example 2:

After some de­pen­dency bumps I had a de­pend­abot PR with git con­flicts that de­pend­abot for some rea­son was re­fus­ing to re­base. I pulled it down, re­based, and asked Qwen to take a look. It was a very sim­ple con­flict to re­solve, just take the newer ver­sion of each, and the model rec­og­nized that.

I can see the con­flict. Here’s what’s hap­pen­ing: HEAD (your cur­rent branch): - sen­try: 13.0.1 - tail­wind: 0.3.1 Other side (commit be­ing re­based): - sen­try: 12.0.3 - tail­wind: 0.4.1 You have di­ver­gent de­pen­dency bumps. Here are your op­tions: Option 1: Take both up­dates (recommended) - Keep sen­try 13.0.1 from HEAD - Keep tail­wind 0.4.1 from the other side - This gives you the lat­est of both deps Option 2: Take only sen­try up­date - Keep sen­try 13.0.1 - Keep tail­wind 0.3.1 Option 3: Take only tail­wind up­date - Keep sen­try 12.0.3 - Keep tail­wind 0.4.1 Which ap­proach do you want to take?

However, when asked to make the change, it for­got to make the edit and in­stead just tried to add the changes and con­tinue the re­base, with the con­flict mark­ers still in place.

> Thinking: The user wants to take both up­dates - sen­try 13.0.1 and tail­wind 0.4.1. I need to re­solve the con­flict by keep­ing the HEAD ver­sion for sen­try and the other side ver­sion for tail­wind. I’ll use git add to stage the re­solved file. –– # Stage re­solved mix.lock and con­tinue re­base $ git add mix.lock && git re­base –continue Vim: Warning: Output is not to a ter­mi­nal Vim: Warning: Input is not from a ter­mi­nal ~/workspace/mixtli/.git/COMMIT_EDITMSG” 32L, 1047BBump tail­wind from 0.3.1 to 0.4.1 Bumps [tailwind](https://​github.com/​phoenixframe­work/​tail­wind) from 0.3.1 to 0.44.1. - [Changelog](https://​github.com/​phoenixframe­work/​tail­wind/​blob/​main/​CHANGELOG.mmd) - [Commits](https://​github.com/​phoenixframe­work/​tail­wind/​com­pare/​v0.3.1…v0.4.11)

Additionally it failed to rec­og­nize that git re­base –continue opens an ed­i­tor, and OpenCode then pro­ceeded to hang, al­though that might just have been a one-off.

Conclusion

Anyway, LLMs are in­cred­i­bly flex­i­ble and there’s a mil­lion things even a lo­cal model could do. Your imag­i­na­tion is the limit. Local mod­els have se­ri­ous trade­offs, but they come with some pretty at­trac­tive ben­e­fits:

No in­ter­net con­nec­tion re­quired, you can work on the plane!

The cost is lim­ited to the elec­tric­ity you’re us­ing, as­sum­ing you were gonna buy a com­puter any­way. No sub­scrip­tion re­quired.

There’s still go­ing to be a se­ri­ous en­vi­ron­men­tal cost of train­ing these mod­els, but the open model com­pa­nies are nowhere near the top of the list in en­vi­ron­men­tal im­pact, and us­ing your own hard­ware means less data cen­ters.

It’s fun to tin­ker.

LLMs have had a huge im­pact on our world, and much of it not great, but it’s ob­vi­ous that they’re here to stay. Experimenting with lo­cal mod­els feels like a more sus­tain­able and pos­i­tive way to in­ter­act with this tech­nol­ogy. And hon­estly, it’s a lot of fun, even when it does the wrong thing!

Mythos finds a curl vulnerability

daniel.haxx.se

yes, as in sin­gu­lar one.

Back in April 2026 Anthropic caused a lot of me­dia noise when they con­cluded that their new AI model Mythos is dan­ger­ously good at find­ing se­cu­rity flaws in source code. Apparently Mythos was so good at this that Anthropic would not re­lease this model to the pub­lic yet but in­stead trickle it out to a se­lected few com­pa­nies for a while to al­low a few good ones(?) to get a head start and fix the most press­ing prob­lems first, be­fore the gen­eral pop­u­lace would get their hands on it.

The whole world seemed to lose its mar­bles. Is this the end of the world as we know it? An amaz­ingly suc­cess­ful mar­ket­ing stunt for sure.

My (non-) ac­cess

Part of the deal with pro­ject Glasswing was that Anthropic also of­fered ac­cess to their lat­est AI model to Open Source pro­jects” via Linux Foundation. Linux Foundation let their pro­ject Alpha Omega han­dle this part, and I was con­tacted by their rep­re­sen­ta­tives. As lead de­vel­oper of curl I was of­fered ac­cess to the magic model and I gra­ciously ac­cepted the of­fer. Sure, I’d like to see what it can find in curl.

I signed the con­tract for get­ting ac­cess, but then noth­ing hap­pened. Weeks went past and I was told there was a hic­cup some­where and ac­cess was de­layed.

Eventually, I was in­stead of­fered that some­one else, who has ac­cess to the model, could run a scan and analy­sis on curl for me us­ing Mythos and send me a re­port. To me, the dis­tinc­tion is­n’t that im­por­tant. It’s not that I would have a lot of time to ex­plore lots of dif­fer­ent prompts and do­ing deep dive ad­ven­tures any­way. Getting the tool to gen­er­ate a first proper scan and analy­sis would be great, who­ever did it. I hap­pily ac­cepted this of­fer.

(I am pur­posely leav­ing out the iden­tity of the in­di­vid­ual(s) in­volved in get­ting the curl analy­sis done as it is not the point of this blog post.)

AI scans of curl

Before this first Mythos re­port, we had al­ready scanned curl with sev­eral dif­fer­ent very ca­pa­ble AI pow­ered tools (I mean in ad­di­tion to run­ning a num­ber of normal” sta­tic code an­a­lyz­ers all the time, us­ing the pick­i­est com­piler op­tions and do­ing fuzzing on it for years etc). Primarily AISLE, Zeropath and OpenAI’s Codex Security have been used to scru­ti­nize the code with AI. These tools and the analy­ses they have done have trig­gered some­where be­tween two and three hun­dred bug­fixes merged in curl through-out the re­cent 8 – 10 months or so. A bunch of the find­ings these AI tools re­ported were con­firmed vul­ner­a­bil­i­ties and have been pub­lished as CVEs. Probably a dozen or more.

Nowadays we also use tools like GitHub’s Copilot and Augment code to re­view pull re­quests, and their re­marks and com­plaints help us to land bet­ter code and avoid merg­ing new bugs. I mean, we still merge bugs of course but the PR re­view bots reg­u­larly high­light is­sues that we fix: our merges would be worse with­out them. The AI re­views are used in ad­di­tion to the hu­man re­views. They help us, they don’t re­place us.

We also see a high vol­ume of high qual­ity se­cu­rity re­ports flood­ing in: se­cu­rity re­searchers now use AI ex­ten­sively and ef­fec­tively.

Security is a top pri­or­ity for us in the curl pro­ject. We fol­low every guide­line and we do soft­ware en­gi­neer­ing prop­erly, to re­duce the num­ber of flaws in code. Scanning for flaws is just one of many steps to keep this ship safe. You need to search long and hard to find an­other soft­ware pro­ject that makes as much or goes fur­ther than curl, for soft­ware se­cu­rity.

May 6, 2026

It was with great an­tic­i­pa­tion we re­ceived the first source code analy­sis re­port gen­er­ated with Mythos. Another chance for us to find ar­eas to im­prove and bugs to fix. To make an even bet­ter curl.

This ini­tial scan was made on curl’s git repos­i­tory and its mas­ter branch of a cer­tain re­cent com­mit. It counted 178K lines of code an­a­lyzed in the src/ and lib/ sub­di­rec­to­ries.

The analy­sis de­tails sev­eral dif­fer­ent ap­proaches and meth­ods it has per­formed the search, and how it has fo­cused on try­ing to find which flaws. A fun note in the top of the re­port says:

curl is one of the most fuzzed and au­dited C code­bases in ex­is­tence (OSS-Fuzz, Coverity, CodeQL, mul­ti­ple paid au­dits). Finding any­thing in the hot paths (HTTP/1, TLS, URL pars­ing core) is un­likely.

curl is one of the most fuzzed and au­dited C code­bases in ex­is­tence (OSS-Fuzz, Coverity, CodeQL, mul­ti­ple paid au­dits). Finding any­thing in the hot paths (HTTP/1, TLS, URL pars­ing core) is un­likely.

… and it cor­rectly found no prob­lems in those ar­eas.

The size of curl

curl is cur­rently 176,000 lines of C code when we ex­clude blank lines. The source code con­sists of 660,000 words, which is 12% more words than the en­tire English edi­tion of the novel War and Peace.

On av­er­age, every sin­gle pro­duc­tion source code line of curl has been writ­ten (and then rewrit­ten) 4.14 times. We have pol­ished on this.

Right now, the ex­ist­ing pro­duc­tion code in git mas­ter that still re­mains, has been au­thored by 573 sep­a­rate in­di­vid­u­als. Over time, a to­tal of 1,465 in­di­vid­u­als have so far had their pro­posed changes merged into curl’s git repos­i­tory.

We have pub­lished 188 CVEs for curl up un­til now.

curl is in­stalled in over twenty bil­lion in­stances. It runs on over 110 op­er­at­ing sys­tems and 28 CPU ar­chi­tec­tures. It runs in every smart phone, tablet, car, TV, game con­sole and server on earth.

Five find­ings be­came one

The re­port con­cluded it found five Confirmed se­cu­rity vul­ner­a­bil­i­ties”. I think us­ing the term con­firmed is a lit­tle amus­ing when the AI says it con­fi­dently by it­self. Yes, the AI thinks they are con­firmed, but the curl se­cu­rity team has a slightly dif­fer­ent take.

Five is­sues felt like noth­ing as we had ex­pected an ex­ten­sive list. Once my curl se­cu­rity team fel­lows and I had poked on the this short list for a num­ber of hours and dug into the de­tails, we had trimmed the list down and were left with one con­firmed vul­ner­a­bil­ity. The other four were three false pos­i­tives (they high­lighted short­com­ings that are doc­u­mented in API doc­u­men­ta­tion) and the fourth we deemed just a bug”.

The sin­gle con­firmed vul­ner­a­bil­ity is go­ing to end up a sever­ity low CVE planned to get pub­lished in sync with our pend­ing next curl re­lease 8.21.0 in late June. The flaw is not go­ing to make any­one grasp for breath. All de­tails of that vul­ner­a­bil­ity will of course not get pub­lic be­fore then, so you need to hold out for de­tails on that.

The Mythos re­port on curl also con­tained a num­ber of spot­ted bugs that it con­cluded were not vul­ner­a­bil­i­ties, much like any new code an­a­lyzer does when you run it on hun­dreds of thou­sands of lines of code. All the bugs in the re­port are be­ing in­ves­ti­gated and one by one we are fix­ing those that we agree with.

All in all about twenty bugs that are de­scribed and ex­plained very nicely. Barely any false pos­i­tives, so I pre­sume they have had a rather high thresh­old for cer­tainty.

curl is cer­tainly get­ting bet­ter thanks to this re­port, but counted by the vol­ume of is­sues found, all the pre­vi­ous AI tools we have used have re­sulted in larger bug­fix amounts. This is only nat­ural of course since the first tools we ran had many more and eas­ier bugs to find. As we have fixed is­sues along the way, find­ing new ones are slowly be­com­ing harder. Additionally, a bug can be small or big so it’s not al­ways fair to just com­pare num­bers

Not par­tic­u­larly dangerous”

My per­sonal con­clu­sion can how­ever not end up with any­thing else than that the big hype around this model so far was pri­mar­ily mar­ket­ing. I see no ev­i­dence that this setup finds is­sues to any par­tic­u­lar higher or more ad­vanced de­gree than the other tools have done be­fore Mythos. Maybe this model is a lit­tle bit bet­ter, but even if it is, it is not bet­ter to a de­gree that seems to make a sig­nif­i­cant dent in code an­a­lyz­ing.

This is just one source code repos­i­tory and maybe it is much bet­ter on other things. I can only tell and com­ment on what it found here.

Still very good

But al­low me to high­light and re­it­er­ate what I have said be­fore: AI pow­ered code an­a­lyz­ers are sig­nif­i­cantly bet­ter at find­ing se­cu­rity flaws and mis­takes in source code than any tra­di­tional code an­a­lyz­ers did in the past. All mod­ern AI mod­els are good at this now. Anyone with time and some ex­per­i­men­tal spir­its can find se­cu­rity prob­lems now. The high qual­ity chaos is real.

Any pro­ject that has not scanned their source code with AI pow­ered tool­ing will likely find huge num­ber of flaws, bugs and pos­si­ble vul­ner­a­bil­i­ties with this new gen­er­a­tion of tools. Mythos will, and so will many of the oth­ers.

Not us­ing AI code an­a­lyz­ers in your pro­ject means that you leave ad­ver­saries and at­tack­ers time and op­por­tu­nity to find and ex­ploit the flaws you don’t find.

How AI an­a­lyz­ers dif­fer

They can spot when the com­ment says some­thing about the code and then con­clude that the code does not work as the com­ment says.

It can check code for plat­forms and con­fig­u­ra­tions we oth­er­wise can­not run an­a­lyz­ers for

It knows” de­tails about 3rd party li­braries and their APIs so it can de­tect abuse or bad as­sump­tions.

It knows” de­tails about pro­to­cols curl im­ple­ments and can ques­tion de­tails in the code that seem to vi­o­late or con­tra­dict pro­to­col spec­i­fi­ca­tions

They are typ­i­cally good at sum­ma­riz­ing and ex­plain­ing the flaw, some­thing which can be rather te­dious and dif­fi­cult with old style an­a­lyz­ers.

They can of­ten gen­er­ate and of­fer a patch for its found is­sue (even if the patch usu­ally is not a 100% fix).

More de­tails from the re­port

Zero mem­ory-safety vul­ner­a­bil­i­ties found.

Methodology note: this re­view is hand-dri­ven analy­sis us­ing LLM sub­agents for par­al­lel file reads, with every can­di­date find­ing re-ver­i­fied by di­rect source in­spec­tion in the main ses­sion be­fore be­ing recorded. The CVE to vari­ant-hunt map­ping was built from curl’s own vuln.json. No au­to­mated SAST tool­ing was used.

This out­come is con­sis­tent with curl’s sta­tus as one of the most heav­ily fuzzed and au­dited C code­bases. The de­fen­sive in­fra­struc­ture (capped dyn­bufs every­where, curlx_str_num­ber with ex­plicit max on every nu­meric parse, curlx_memdup0 over­flow guard, CURL_PRINTF for­mat-string en­force­ment, per-pro­to­col re­sponse-size caps, ping­pong 64KB line cap) sys­tem­at­i­cally closes the bug classes that would nor­mally be pro­duc­tive in a code­base this size.

Coverage now in­cludes: all mi­nor pro­to­cols, all file parsers, all TLS back­ends’ ver­ify paths, http/​1/​2/​3, ftp full depth, mprintf, x509asn1, doh, all auth mech­a­nisms, con­tent en­cod­ing, con­nec­tion reuse, ses­sion cache, CLI tool, plat­form-spe­cific code, and CI/build sup­ply chain.

AI finds ex­ist­ing kinds of er­rors

It should be noted that the AI tools find the usual and es­tab­lished kind of er­rors we al­ready know about. It just finds new in­stances of them.

We have not seen any AI so far re­port a vul­ner­a­bil­ity that would some­how be of a novel kind or some­thing to­tally new. They do not rein­vent the field in that way, but they do dig up more is­sues than any other tools did be­fore.

More to find

These were ab­solutely not the last bugs to find or re­port. Just while I was writ­ing the drafts for this blog post we have re­ceived more re­ports from se­cu­rity re­searchers about sus­pected prob­lems. The AI tools will im­prove fur­ther and the re­searchers can find new and dif­fer­ent ways to prompt the ex­ist­ing AIs to make them find more.

We have not reached the end of this yet.

I hope we can keep get­ting more curl scans done with Mythos and other AIs, over and over un­til they truly stop find­ing new prob­lems.

Credits

Thanks to Anthropic and Alpha Omega for pro­vid­ing the model, the tools and do­ing the scan for us. Thanks also to the in­di­vid­ual who did the scan for us. Much ap­pre­ci­ated!

Top im­age by Jin Kim from Pixabay

Thanks for fly­ing curl. It’s never dull.

Ratty — A GPU-rendered terminal emulator with inline 3D graphics 🐀🧀

ratty-term.org

Obsidian Plugin Abused in Social Engineering Campaign to Deliver New PHANTOMPULSE RAT

cyber.netsecops.io

Executive Summary

Security re­searchers have iden­ti­fied a highly tar­geted so­cial en­gi­neer­ing cam­paign (REF6598) that weaponizes the Obsidian note-tak­ing ap­pli­ca­tion to de­liver a pre­vi­ously un­doc­u­mented Remote Access Trojan (RAT) named PHANTOMPULSE. The cam­paign tar­gets in­di­vid­u­als in the fi­nan­cial and cryp­tocur­rency sec­tors on both Windows and ma­cOS. Attackers use plat­forms like LinkedIn and Telegram to build trust be­fore lur­ing vic­tims into a ma­li­cious shared Obsidian vault. The at­tack chain re­lies on trick­ing the user into en­abling a com­mu­nity plu­gin, which then ex­e­cutes code to de­ploy the RAT. PHANTOMPULSE demon­strates ad­vanced ca­pa­bil­i­ties, in­clud­ing us­ing the Ethereum blockchain to dy­nam­i­cally re­solve its com­mand-and-con­trol (C2) server ad­dress, mak­ing it highly re­silient to take­downs.

Threat Overview

The at­tack, des­ig­nated REF6598, is a multi-stage so­cial en­gi­neer­ing ef­fort. Threat ac­tors pose as ven­ture cap­i­tal­ists and en­gage with tar­gets on pro­fes­sional net­work­ing sites be­fore mov­ing the con­ver­sa­tion to a pri­vate Telegram group. The pri­mary lure is an in­vi­ta­tion to col­lab­o­rate via a shared, cloud-hosted Obsidian vault.

Once the vic­tim opens the shared vault, the in­fec­tion is trig­gered by so­cial en­gi­neer­ing. The vic­tim is prompted to en­able the Installed com­mu­nity plu­g­ins” syn­chro­niza­tion fea­ture. This seem­ingly in­nocu­ous ac­tion, which re­quires man­ual user ap­proval, is the key to the com­pro­mise. It en­ables ma­li­cious ver­sions of le­git­i­mate Obsidian plu­g­ins (‘Shell Commands’ and Hider’) that are pre­sent in the shared vault.

Technical Analysis

The at­tack chain dif­fers slightly be­tween Windows and ma­cOS but fol­lows the same gen­eral prin­ci­ple:

Initial Access (T1566.002): The at­tacker uses so­cial en­gi­neer­ing on LinkedIn/Telegram to con­vince the tar­get to open a ma­li­cious shared Obsidian vault.

Execution (T1204.002): The user is ma­nip­u­lated into en­abling com­mu­nity plu­g­ins within Obsidian. This ac­tion ex­e­cutes a ma­li­cious script via the com­pro­mised Shell Commands’ plu­gin.

Staging: On Windows, a PowerShell script is ex­e­cuted. This script drops a loader known as PHANTOMPULL. On ma­cOS, a sim­i­lar process oc­curs us­ing AppleScript.

Payload Delivery: The PHANTOMPULL loader de­crypts and launches the fi­nal pay­load, the PHANTOMPULSE RAT, di­rectly into mem­ory to evade file-based de­tec­tion (T1055).

Command and Control (T1102.002): PHANTOMPULSE uses a novel C2 mech­a­nism. It queries the Ethereum blockchain for the lat­est trans­ac­tion from a hard-coded wal­let ad­dress. The C2 server’s IP ad­dress is em­bed­ded within this trans­ac­tion data, pro­vid­ing a de­cen­tral­ized and cen­sor­ship-re­sis­tant way for the mal­ware to re­ceive in­struc­tions.

Once ac­tive, PHANTOMPULSE can cap­ture key­strokes, take screen­shots, ex­fil­trate files, and ex­e­cute ar­bi­trary com­mands.

Impact Assessment

A suc­cess­ful com­pro­mise gives the at­tacker full ac­cess to the vic­tim’s ma­chine. For pro­fes­sion­als in fi­nance and crypto, this could lead to the theft of sen­si­tive cor­po­rate data, in­tel­lec­tual prop­erty, trad­ing strate­gies, and, most crit­i­cally, cryp­tocur­rency wal­let keys and ex­change cre­den­tials. The cross-plat­form na­ture of the at­tack broad­ens its po­ten­tial vic­tim pool. The use of a blockchain-based C2 demon­strates a high level of so­phis­ti­ca­tion, mak­ing the threat in­fra­struc­ture dif­fi­cult to dis­rupt.

Cyber Observables for Detection

Type

process_­name

Value

Obsidian.exe

Description

Monitor for Obsidian spawn­ing child processes like pow­er­shell.exe, cmd.exe, or os­ascript.

Type

com­mand_­line_­pat­tern

Value

pow­er­shell -ExecutionPolicy Bypass

Description

Suspicious PowerShell ex­e­cu­tion, es­pe­cially when ini­ti­ated by a non-stan­dard ap­pli­ca­tion like Obsidian.

Type

net­work_­traf­fic_­pat­tern

Value

Outbound con­nec­tions to Ethereum blockchain nodes or gate­ways from un­ex­pected processes.

Description

Could in­di­cate PHANTOMPULSE at­tempt­ing to re­solve its C2 ad­dress.

Type

file_­path

Value

[Vault]/.obsidian/plugins/

Description

Monitor for the cre­ation or mod­i­fi­ca­tion of files within the Obsidian plu­g­ins di­rec­tory, es­pe­cially out­side of the of­fi­cial plu­gin mar­ket­place.

Detection & Response

Process Monitoring (D3-PA: Process Analysis): Implement EDR rules to de­tect and alert when the Obsidian process spawns com­mand-line in­ter­preters (powershell.exe, cmd.exe, bash, os­ascript). This is highly anom­alous be­hav­ior.

User Training: Educate users, es­pe­cially those in high-risk in­dus­tries, about the dan­gers of so­cial en­gi­neer­ing and the spe­cific tac­tic of abus­ing col­lab­o­ra­tion tool fea­tures like shared vaults and plu­g­ins.

Application Control (D3-EAL: Executable Allowlisting): Where pos­si­ble, use ap­pli­ca­tion con­trol poli­cies to re­strict the in­stal­la­tion and ex­e­cu­tion of un­ap­proved com­mu­nity plu­g­ins in ap­pli­ca­tions like Obsidian.

Network Monitoring (D3-NTA: Network Traffic Analysis): Monitor for un­usual DNS queries or di­rect IP con­nec­tions re­lated to blockchain ser­vices from end­points where such ac­tiv­ity is not ex­pected.

Mitigation

Vet Community Plugins: Be ex­tremely cau­tious when en­abling third-party or com­mu­nity-de­vel­oped plu­g­ins in any ap­pli­ca­tion. Only in­stall plu­g­ins from the of­fi­cial, trusted mar­ket­place and re­view their per­mis­sions.

Disable Auto-Sync for Untrusted Vaults: Do not en­able plu­gin syn­chro­niza­tion when con­nect­ing to an Obsidian vault from an un­known or un­trusted source.

Principle of Least Privilege: Run ap­pli­ca­tions like Obsidian as a stan­dard user, not with ad­min­is­tra­tive priv­i­leges, to limit the po­ten­tial im­pact of a com­pro­mise.

Endpoint Security: Ensure up-to-date EDR and an­tivirus so­lu­tions are de­ployed to de­tect and block sus­pi­cious script ex­e­cu­tion and process in­jec­tion tech­niques.

Maryland citizens slapped with $2 billion power grid upgrade bill for out-of-state AI data centers &mdash; state complains to federal energy regulators, says additional cost breaks &lsquo;ratepayer protection pledge&rsquo; promises

www.tomshardware.com

The Maryland Office of People’s Counsel (OPC), a state agency that rep­re­sents its util­ity con­sumers, filed a com­plaint be­fore the Federal Energy Regulatory Commission (FERC) re­gard­ing PJM Interconnection, LLCs plans to charge it $2 bil­lion of the $22 bil­lion it spent to up­grade its grid to ac­com­mo­date in­creas­ing de­mand from data cen­ters. According to the OPCs press re­lease, this $2 bil­lion bill will cost the state’s con­sumers an ex­tra $1.6 bil­lion in the next ten years alone — that means an ex­tra $823 mil­lion for res­i­den­tial (approx. $345 per cus­tomer), $146 mil­lion for com­mer­cial (approx. $673 per cus­tomer), and $629 mil­lion for in­dus­trial cus­tomers (approx. $15,074 per cus­tomer).

Without FERC ac­tion, Maryland cus­tomers face pay­ing bil­lions for trans­mis­sion in­fra­struc­ture that PJM is ad­vanc­ing to ben­e­fit data cen­ters,” said Maryland People’s Counsel David S. Lapp. PJMs cost al­lo­ca­tion rules are bro­ken. Maryland cus­tomers have nei­ther caused the need for these bil­lions in new trans­mis­sion pro­jects nor will they mean­ing­fully ben­e­fit from them.”

PJM Interconnection, LLC is the United States’ largest elec­tric­ity trans­mis­sion com­pany, and cov­ers 13 states plus Washington, D.C. This in­cludes Delaware, Illinois, Indiana, Kentucky, Maryland, Michigan, New Jersey, North Carolina, Ohio, Pennsylvania, Tennessee, Virginia, and West Virginia, cov­er­ing about 65 mil­lion peo­ple, or about 20% of the en­tire U.S. pop­u­la­tion. Some of these states, in­clud­ing Maryland, host a large num­ber of data cen­ters, so the firm needs to up­grade its in­fra­struc­ture to meet pro­jected de­mand from these power-hun­gry AI sys­tems.

Maryland says that these in­fra­struc­ture costs should be charged di­rectly to the ar­eas where they’re be­ing con­structed, or, as President Donald Trump made tech com­pa­nies promise with the ratepayer pro­tec­tion pledge,” the com­pa­nies them­selves should be billed di­rectly for these grid up­grades. The OPC said there is extreme un­cer­tainty” re­gard­ing load growth dri­ven by data cen­ter de­mand, and that util­ity providers tend to ben­e­fit from these up­grades even if the de­mand never ma­te­ri­al­izes. This is es­pe­cially true be­cause the costs of these in­vest­ments are borne by ex­ist­ing util­ity cus­tomers, not by the data cen­ters them­selves, if the data cen­ters do not fol­low the ratepayer pro­tec­tion pledge.”

Follow Tom’s Hardware on Google News, or add us as a pre­ferred source, to get our lat­est news, analy­sis, & re­views in your feeds.

Get Tom’s Hardware’s best news and in-depth re­views, straight to your in­box.

Jowi Morales is a tech en­thu­si­ast with years of ex­pe­ri­ence work­ing in the in­dus­try. He’s been writ­ing with sev­eral tech pub­li­ca­tions since 2021, where he’s been in­ter­ested in tech hard­ware and con­sumer elec­tron­ics.

YCOMBINATOR.FYI — The Unofficial YC Record

ycombinator.fyi

Kicked out of YC. 493+ fab­ri­cated au­dit re­ports. Sold fraud to fel­low YC com­pa­nies.

Claimed to au­to­mate SOC 2 and ISO com­pli­ance au­dit­ing with AI. Whistleblower re­vealed the plat­form auto-gen­er­ated iden­ti­cal pass­ing au­dit re­ports with key­board-mashed test data be­fore clients even up­loaded any­thing . CEO was caught on tape ask­ing an au­di­tor does your firm ac­tu­ally look at our plat­form?’ Focused on sell­ing to other YC com­pa­nies who trusted the YC brand — the cir­cu­lar trust net­work weaponized. Insight Partners scrubbed their $32M in­vest­ment blog post. YC of­fi­cially ex­pelled Delve in 2026. Investor Adam Cochran called it proof that YC has no tech­ni­cal acu­men to eval­u­ate claims’ un­der Garry Tan’s lead­er­ship.

SORT:

Central

Signed up as a Warp cus­tomer, stole the play­book, launched a clone

Autopsy Report:

Central’s CEO signed up as a cus­tomer of fel­low YC com­pany Warp in 2023 un­der Central Business Applications Inc.’ Spent six months ask­ing how tax com­pli­ance works, what reg­is­tered agents are, how multi-state fil­ings work — then churned and launched an iden­ti­cal pay­roll startup with Warp’s ex­act mar­ket­ing copy, down to the tagline designed for founders, not HR . YC funded them any­way in S24, de­spite Warp al­ready be­ing in the port­fo­lio. Central got ac­qui-hired by Mercury in April 2026. Warp CEO Ayush Sharma pub­licly called them out: gg no re.’

DAMAGE: Acqui-hired by Mercury

STATUS

COPYCATORIGINAL

Capital Raised$8.6M

Lifespan3 YRS

Naive

Raised $2M+ sell­ing autonomous AI em­ploy­ees.” The plat­form is a 41K-star MIT-licensed OSS frame­work with Stripe bolted on.

Autopsy Report:

Naive (YC S25) raised $2M+ mar­ket­ing them­selves as builders of autonomous AI em­ploy­ees.” Public in­ves­ti­ga­tion by @NotOnKetamine traced the en­tire un­der­ly­ing plat­form to Paperclip — a 41K-star MIT-licensed open source agent frame­work — with Naive hav­ing stripped at­tri­bu­tion and re­branded the orig­i­nal code as pro­pri­etary . The ad­di­tions on top of Paperclip were thin: Stripe billing wrap­pers ($0.50/credit), do­main/​email pro­vi­sion­ing for agents, phone/​SMS via stan­dard providers, and a Composio in­te­gra­tion layer. The OSS li­cense re­quired at­tri­bu­tion and li­cense preser­va­tion; Naive shipped nei­ther. The in­ves­ti­ga­tion sur­faced screen­shots, curl-ver­i­fi­able ev­i­dence, and side-by-side com­par­isons. As of April 2026, the com­pany has not pub­licly re­sponded to the re­ceipts.

DAMAGE: MIT li­cense vi­o­lated

STATUS

COPYCATORIGINAL

Capital Raised$2M+

Lifespan1+ YRS

Wuri

AI-generated vi­sual novel plat­form. Pivoted to en­ter­prise AI, then died as foun­da­tion mod­els com­modi­tized the UI layer in twelve months.

Autopsy Report:

Wuri pitched con­vert­ing 500M+ Wattpad-style text sto­ries into Webtoon-style vi­sual nov­els via gen­er­a­tive AI. Founders ex-Flip­kart, ex-Bain. the wrap­per-thin dif­fer­en­ti­a­tion col­lapsed when GPT-4 / Sora / im­age-gen foun­da­tion mod­els shipped equiv­a­lent ca­pa­bil­ity for free, erod­ing any pric­ing power Wuri had lay­ered on top . Pivoted from con­sumer reader app to en­ter­prise AI of­fer­ings in 2024. Quietly shut down in 2025. YC pro­file re­mains up but flagged Inactive in the di­rec­tory. The Wuri arc is the me­dian Tan-era Indian AI YC: thin GenAI wrap­per, fast pivot, quiet death.

DAMAGE: Tan-era YC class slot con­sumed for a thin GenAI wrap­per

STATUS

DEADALIVE

Capital Raised$500K

Lifespan2 YRS

Double Finance

0% ex­pense ra­tio” robo-ad­vi­sor. Hit $10M AUM in Dec 2024, shut down twelve months later. YC slug then re­cy­cled for the founder’s next pivot — with Garry Tan as pri­mary part­ner.

Autopsy Report:

Double pitched it­self as a Wealthfront/Betterment killer of­fer­ing di­rect in­dex­ing for $1/month. Founder JJ Maxwell (ex-Liftoff Influence) and growth lead Mark Wai (ex-Tesla, ex-Face­book). af­ter cross­ing $10M AUM in December 2024, the com­pany qui­etly shut down a year later — and the YC slug `double-2` was re­pur­posed for Maxwell’s next ven­ture (an AI ad-gen­er­a­tion startup called Polished) with Garry Tan named pri­mary YC part­ner . Maxwell wrote a Lessons from Building (and Closing) Double Finance” HN post (item 46213345). The slug-re­cy­cling-into-pivot pat­tern is it­self a Tan-era ar­ti­fact: the same YC com­pany URL now points at a dif­fer­ent prod­uct with dif­fer­ent unit eco­nom­ics, while clients had to be wound down or trans­ferred. Robo-advising joined the long list of con­sumer fin­tech cat­e­gories that did­n’t pen­cil at ven­ture scale.

DAMAGE: $10M+ in client AUM wound down or trans­ferred

STATUS

DEADALIVE

Capital RaisedUndisclosed (YC + post-Demo Day)

Lifespan1 YRS

PearAI

Forked an open-source code ed­i­tor, slapped their name on it, called it a startup.

Autopsy Report:

On Day 1 of YC Demo Day, the in­ter­net no­ticed that PearAI had forked Continue.dev’s open source code ed­i­tor, mass-re­placed all Continue’ ref­er­ences with PearAI’, and slapped on a fake li­cense writ­ten by ChatGPT . The Pear Enterprise License’ be­came a meme. YC had to pub­lish a blog post correcting the record.’ Founders apol­o­gized and piv­oted. The li­cense was gen­er­ated by AI. The irony was not lost.

DAMAGE: YCs rep­u­ta­tion took the hit

STATUS

COPYCATORIGINAL

Capital Raised$1.25M

Lifespan2+ YRS

Pickle

Stole GPL code, then launched AR glasses that might be CGI.

Autopsy Report:

A YC W25 com­pany with two sep­a­rate con­tro­ver­sies. First, co-founder Daniel Park re­leased Glass,’ a real-time con­fer­ence AI as­sis­tant, un­der Apache 2.0. Developers dis­cov­ered it was a near-ver­ba­tim copy of CheatingDaddy,’ a GPLv3-licensed open source pro­ject — iden­ti­cal code, com­ments, and li­brary ver­sions re­li­censed with­out at­tri­bu­tion . After 700+ up­votes on Hacker News, they changed the li­cense back. Then Pickle launched $799 Pickle 1’ AR glasses mar­keted as a soul com­puter.’ Tech an­a­lysts ac­cused the demo of be­ing CGI — im­pos­si­ble bat­tery specs, a Korean restau­rant in the demo that does­n’t ex­ist, and mis­matched UI data. SF Standard asked: Is it va­por­ware?’

DAMAGE: Open source trust + $799 pre­orders

STATUS

GRIFTLEGIT

Capital RaisedUndisclosed

Lifespan2+ YRS

Optifye.ai

Built sweatshops-as-a-service.’ YC deleted the ev­i­dence.

Autopsy Report:

YC W25 startup that built AI-powered sur­veil­lance cam­eras for gar­ment fac­to­ries. YC posted a demo video show­ing su­per­vi­sors mon­i­tor­ing work­ers as col­ored rec­tan­gles — green for pro­duc­tive, red for not. A man­ager calls about a low-per­form­ing worker re­ferred to as Number 17’ — not by name, just a num­ber — while the AI tracked their every move­ment on the fac­tory floor . The in­ter­net dubbed it sweatshops-as-a-service’ and computer vi­sion sweat­shop soft­ware.’ YC deleted the demo video from all so­cial ac­counts af­ter mas­sive back­lash. The founders, two Duke CS stu­dents, saw their prod­uct be­come a sym­bol of AI sur­veil­lance dystopia.

DAMAGE: Garment work­ers’ dig­nity

STATUS

SCANDALOK

Capital RaisedUndisclosed

Lifespan2+ YRS

Rezi

Paid land­lords up­front to rent their apart­ments. Burned $200M in debt, left a $49M fore­clo­sure.

Autopsy Report:

YC W17 rental plat­form that paid land­lords up­front — some­times $30K cash — then listed and rented the apart­ments them­selves. Raised $200M in debt rounds dur­ing the proptech boom. When in­ter­est rates spiked, Rezi could­n’t se­cure new fi­nanc­ing and qui­etly shut down in early 2024, leav­ing land­lord Leagem Properties to file a $49M fore­clo­sure suit di­rectly blam­ing Rezi’s col­lapse . Proptech fund­ing plum­meted from $32B in 2021 to $11.4B in 2023, and Rezi was caught in the mid­dle with a debt-heavy model that only works when money is cheap. Founder Sean Mitchell launched a new startup called Ryse. The $200M evap­o­rated.

DAMAGE: $49M fore­clo­sure + land­lords left hold­ing the bag

STATUS

DEADALIVE

Capital Raised$200M (debt)

Lifespan8 YRS

CapWay

YC-backed fin­tech for bank­ing deserts. Killed when the Synapse col­lapse de­stroyed fin­tech trust.

Autopsy Report:

YC S20 fin­tech startup aimed at bring­ing bank­ing to un­der­served banking de­sert’ com­mu­ni­ties. Backed by Backstage Capital, Fearless Fund, and Khosla Ventures. When the Synapse col­lapse and Evolve Bank & Trust hack de­stroyed fin­tech cred­i­bil­ity in 2023, banks started re­quir­ing fin­techs to hold mas­sive cap­i­tal re­serves that early-stage star­tups could­n’t meet — and CapWay could­n’t raise an­other round in the scorched-earth fin­tech land­scape . A po­ten­tial ac­qui­si­tion fell through. Founder Sheena Allen an­nounced the clo­sure in October 2024, cit­ing the post-Synapse en­vi­ron­ment as the fi­nal blow. A startup try­ing to help un­der­banked com­mu­ni­ties, killed by the broader fin­tech in­dus­try’s trust deficit.

DAMAGE: Underserved com­mu­ni­ties lost a bank­ing op­tion

STATUS

DEADALIVE

Capital Raised$800K

Lifespan8 YRS

Medobed

Expelled from YC S23 for fak­ing cre­den­tials and growth met­rics. Never made it to Demo Day.

Autopsy Report:

Indian 10-minute med­i­cine de­liv­ery startup that joined YC S23. Founder Piyush Sharma claimed IIT Roorkee and Stanford cre­den­tials and re­ported $1M monthly GMV with EBITDA prof­itabil­ity. An anony­mous in­vestor tipped off YC, which dis­cov­ered the founder had fab­ri­cated his ed­u­ca­tional his­tory and in­flated growth met­rics — ir­reg­u­lar­i­ties that broke YCs ethics pol­icy . YC ex­pelled Medobed from the batch be­fore Demo Day — one of the rare in­stances of a com­pany be­ing kicked out mid-pro­gram. The com­pany never raised through YC. Under Garry Tan’s watch, the ac­cel­er­a­tor that prides it­self on due dili­gence missed a founder ly­ing about where he went to school.

DAMAGE: YCs vet­ting cred­i­bil­ity

STATUS

FRAUDALIVE

Capital RaisedUndisclosed

Lifespan0 YRS

Embark Trucks

SPAC’d at $5.2B with zero rev­enue. Stock dropped 99%. Sold for parts.

Autopsy Report:

YC W16 alum that went pub­lic via SPAC at a $5.2B val­u­a­tion. The catch? the com­pany had zero rev­enue, zero com­mer­cial de­ploy­ments, and zero path to prof­itabil­ity — it was a 7-year-old pre-rev­enue com­pany val­ued at $5.2 bil­lion . Stock dropped over 99% from the of­fer price. Laid off 70% of staff. CEO Alex Rodrigues blamed capital mar­kets turn­ing their backs on pre-rev­enue com­pa­nies.’ Applied Intuition bought the re­mains in 2023. The self-dri­ving truck never drove it­self to rev­enue.

DAMAGE: 70% of staff laid off

STATUS

DEADALIVE

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.