<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Prasanna Nattuthurai</title>
    <description>The latest articles on DEV Community by Prasanna Nattuthurai (@prasannavarshan).</description>
    <link>https://dev.to/prasannavarshan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4089306%2Fd5b8639c-5463-4925-b8ff-95307bb28ce8.png</url>
      <title>DEV Community: Prasanna Nattuthurai</title>
      <link>https://dev.to/prasannavarshan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/prasannavarshan"/>
    <language>en</language>
    <item>
      <title>Stop Reading Your AI Agent. Start Talking to It.</title>
      <dc:creator>Prasanna Nattuthurai</dc:creator>
      <pubDate>Thu, 12 Nov 2026 01:42:09 +0000</pubDate>
      <link>https://dev.to/prasannavarshan/stop-reading-your-ai-agent-start-talking-to-it-5495</link>
      <guid>https://dev.to/prasannavarshan/stop-reading-your-ai-agent-start-talking-to-it-5495</guid>
      <description>&lt;p&gt;&lt;em&gt;How I gave my coding agent a free, fully local voice for my personal projects, and why I think this is where agentic coding is headed.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🏡 Everything here was built and used on my own side projects, on my personal laptop.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;Here's a confession: I've stopped reading most of what my coding agents write.&lt;/p&gt;

&lt;p&gt;Not because it's bad. Because there's too much of it. An agent kicks off, reads twelve files, runs the tests, edits four modules, and then hands me a 900-word wall of text with tables, file paths and a bulleted summary of a bulleted summary. By paragraph three my eyes are skimming. By paragraph five I've wandered off to another tab.&lt;/p&gt;

&lt;p&gt;That's the quiet problem with agentic coding nobody talks about.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The agents got faster. Our attention didn't.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The bottleneck isn't the model anymore. It's me, scrolling.&lt;/p&gt;

&lt;p&gt;So I tried something different. I gave my agents a voice and ears. Now I ask a question out loud, the agent goes off and does the work, and comes back with two spoken sentences: &lt;em&gt;"Tests pass, I changed the retry logic in two files. Want the details on screen?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💻 It runs entirely on my laptop. 💸 It cost me nothing. I built it in Claude Code, but nothing about it is Claude-specific: any agent that can use MCP tools can get the same thing.&lt;/p&gt;

&lt;p&gt;Here's how, and why I think you should try it.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎙️ Why voice, why now
&lt;/h2&gt;

&lt;p&gt;Three things changed my mind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Agents produce output faster than humans can read it.&lt;/strong&gt;&lt;br&gt;
A single agentic task can generate more text than a short blog post. Reading all of it is exhausting; skimming it means you miss the one line that mattered. Speech forces the agent to &lt;em&gt;summarise&lt;/em&gt;. You can't speak a table. You can't speak a 40-line diff. So the agent has to tell you what actually matters, and put the rest on screen if you want it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Talking keeps you in the loop without keeping you glued to the screen.&lt;/strong&gt;&lt;br&gt;
When I'm in voice mode I can lean back, look away, think. The agent works between turns and reports back by voice. It feels less like supervising a log file and more like pairing with a friend who's sitting next to you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. It's finally free and local.&lt;/strong&gt;&lt;br&gt;
A year ago this meant cloud speech APIs, API keys, and paying per minute. Today, open models like &lt;strong&gt;Whisper&lt;/strong&gt; (speech-to-text) and &lt;strong&gt;Kokoro&lt;/strong&gt; (text-to-speech) run comfortably on a laptop. No account, no key, no audio leaving your machine.&lt;/p&gt;


&lt;h2&gt;
  
  
  🔌 The idea: one voice layer, any agent
&lt;/h2&gt;

&lt;p&gt;The trick is that the voice doesn't live &lt;em&gt;inside&lt;/em&gt; the agent. It lives in an &lt;strong&gt;MCP server&lt;/strong&gt;, the standard plug-in protocol that Claude Code and a growing list of other coding agents already speak.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frpe8vw3zj25pirm37zdk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frpe8vw3zj25pirm37zdk.png" alt="One voice layer, any coding agent: you speak, Whisper turns it into text, VoiceMode hands it to your agent, Kokoro speaks the reply. All local, all free." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The MCP server I use is &lt;strong&gt;VoiceMode&lt;/strong&gt; (open source, MIT licensed). It exposes one main tool, &lt;code&gt;converse&lt;/code&gt;: speak something, then listen and return what the user said. Any agent that can call MCP tools can now hold a conversation.&lt;/p&gt;

&lt;p&gt;That's the whole point: &lt;strong&gt;build the voice once, plug it into every agent you use.&lt;/strong&gt; When the next coding assistant comes along, it gets a voice for free too.&lt;/p&gt;


&lt;h2&gt;
  
  
  ⚡ Setup: about ten minutes
&lt;/h2&gt;

&lt;p&gt;I'm on an Apple Silicon Mac with 16 GB of RAM. Nothing exotic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Install VoiceMode and the local speech services&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvx voice-mode-install
voicemode service install whisper   &lt;span class="c"&gt;# speech-to-text&lt;/span&gt;
voicemode service install kokoro    &lt;span class="c"&gt;# text-to-speech&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Pin it to local only&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By default VoiceMode can fall back to a cloud API if it finds an API key. I don't want that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;voicemode config &lt;span class="nb"&gt;set&lt;/span&gt; VOICEMODE_STT_BASE_URLS http://127.0.0.1:2022/v1
voicemode config &lt;span class="nb"&gt;set&lt;/span&gt; VOICEMODE_TTS_BASE_URLS http://127.0.0.1:8880/v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now nothing you say leaves the machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Plug it into Claude Code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add --scope user voicemode -- ~/.local/bin/voicemode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Add a &lt;code&gt;/talk&lt;/code&gt; command&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A tiny custom slash command: a markdown file in &lt;code&gt;~/.claude/commands/&lt;/code&gt; whose prompt says &lt;em&gt;start a voice loop with the &lt;code&gt;converse&lt;/code&gt; tool, keep replies short, stop when I say “stop”.&lt;/em&gt; Type &lt;code&gt;/talk&lt;/code&gt;, and you’re talking.&lt;/p&gt;

&lt;p&gt;That's it.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧩 Using a different agent?
&lt;/h3&gt;

&lt;p&gt;The same recipe works in any coding agent that supports MCP. The pieces are always the same:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run the two local speech services&lt;/strong&gt; (Whisper and Kokoro). They don't care which agent calls them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Register VoiceMode as an MCP server&lt;/strong&gt; in your agent's MCP config, pointing at the &lt;code&gt;voicemode&lt;/code&gt; binary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allow its &lt;code&gt;converse&lt;/code&gt; tool&lt;/strong&gt;, if your agent asks you to approve tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a reusable prompt&lt;/strong&gt; (a slash command, custom prompt or rules file, whatever your agent calls it) with the voice-loop rules below.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Check your agent's docs for where its MCP config lives. Some read one global file; others want the server listed per agent or per project.&lt;/p&gt;




&lt;h2&gt;
  
  
  💬 Making it feel like a conversation, not a robot
&lt;/h2&gt;

&lt;p&gt;Getting audio in and out is the easy part. Making it &lt;em&gt;pleasant&lt;/em&gt; took some prompt tuning. The rules that made the biggest difference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One or two sentences, ~25 words.&lt;/strong&gt; The agent offers to say more instead of explaining everything. This alone fixes the wall-of-text problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never read code, paths or tables aloud.&lt;/strong&gt; Speak the summary; put the detail on screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A live transcript.&lt;/strong&gt; Before each turn the agent prints &lt;code&gt;🎤 You: …&lt;/code&gt; and &lt;code&gt;🔊 Friday: …&lt;/code&gt;, so the screen matches what you hear. Handy when the speech recognizer mishears you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A persona.&lt;/strong&gt; In voice, my agent answers as "Friday": calm, concise, a little dry. Put the persona in the shared prompt and every agent you wire up sounds like the same assistant. Small thing, surprisingly nice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sound human.&lt;/strong&gt; Vary the acknowledgements. Nothing kills the vibe like hearing "Great question!" eleven times.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🛑 The safety rule you must not skip
&lt;/h2&gt;

&lt;p&gt;Speech recognition mishears things, especially names, numbers and jargon. "Delete the test branch" and "delete the best branch" are one syllable apart.&lt;/p&gt;

&lt;p&gt;So my rule is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read-only questions&lt;/strong&gt; ("what failed?", "summarise the last change"): the agent just answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything that changes or sends something&lt;/strong&gt; (file writes, deletes, git commits and pushes): the agent repeats back what it understood in one sentence and &lt;strong&gt;waits for a spoken "yes"&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 &lt;strong&gt;Never let a voice command run an action unconfirmed.&lt;/strong&gt; This one rule is what makes voice safe enough to actually rely on.&lt;/p&gt;




&lt;h2&gt;
  
  
  🪙 "But doesn't voice burn more tokens?"
&lt;/h2&gt;

&lt;p&gt;I wondered the same, so I measured it from my own session logs.&lt;/p&gt;

&lt;p&gt;Across 16 voice exchanges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~350 output tokens per turn&lt;/strong&gt; (spoken reply + on-screen transcript + reasoning)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~800 uncached input tokens in total.&lt;/strong&gt; What you say comes back as a tiny bit of text.&lt;/li&gt;
&lt;li&gt;The big number, ~110k cached tokens per call, is just the conversation context being re-read, which is &lt;strong&gt;exactly the same when you type&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✅ &lt;strong&gt;Verdict:&lt;/strong&gt; a voice turn costs about the same as a typed one. Speech-to-text and text-to-speech run locally, so they're free. The only extra is the transcript line, maybe 50–100 tokens a turn. If anything, voice &lt;em&gt;saves&lt;/em&gt; tokens, because short spoken answers replace long written ones.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Caveat: that's one session, not a controlled typed-vs-spoken comparison. Take it as a strong hint, not a benchmark.)&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  ⏱️ Is it fast enough?
&lt;/h2&gt;

&lt;p&gt;Mostly, yes. The voice gap after the agent finishes thinking is about &lt;strong&gt;2 seconds&lt;/strong&gt;: ~0.3 s for Whisper to transcribe you, ~1.6 s for Kokoro to start speaking. The real variable is how long the &lt;em&gt;model&lt;/em&gt; thinks, which you'd wait for anyway.&lt;/p&gt;

&lt;p&gt;One surprise: &lt;strong&gt;Kokoro was faster on CPU than on the Mac's GPU.&lt;/strong&gt; Time to first audio was 0.4 s on CPU versus 2.2 s on GPU, and the GPU never caught up after warm-up. If your local text-to-speech feels slow, try forcing CPU.&lt;/p&gt;

&lt;p&gt;And a lesson: when benchmarking TTS, use &lt;strong&gt;new text every time&lt;/strong&gt;. Repeated text gets cached, and my first measurement looked 5× better than reality.&lt;/p&gt;




&lt;h2&gt;
  
  
  🪤 Gotchas I hit, so you don't
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bluetooth headset as the mic → beeps and robot voice.&lt;/strong&gt; macOS flips the headset into "call mode" when it starts listening. Use the laptop mic for input and keep the headset for output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;voice-mode-install --model small&lt;/code&gt; broke the Whisper install&lt;/strong&gt; on my setup. Install Whisper on its own with &lt;code&gt;voicemode service install whisper&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python version mismatch.&lt;/strong&gt; Kokoro wants Python 3.10; if your machine defaults to newer, rebuild its venv on 3.10.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Corrupt model download.&lt;/strong&gt; If Kokoro won't start, check the model file size. Mine was 50 MB short.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not every agent reads a global MCP config.&lt;/strong&gt; If the voice tools don't show up, add the server to the per-agent or per-project config.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy note:&lt;/strong&gt; VoiceMode's logs store what you said in plain text. Put them on your cleanup list.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🚫 What I didn't use, and why
&lt;/h2&gt;

&lt;p&gt;I looked around before settling on this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code's built-in &lt;code&gt;/voice&lt;/code&gt;&lt;/strong&gt;: great for dictation, but input-only and cloud-transcribed. No spoken replies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other voice MCP servers&lt;/strong&gt;: either barely maintained, macOS-only and stale, or multiple GBs of models per project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud voice tools&lt;/strong&gt;: some have nicer live transcripts, but they send your audio to the cloud and some bill separately. Not what I wanted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;VoiceMode was the only option I found that was fully local, actively maintained, and worked across every project and every agent.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 "Is AI making us dumber?"
&lt;/h2&gt;

&lt;p&gt;You've probably seen the term &lt;strong&gt;cognitive debt&lt;/strong&gt; going around, along with &lt;strong&gt;cognitive surrender&lt;/strong&gt; and &lt;strong&gt;AI brain fry&lt;/strong&gt;. The worry is real, and the research is starting to back it up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In a &lt;a href="https://www.infoq.com/news/2026/02/ai-coding-skill-formation/" rel="noopener noreferrer"&gt;randomized trial from Anthropic&lt;/a&gt;, junior developers who used an AI assistant scored &lt;strong&gt;17% lower&lt;/strong&gt; on comprehension tests than those who coded by hand. The interesting part is &lt;em&gt;how&lt;/em&gt; they used it. People who handed the whole task to the AI scored below 40%. People who asked it to &lt;em&gt;explain&lt;/em&gt; things scored above 65%, as well as or better than the no-AI group.&lt;/li&gt;
&lt;li&gt;Researchers &lt;a href="https://arxiv.org/pdf/2609.20143" rel="noopener noreferrer"&gt;Shaw and Nave&lt;/a&gt; draw a useful line between &lt;strong&gt;cognitive offloading&lt;/strong&gt; (you delegate a task but keep thinking about it) and &lt;strong&gt;cognitive surrender&lt;/strong&gt; (the AI's answer becomes your answer without much scrutiny).&lt;/li&gt;
&lt;li&gt;Boston Consulting Group researchers coined &lt;a href="https://builtin.com/articles/ai-brain-fry-software-developers" rel="noopener noreferrer"&gt;"AI brain fry"&lt;/a&gt; for the mental hangover of keeping up with tools that produce more than you can absorb.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's my take: the wall of text is a big part of the problem. When an agent dumps 900 words on you, the easiest move is to skim, nod and accept. That's surrender by exhaustion.&lt;/p&gt;

&lt;p&gt;Voice won't fix this on its own. You can say "yes" to everything just as lazily as you can click "accept". But it changes the default in a few useful ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You have to say what you want out loud.&lt;/strong&gt; Putting your intent into a sentence is thinking. It's hard to explain a task without understanding it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short answers invite follow-ups.&lt;/strong&gt; When the agent says "I changed the retry logic in two files", the natural reply is "why two?" A spoken back-and-forth pulls you into the reasoning instead of past it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Less to read means less overload.&lt;/strong&gt; You get the gist by ear and open the detail on screen only when it matters. That's the brain fry problem in reverse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The confirmation step makes you decide.&lt;/strong&gt; The agent says back what it's about to do, and you have to actively agree.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few habits I've picked up to stay on the &lt;em&gt;offloading&lt;/em&gt; side of the line:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Say your guess before you ask. ("I think it's the timeout. Check?")&lt;/li&gt;
&lt;li&gt;Ask "why" at least once per task.&lt;/li&gt;
&lt;li&gt;Ask the agent to explain, not just to do, especially in code you don't know yet.&lt;/li&gt;
&lt;li&gt;Every so often, solve something yourself first and use the agent to check it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Voice makes these habits feel natural, because they're just how you'd talk to a person.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Where this is going
&lt;/h2&gt;

&lt;p&gt;I don't think we'll read our agents' output forever. As agents take on longer, more autonomous tasks, the human job shifts from &lt;em&gt;reading everything&lt;/em&gt; to &lt;em&gt;staying oriented&lt;/em&gt;: knowing what happened, what's blocked, and what needs a decision, while still doing the thinking yourself.&lt;/p&gt;

&lt;p&gt;Voice is good at exactly that. It's short and in the moment, and you don't have to sit and stare at a screen to follow it. Detail stays on screen for when you want it; the gist comes to you.&lt;/p&gt;

&lt;p&gt;The setup is free, local, and about ten minutes of work. If you spend your evenings building side projects with a coding agent, try talking to it for a week. I don't think you'll go back to scrolling.&lt;/p&gt;




&lt;p&gt;🔭 &lt;em&gt;Next up for me: trying NVIDIA's Parakeet model for even faster speech recognition, and moving the speech services to a home server so the laptop stays cool.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>mcp</category>
      <category>claude</category>
    </item>
    <item>
      <title>My AI Trading Bot Looked Profitable. Every Number That Said So Was the Wrong Number.</title>
      <dc:creator>Prasanna Nattuthurai</dc:creator>
      <pubDate>Fri, 06 Nov 2026 02:41:32 +0000</pubDate>
      <link>https://dev.to/prasannavarshan/my-ai-trading-bot-looked-profitable-every-number-that-said-so-was-the-wrong-number-309l</link>
      <guid>https://dev.to/prasannavarshan/my-ai-trading-bot-looked-profitable-every-number-that-said-so-was-the-wrong-number-309l</guid>
      <description>&lt;h2&gt;
  
  
  Five ways a prediction-market model fooled me, and the one metric I trust now
&lt;/h2&gt;

&lt;p&gt;For five months I ran an LLM-powered forecaster against real prediction markets: Kalshi and Polymarket. It researched a market, estimated a probability, compared it with the price, and logged a "shadow" bet whenever it disagreed enough. Every resolved bet went into a SQLite database.&lt;/p&gt;

&lt;p&gt;The dashboards looked great. Directional accuracy was above 60%. Brier scores cleared the bar I'd set. One strategy beat the market on its first 45 trades. Another showed a statistically significant edge.&lt;/p&gt;

&lt;p&gt;None of it held up. That isn't because the model was secretly brilliant and unlucky. Each of those numbers was measuring something other than "will this make money." This post is about how I found that out, one metric at a time. It's the most useful engineering lesson I've learned this year, and it applies well beyond betting. If you evaluate any AI system, whether a classifier, a ranker, an agent or a forecaster, you probably have one of these five problems too.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr1ydotx18863eqpdryey.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr1ydotx18863eqpdryey.png" alt="Every metric that said yes, next to the one that mattered" width="799" height="583"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  1. Accuracy over everything vs. accuracy on what you act on
&lt;/h3&gt;

&lt;p&gt;The headline number was &lt;strong&gt;60.9% directional accuracy&lt;/strong&gt;. That was over every market the model looked at.&lt;/p&gt;

&lt;p&gt;But most of those were SKIPs: markets where the model's estimate sat close to the price, so no bet would ever be placed. On those, "pick the favourite" is easy and the model agrees with the crowd. That pads the score.&lt;/p&gt;

&lt;p&gt;The bets that matter are the ones where the model &lt;em&gt;disagrees&lt;/em&gt; with the market by 5 points or more. That's the only subset where money moves. On that subset:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;61 of 152 correct. 40.1%.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The corpus number and the actionable number pointed in &lt;strong&gt;opposite directions&lt;/strong&gt;. The model was right when it agreed with everyone and wrong 60% of the time when it had an opinion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; score the decisions your system actually makes, not everything it touches. If your agent only acts on 15% of inputs, the other 85% are noise in your eval.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Beating a fixed bar vs. beating the baseline you compete with
&lt;/h3&gt;

&lt;p&gt;My go-live gate said "Brier score under 0.25." Soccer came in at 0.2177, which passed.&lt;/p&gt;

&lt;p&gt;The market's own price scored 0.2118 on the same games.&lt;/p&gt;

&lt;p&gt;The model "passed" while losing to the thing it was betting against. A fixed threshold told me nothing. The only bar that matters for a forecaster is the &lt;strong&gt;price&lt;/strong&gt;, because the price is your opponent. When I re-scored every football category against the market, the market won every time. Over 517 markets it was 0.2376 for the model and 0.2166 for the price. I killed football forecasting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; a threshold is a guess. A baseline is a fact. Compare against the best simple thing you could do instead, whether that's the price, the heuristic, the last model or the &lt;code&gt;if/else&lt;/code&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Small-sample edge vs. the same edge at 4× the data
&lt;/h3&gt;

&lt;p&gt;Cross-venue "arbitrage" (the same event priced differently on two exchanges) looked like the cleanest win. At &lt;strong&gt;n=45&lt;/strong&gt;, its Brier score was 0.0906 against the market's 0.0928. It was a small edge, but a real one, and it was enough that I wired it up to place real orders.&lt;/p&gt;

&lt;p&gt;At &lt;strong&gt;n=170&lt;/strong&gt;, the edge was gone: 0.1951 for the model, 0.1951 for the market. On the 21 bets it actually took, it went 9 for 21.&lt;/p&gt;

&lt;p&gt;A negative or zero edge doesn't turn positive with more data. More volume only makes you more certain it's not there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; never promote a system on its first good sample. Decide the checkpoint size &lt;em&gt;before&lt;/em&gt; you look, and don't let an early number buy production access.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Gross edge vs. net edge at the price you can actually get
&lt;/h3&gt;

&lt;p&gt;This one hurt the most because the bug was a single constant.&lt;/p&gt;

&lt;p&gt;The "favourites" strategy bought heavily favoured outcomes priced 0.65–0.80. I pre-registered it: run until 100 resolved out-of-sample bets, then judge. At 135 resolved bets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hit rate &lt;strong&gt;65.19%&lt;/strong&gt; against an average price of &lt;strong&gt;68.69%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Gross edge &lt;strong&gt;-3.50pp&lt;/strong&gt; per contract&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That was already negative. Then I found my fee model was wrong. I had Polymarket US's taker fee as &lt;code&gt;0.003 × p&lt;/code&gt;. The real formula is &lt;code&gt;0.0695 × p × (1 − p)&lt;/code&gt;, about 1.5 points at these prices, or &lt;strong&gt;five times what I'd modelled&lt;/strong&gt;. Net edge was &lt;strong&gt;-4.97pp&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Even the one &lt;em&gt;statistically significant&lt;/em&gt; positive result had the same problem. Closing-line value on favourites was +1.15pp, with a 95% CI of [+0.37, +1.97] that stayed above zero. But that was measured at the &lt;em&gt;mid&lt;/em&gt;-price. Scored at the ask, the price you actually pay, after fees, it came to &lt;strong&gt;-1.93pp&lt;/strong&gt;. The spread plus the fee ate it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; measure at the point of execution, with the real costs, from the real docs. "Edge at mid" is a backtest fantasy. For AI systems the equivalent is quality measured without latency, token cost or the retry rate.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. Data that leaks vs. data the model could actually see
&lt;/h3&gt;

&lt;p&gt;The economics markets ask things like "Will CPI come in above 3.2%?" The model's estimates were wildly off, and for weeks I treated that as a modelling problem.&lt;/p&gt;

&lt;p&gt;It was a parsing bug. Any strike with a decimal point got cut off, so the question the model received read &lt;strong&gt;"more than -0"&lt;/strong&gt;. 251 rows were estimated from questions with the threshold missing. I was grading a model on a test it could not read.&lt;/p&gt;

&lt;p&gt;The opposite leak showed up in the backtest. The walk-forward calibration was quietly using information from after the forecast time (look-ahead), and it counted some markets more than once. Fixing both made the numbers worse, and they were also the first honest ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; before you blame the model, read the exact input it received. Then check that your evaluation can't see the future.&lt;/p&gt;




&lt;h3&gt;
  
  
  The one metric I trust now
&lt;/h3&gt;

&lt;p&gt;After all five fixes, one number decides whether real money is allowed:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Realised edge, net of fees, at the executable price, one row per market, with a bootstrapped 95% confidence interval whose lower bound must clear zero.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The all-time figure today: &lt;strong&gt;-0.61pp, CI [-8.01, +6.88].&lt;/strong&gt; Not cleared.&lt;/p&gt;

&lt;p&gt;Then there's the part nobody puts on a dashboard: statistical power. My earlier gate said I needed "about 400 more bets." That was a 50%-power figure built on an effect size that turned out to be inflated. At 80% power, proving a real &lt;strong&gt;3-point edge takes about 2,000 bets.&lt;/strong&gt; At my volume, that's a long time.&lt;/p&gt;

&lt;p&gt;So the bot doesn't trade. Specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real orders are hard-disabled.&lt;/strong&gt; Every command that can place one refuses unless an explicit environment flag is set for that single run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There's a lifetime loss cap&lt;/strong&gt; of $50 per venue, enforced in code, not by willpower.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kill criteria are pre-registered, and "kill" is the default.&lt;/strong&gt; The economics strategy gets judged on a fixed date against a fixed test. If the result is ambiguous, it dies. I wrote the rule down before seeing the data so I can't negotiate with myself later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No refitting a dead hypothesis.&lt;/strong&gt; When "favourites" failed, the 0.70–0.80 slice looked mildly positive. That slice came from cutting up a failed experiment, so it doesn't get a test of its own.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Why I'm publishing a failure
&lt;/h3&gt;

&lt;p&gt;Every "I built an AI trading bot" post I've read ends with an upward-sloping equity curve. Mine ends with a confidence interval that straddles zero and an honest power calculation. That's the more useful post.&lt;/p&gt;

&lt;p&gt;The bot didn't fail because LLMs can't reason about probabilities. The &lt;em&gt;evaluation&lt;/em&gt; failed five times, and each failure flattered the model. That pattern is everywhere in AI engineering right now: a leaderboard number that doesn't match your traffic, a demo scored on the easy cases, a cost model off by 5×, a benchmark leaking into the training data.&lt;/p&gt;

&lt;p&gt;The forecaster was the easy part. Building a measurement system that couldn't lie to me took months, and that's the part I'd hire for.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you're building evals for agents or forecasters and want to compare notes on baselines, power and pre-registration, I'd love to hear how you're doing it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>testing</category>
    </item>
    <item>
      <title>Your Developer Toolchain Is a Supply Chain Attack Surface — Here's How I Hardened Mine</title>
      <dc:creator>Prasanna Nattuthurai</dc:creator>
      <pubDate>Sat, 22 Aug 2026 17:14:58 +0000</pubDate>
      <link>https://dev.to/prasannavarshan/your-developer-toolchain-is-a-supply-chain-attack-surface-heres-how-i-hardened-mine-4ghm</link>
      <guid>https://dev.to/prasannavarshan/your-developer-toolchain-is-a-supply-chain-attack-surface-heres-how-i-hardened-mine-4ghm</guid>
      <description>&lt;p&gt;&lt;em&gt;A practical guide to applying SLSA, Sigstore, and SBOM-based verification to the tools on your local machine.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;We obsess over dependency scanning in CI/CD. We pin versions in &lt;code&gt;package-lock.json&lt;/code&gt;. We run Dependabot. But what about the tools that &lt;em&gt;build&lt;/em&gt; your software?&lt;/p&gt;

&lt;p&gt;Every morning, you run &lt;code&gt;brew upgrade&lt;/code&gt;. npm updates your global packages. &lt;code&gt;rustup update&lt;/code&gt; refreshes your Rust toolchain. Each of these is an implicit trust decision:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I trust that the binary I just downloaded is exactly what the publisher built, from the source code I'd expect, without tampering in transit."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For most developers, that trust is blind. I decided to make it explicit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Threat Model: What Can Go Wrong
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threat&lt;/th&gt;
&lt;th&gt;Real-World Example&lt;/th&gt;
&lt;th&gt;Likelihood&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Malicious package in registry&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;event-stream&lt;/code&gt; (npm, 2018), &lt;code&gt;ua-parser-js&lt;/code&gt; (npm, 2021)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;High&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compromised Homebrew tap&lt;/td&gt;
&lt;td&gt;Typosquatting tap overrides official formula&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;npm postinstall script RCE&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;eslint-scope&lt;/code&gt; credential theft (2018)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;High&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Binary replaced after install&lt;/td&gt;
&lt;td&gt;Compromised local system, PATH hijack&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS/BGP hijack on update channel&lt;/td&gt;
&lt;td&gt;PyPI CDN poisoning (hypothetical)&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency confusion&lt;/td&gt;
&lt;td&gt;Internal package name claimed on public registry&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The attack surface is your entire toolchain: &lt;strong&gt;compilers, linters, security scanners, CLI tools, language runtimes&lt;/strong&gt;. If an attacker compromises &lt;code&gt;trivy&lt;/code&gt;, they can hide their own CVEs from your scans.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture: 6 Defense Layers
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiocvviu2qkcdw6dbdw6z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiocvviu2qkcdw6dbdw6z.png" alt="Supply Chain Verification Architecture" width="800" height="853"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────────┐
│                    DEVELOPER WORKSTATION                             │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Layer 1: TRANSPORT SECURITY                                        │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │  All channels: TLS 1.2+ (brew, npm, PyPI, crates.io, Go)    │  │
│  └──────────────────────────────────────────────────────────────┘  │
│                              │                                      │
│  Layer 2: CRYPTOGRAPHIC VERIFICATION                                │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │  brew: SHA256 bottles    │  uv: Sigstore cosign              │  │
│  │  rustup: signed manifest │  npm: ECDSA registry sigs         │  │
│  │  macOS: Apple code-sign  │  Go: sum.golang.org               │  │
│  └──────────────────────────────────────────────────────────────┘  │
│                              │                                      │
│  Layer 3: SOURCE ALLOWLISTING                                       │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │  Homebrew tap allowlist (BLOCK on untrusted)                  │  │
│  │  npm pinned package list + --ignore-scripts                   │  │
│  └──────────────────────────────────────────────────────────────┘  │
│                              │                                      │
│  Layer 4: SYSTEM INTEGRITY                                          │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │  SIP enabled │ Gatekeeper enabled │ No world-writable PATH   │  │
│  └──────────────────────────────────────────────────────────────┘  │
│                              │                                      │
│  Layer 5: AUDIT TRAIL                                               │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │  SHA256 pre/post snapshot │ Timestamped log │ Diff alerting   │  │
│  └──────────────────────────────────────────────────────────────┘  │
│                              │                                      │
│  Layer 6: SUPPLY-CHAIN VERIFICATION (post-update)                   │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │  ┌─────────────┐ ┌───────────────┐ ┌─────────────────────┐  │  │
│  │  │ brew verify │ │ npm audit sig │ │ gh attestation      │  │  │
│  │  │ (Sigstore)  │ │ (provenance)  │ │ verify (SLSA L3)    │  │  │
│  │  └─────────────┘ └───────────────┘ └─────────────────────┘  │  │
│  │  ┌─────────────┐ ┌───────────────┐ ┌─────────────────────┐  │  │
│  │  │ syft→grype  │ │ govulncheck   │ │ cargo audit         │  │  │
│  │  │ (SBOM+CVE)  │ │ (-mode=binary)│ │ (RustSec DB)        │  │  │
│  │  └─────────────┘ └───────────────┘ └─────────────────────┘  │  │
│  │  ┌─────────────┐ ┌───────────────┐                          │  │
│  │  │ codesign -v │ │ SHA diff alert│                          │  │
│  │  │ (macOS sig) │ │ (tampering)   │                          │  │
│  │  └─────────────┘ └───────────────┘                          │  │
│  └──────────────────────────────────────────────────────────────┘  │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Gap Analysis: Industry vs. Reality
&lt;/h2&gt;

&lt;p&gt;I benchmarked my existing toolchain updater against SLSA v1.0, NIST SP 800-218 (SSDF), and CISA's Secure Software Development guidance. Here's what I found:&lt;/p&gt;

&lt;h3&gt;
  
  
  What Most Developers Have (SLSA L1)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;✅ Lock files with checksums (&lt;code&gt;package-lock.json&lt;/code&gt;, &lt;code&gt;Cargo.lock&lt;/code&gt;, &lt;code&gt;go.sum&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;✅ TLS on all download channels&lt;/li&gt;
&lt;li&gt;✅ Package manager verifies checksums at install time&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What's Missing (SLSA L2/L3)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gap&lt;/th&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;th&gt;What's actually needed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No independent provenance verification&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Don't just trust the package manager's internal check — verify Sigstore attestations yourself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;npm audit&lt;/code&gt; ≠ &lt;code&gt;npm audit signatures&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;CVE scanning is different from provenance verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Release downloads unattested&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;If you download a tarball from Releases, verify SLSA provenance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No SBOM of installed toolchain&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;You can't find vulns inside compiled binaries without an SBOM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHA recorded but never compared&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;High&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A hash that's never diffed is security theater&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go binaries never scanned&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Most security tools (trivy, gh, gitleaks) are Go — embedded deps have vulns too&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cargo binaries never audited&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;crates.io has zero Sigstore support — &lt;code&gt;cargo audit&lt;/code&gt; is all you have&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Sigstore Provenance Verification (Homebrew)
&lt;/h3&gt;

&lt;p&gt;Homebrew bottles from &lt;code&gt;homebrew-core&lt;/code&gt; have had SLSA L3 Sigstore attestations since 2024. Most developers don't know this — or that you can verify them independently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Verify your security tools came from the real Homebrew CI&lt;/span&gt;
brew verify trivy
brew verify semgrep
brew verify gitleaks
brew verify gh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This checks the Sigstore transparency log and confirms the bottle was built by Homebrew's GitHub Actions workflow, from the expected source commit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real output from my system:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[sec]   ✓ provenance verified: trivy
[sec]   ✓ provenance verified: semgrep
[sec]   ✓ provenance verified: gitleaks
[sec]   ✓ provenance verified: osv-scanner
[sec]   ✓ provenance verified: gh
[sec]   ✓ provenance verified: jq
[sec]   ✓ provenance verified: node
[sec]   ✓ provenance verified: python@3.13
[sec]   ✓ provenance verified: go
[ok]    Homebrew provenance: all 9 critical formulae verified
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. npm Provenance Signatures (Not Just &lt;code&gt;npm audit&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;npm audit&lt;/code&gt; checks for known CVEs. &lt;code&gt;npm audit signatures&lt;/code&gt; checks that packages were built from their claimed source repo via Sigstore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# This checks CVEs:&lt;/span&gt;
npm audit

&lt;span class="c"&gt;# This checks provenance (different!):&lt;/span&gt;
npm audit signatures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are complementary, not interchangeable. A package can have zero CVEs but be a complete forgery.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. SBOM + Binary Vulnerability Scanning
&lt;/h3&gt;

&lt;p&gt;Here's the insight most developers miss: &lt;strong&gt;compiled binaries embed their entire dependency tree.&lt;/strong&gt; A Go binary built with a vulnerable &lt;code&gt;net/http&lt;/code&gt; is vulnerable even after you've updated your system Go.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate an SBOM of all your Homebrew binaries&lt;/span&gt;
syft &lt;span class="nb"&gt;dir&lt;/span&gt;:&lt;span class="si"&gt;$(&lt;/span&gt;brew &lt;span class="nt"&gt;--prefix&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/bin &lt;span class="nt"&gt;-o&lt;/span&gt; cyclonedx-json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; toolchain-sbom.json

&lt;span class="c"&gt;# Scan for embedded vulnerabilities&lt;/span&gt;
grype sbom:toolchain-sbom.json &lt;span class="nt"&gt;--only-fixed&lt;/span&gt; &lt;span class="nt"&gt;--fail-on&lt;/span&gt; high
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;My result:&lt;/strong&gt; &lt;code&gt;grype: no high/critical fixable vulns in toolchain binaries&lt;/code&gt; ✅&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Go Binary Vulnerability Scanning
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;govulncheck -mode=binary&lt;/code&gt; is the Go team's official tool for scanning compiled Go binaries. It reads the embedded module info and checks against the Go vulnerability database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Scan your security tools (they're all written in Go!)&lt;/span&gt;
govulncheck &lt;span class="nt"&gt;-mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;binary &lt;span class="si"&gt;$(&lt;/span&gt;which trivy&lt;span class="si"&gt;)&lt;/span&gt;
govulncheck &lt;span class="nt"&gt;-mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;binary &lt;span class="si"&gt;$(&lt;/span&gt;which gh&lt;span class="si"&gt;)&lt;/span&gt;
govulncheck &lt;span class="nt"&gt;-mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;binary &lt;span class="si"&gt;$(&lt;/span&gt;which gitleaks&lt;span class="si"&gt;)&lt;/span&gt;
govulncheck &lt;span class="nt"&gt;-mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;binary &lt;span class="si"&gt;$(&lt;/span&gt;which osv-scanner&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a first run this surfaced a batch of Go stdlib DoS advisories in one of the scanners above — the binary had been compiled against an older Go toolchain than the one that fixed them. Low risk in a local CLI context, and resolved by a rebuild, but the point is that nothing else I ran would have told me.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. SHA Diff Alerting (The Most Important One)
&lt;/h3&gt;

&lt;p&gt;Recording SHA256 hashes is meaningless if you never compare them. The real value is &lt;strong&gt;detecting unexplained changes&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# After every update run, record hashes&lt;/span&gt;
shasum &lt;span class="nt"&gt;-a&lt;/span&gt; 256 &lt;span class="si"&gt;$(&lt;/span&gt;brew &lt;span class="nt"&gt;--prefix&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/bin/trivy &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; binary-shas.txt

&lt;span class="c"&gt;# On next run, compare:&lt;/span&gt;
&lt;span class="c"&gt;# - If SHA changed AND an update was logged → expected ✅&lt;/span&gt;
&lt;span class="c"&gt;# - If SHA changed WITHOUT a logged update → RED FLAG 🚨&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;My implementation detects this automatically:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[sec]   Comparing runs: 2026-08-09 → 2026-08-18
[sec]   ✓ go: SHA changed (update logged)
[ok]    Binary diff: 1 changes, all explained by logged updates
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a binary ever changes without a corresponding update in the log, the script flags it as potential tampering.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Complete Flow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────┐
│   brew upgrade   │──→ SHA256 bottle verification (automatic)
│   npm install -g │──→ --ignore-scripts (kill postinstall RCE)
│   uv self update │──→ Sigstore cosign verification (automatic)
│   rustup update  │──→ Signed manifest verification (automatic)
└────────┬─────────┘
         │
         ▼
┌──────────────────────────────────────────────────────┐
│            POST-UPDATE VERIFICATION                   │
├──────────────────────────────────────────────────────┤
│                                                      │
│  1. brew verify (Sigstore provenance)                │
│  2. npm audit signatures (provenance ≠ CVE scan)    │
│  3. gh attestation verify (SLSA L3 for Releases)    │
│  4. syft → grype (SBOM-based binary vuln scan)      │
│  5. codesign -v (macOS signature validation)        │
│  6. SHA diff alerting (detect unexplained changes)   │
│  7. govulncheck -mode=binary (Go embedded deps)     │
│  8. cargo audit (RustSec advisory DB)               │
│                                                      │
└────────┬─────────────────────────────────────────────┘
         │
         ▼
┌──────────────────┐
│  SHA256 snapshot │──→ Baseline for next run's diff
│  SBOM archived   │──→ Audit trail
│  Log recorded    │──→ Evidence
└──────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What This Catches (Real Examples)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Which layer detects it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;event-stream&lt;/code&gt; style npm hijack&lt;/td&gt;
&lt;td&gt;Layer 3 (&lt;code&gt;--ignore-scripts&lt;/code&gt;) blocks execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typosquatting Homebrew tap&lt;/td&gt;
&lt;td&gt;Layer 3 (tap allowlist blocks unknown taps)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compromised brew bottle&lt;/td&gt;
&lt;td&gt;Layer 6 (&lt;code&gt;brew verify&lt;/code&gt; — Sigstore attestation mismatch)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Binary replaced by attacker on disk&lt;/td&gt;
&lt;td&gt;Layer 5/6 (SHA diff alert — unexplained change)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vulnerable dependency inside &lt;code&gt;trivy&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Layer 6 (&lt;code&gt;govulncheck -mode=binary&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;npm package without provenance&lt;/td&gt;
&lt;td&gt;Layer 6 (&lt;code&gt;npm audit signatures&lt;/code&gt; flags it)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PATH hijack via world-writable dir&lt;/td&gt;
&lt;td&gt;Layer 4 (detected at script startup)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Ecosystem Reality Check
&lt;/h2&gt;

&lt;p&gt;Not everything is verifiable yet. Here's the honest state as of 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ecosystem&lt;/th&gt;
&lt;th&gt;Provenance Coverage&lt;/th&gt;
&lt;th&gt;What's available&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Homebrew&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ 100% (homebrew-core)&lt;/td&gt;
&lt;td&gt;Sigstore attestations on all bottles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;npm&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚠️ ~7% of packages&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;npm audit signatures&lt;/code&gt; + registry ECDSA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PyPI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚠️ ~17% of uploads&lt;/td&gt;
&lt;td&gt;PEP 740 attestations (growing fast)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Go modules&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Checksums (sum.golang.org)&lt;/td&gt;
&lt;td&gt;No Sigstore, but tamper-evident log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;crates.io&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Checksums only&lt;/td&gt;
&lt;td&gt;No Sigstore, no provenance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitHub Releases&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚠️ Opt-in&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gh attestation verify&lt;/code&gt; (if publisher enables)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The strategy is &lt;strong&gt;defense in depth&lt;/strong&gt;: verify what you can cryptographically, scan what you can't verify, and detect tampering as a last resort.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started (15 Minutes)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Install the verification tools&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;syft grype cosign
go &lt;span class="nb"&gt;install &lt;/span&gt;golang.org/x/vuln/cmd/govulncheck@latest
cargo &lt;span class="nb"&gt;install &lt;/span&gt;cargo-audit

&lt;span class="c"&gt;# 2. Verify your current Homebrew bottles&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in &lt;/span&gt;trivy semgrep gh node python@3.13 go&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;brew verify &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✓ &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✗ &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# 3. Check npm provenance&lt;/span&gt;
npm audit signatures

&lt;span class="c"&gt;# 4. Generate your first toolchain SBOM&lt;/span&gt;
syft &lt;span class="nb"&gt;dir&lt;/span&gt;:&lt;span class="si"&gt;$(&lt;/span&gt;brew &lt;span class="nt"&gt;--prefix&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/bin &lt;span class="nt"&gt;-o&lt;/span&gt; cyclonedx-json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/toolchain-sbom.json
grype sbom:~/toolchain-sbom.json &lt;span class="nt"&gt;--only-fixed&lt;/span&gt;

&lt;span class="c"&gt;# 5. Scan Go binaries&lt;/span&gt;
govulncheck &lt;span class="nt"&gt;-mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;binary &lt;span class="si"&gt;$(&lt;/span&gt;which trivy&lt;span class="si"&gt;)&lt;/span&gt;
govulncheck &lt;span class="nt"&gt;-mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;binary &lt;span class="si"&gt;$(&lt;/span&gt;which gh&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# 6. Create your SHA baseline&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;bin &lt;span class="k"&gt;in &lt;/span&gt;brew node npm go cargo trivy gh&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;shasum &lt;span class="nt"&gt;-a&lt;/span&gt; 256 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;which &lt;span class="nv"&gt;$bin&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/binary-shas-baseline.txt
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Recording hashes is not monitoring.&lt;/strong&gt; A hash file that's never diffed is security theater. You need automated comparison with explainability (was there a logged update or not?).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Your security tools are attack surface.&lt;/strong&gt; &lt;code&gt;trivy&lt;/code&gt;, &lt;code&gt;semgrep&lt;/code&gt;, &lt;code&gt;gitleaks&lt;/code&gt; — if these are compromised, they can hide their own findings. Scan the scanners.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;npm audit&lt;/code&gt; is not &lt;code&gt;npm audit signatures&lt;/code&gt;.&lt;/strong&gt; One checks CVEs, the other checks provenance. You need both.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Bash 3.2 is still the default on macOS.&lt;/strong&gt; If your security script uses &lt;code&gt;mapfile&lt;/code&gt; or associative arrays, it silently fails on every Mac. Ship it or test it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Timeouts matter.&lt;/strong&gt; Any network call in a security script that can hang indefinitely will eventually hang indefinitely. I learned this the hard way when Sigstore transparency log queries hung for 90 minutes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SLSA is a consumer spec too.&lt;/strong&gt; Most people think SLSA is for producers/builders. It's equally a spec for how consumers should verify artifacts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A check that fires on a healthy machine is worse than no check.&lt;/strong&gt; My world-writable &lt;code&gt;PATH&lt;/code&gt; test matched any two &lt;code&gt;w&lt;/code&gt; characters in the permission string — so it flagged every &lt;em&gt;group&lt;/em&gt;-writable directory, which is Homebrew's own normal layout. It also read the wrong column, testing other-&lt;em&gt;read&lt;/em&gt; instead of other-write. Result: &lt;code&gt;PATH hijack risk!&lt;/code&gt; on a correctly configured Mac, every single run — a warning with no way to act on it, which is the kind you stop reading. A false positive doesn't just waste attention; it trains you to skip the output, and then the true positive scrolls past too. Prefer a false negative you'll notice missing over a false alarm you'll learn to filter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;My tool was broken for every user but me, and only a second machine could tell me.&lt;/strong&gt; One line read the list of Rust-installed binaries:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   cargo &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--list&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"^[a-z]"&lt;/span&gt; | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; line&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under &lt;code&gt;set -euo pipefail&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt; matching nothing exits 1, which aborts the entire script. On a machine with no cargo-installed binaries — a fresh Mac, most people's Mac — the dry run died silently a third of the way through. It worked flawlessly for me because I happen to have some. No amount of testing on my own laptop could have surfaced it; a CI runner found it on its first execution. If you're writing a tool that inspects an environment, the one environment you cannot test in is your own.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://slsa.dev/spec/v1.0/verifying-artifacts" rel="noopener noreferrer"&gt;SLSA v1.0 — Verification specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://csrc.nist.gov/publications/detail/sp/800-218/final" rel="noopener noreferrer"&gt;NIST SP 800-218 (SSDF)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Homebrew/brew/issues/17019" rel="noopener noreferrer"&gt;Homebrew Sigstore attestations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.npmjs.com/generating-provenance-statements" rel="noopener noreferrer"&gt;npm provenance documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck" rel="noopener noreferrer"&gt;govulncheck binary mode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/anchore/syft" rel="noopener noreferrer"&gt;Anchore syft + grype&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://rustsec.org/" rel="noopener noreferrer"&gt;RustSec Advisory Database&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The full implementation is open source — a 1,300-line bash script with no&lt;br&gt;
runtime dependencies beyond Homebrew itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew tap prasannavarshan/tap
brew trust prasannavarshan/tap    &lt;span class="c"&gt;# Homebrew gates third-party taps&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;update-toolchain

update-toolchain &lt;span class="nt"&gt;--dry&lt;/span&gt;            &lt;span class="c"&gt;# report; change nothing&lt;/span&gt;
update-toolchain &lt;span class="nt"&gt;--audit&lt;/span&gt;          &lt;span class="c"&gt;# security checks only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source, threat model and known limitations:&lt;br&gt;
&lt;a href="https://github.com/prasannavarshan/update-toolchain" rel="noopener noreferrer"&gt;github.com/prasannavarshan/update-toolchain&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Allowlists are config files rather than code, so nothing updates unless you have&lt;br&gt;
named it — and the config is parsed, never sourced, because a tool that decides&lt;br&gt;
what to trust should not let its own config file execute anything.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; #SupplyChain #DevSecOps #SLSA #Sigstore #macOS #Homebrew #SBOM #SecurityEngineering&lt;/p&gt;

</description>
      <category>security</category>
      <category>supplychain</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
