<?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: Nathan Schram</title>
    <description>The latest articles on DEV Community by Nathan Schram (@nathanschram).</description>
    <link>https://dev.to/nathanschram</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3407497%2F7010b19e-8815-427b-9c52-de6b1c7bcf66.png</url>
      <title>DEV Community: Nathan Schram</title>
      <link>https://dev.to/nathanschram</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nathanschram"/>
    <language>en</language>
    <item>
      <title>I voice-code from my phone while walking my dog</title>
      <dc:creator>Nathan Schram</dc:creator>
      <pubDate>Thu, 02 Apr 2026 05:22:14 +0000</pubDate>
      <link>https://dev.to/nathanschram/i-voice-code-from-my-phone-while-walking-my-dog-3d8g</link>
      <guid>https://dev.to/nathanschram/i-voice-code-from-my-phone-while-walking-my-dog-3d8g</guid>
      <description>&lt;p&gt;Last Wednesday afternoon I was at the oval with Normi, my 13-year-old dog, playing tug of war with his favourite rope ball. Between rounds I pulled out my phone, recorded a voice note asking Claude Code to run the full engine test suite across six Telegram chats, and went back to playing. Twenty minutes later, Normi and I were both sitting on the grass, absolutely pooped. I checked Telegram. Claude Code had finished testing, logged the bugs it found, and created GitHub issues for each one. I hadn't typed a single character.&lt;/p&gt;

&lt;p&gt;That's most of my afternoons now.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I spend 2-4 hours a day walking my 13-year-old dog Normi. During those walks, I dictate coding tasks to Claude Code via Telegram voice notes using &lt;a href="https://github.com/littlebearapps/untether" rel="noopener noreferrer"&gt;Untether&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Voice input is roughly 4x faster than typing on a phone (150 WPM speaking vs 40 WPM typing). The walks themselves boost creative output by 60% compared to sitting (Stanford, &lt;a href="https://news.stanford.edu/stories/2014/04/walking-vs-sitting-042414" rel="noopener noreferrer"&gt;176 participants&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;This isn't a novelty. It's how I work every day. Honestly, I get more done on walks than I do at my desk.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Simon Willison &lt;a href="https://x.com/simonw/status/1853872615922012186" rel="noopener noreferrer"&gt;put it well&lt;/a&gt; back in November 2024: "Coding while walking the dog is an underrated benefit of AI tooling." He never wrote the detailed post. So here it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: AI coding agents need you at a terminal
&lt;/h2&gt;

&lt;p&gt;AI coding agents are genuinely useful. 71% of developers who regularly use AI agents use Claude Code (&lt;a href="https://www.getpanto.ai/blog/ai-coding-assistant-statistics" rel="noopener noreferrer"&gt;Pragmatic Engineer survey&lt;/a&gt;, 15,000 developers, Feb 2026). 4% of all public GitHub commits are now authored by Claude Code (&lt;a href="https://newsletter.semianalysis.com/p/claude-code-is-the-inflection-point" rel="noopener noreferrer"&gt;SemiAnalysis&lt;/a&gt;, Feb 2026).&lt;/p&gt;

&lt;p&gt;They all share one assumption: you're sitting at a terminal.&lt;/p&gt;

&lt;p&gt;Claude Code runs in a terminal. You watch it work. It asks permission to edit files or run commands. You type &lt;code&gt;y&lt;/code&gt; or &lt;code&gt;n&lt;/code&gt;. If you walk away, it stalls. The session just sits there waiting for you to come back and press a key.&lt;/p&gt;

&lt;p&gt;I'm a vibe coder. Not CS-trained, background in sales and ops, 13 years in tech. I run Little Bear Apps in Melbourne and I &lt;a href="https://littlebearapps.com/blog/dogfooding-bugs-tests-cant-find/" rel="noopener noreferrer"&gt;build tools to scratch my own itch&lt;/a&gt;. And I kept finding myself mid-session with Claude Code on my MacBook, absolutely in the zone, when I had to leave. Walk the dog. Go to the shops. Whatever. I hated it. Every time I walked away, the session died.&lt;/p&gt;

&lt;p&gt;I tried the Blink shell iOS app with TMUX and MOSH connecting to my VPS. That worked okay. I could at least see the terminal from my phone. Typing on a tiny screen while holding a leash isn't great.&lt;/p&gt;

&lt;p&gt;There are official solutions now. Claude Code Remote Control (February 2026) lets you scan a QR code from the Claude mobile app. Claude Code Channels (March 20, 2026) adds Telegram and Discord support through MCP. Both are Claude-only, text-only, and Channels still pauses at the terminal when it needs permission.&lt;/p&gt;

&lt;p&gt;As of March 2026, none of them support voice input, multiple AI engines, or interactive permission buttons from a phone. I needed a proper remote coding workflow - one where I could speak a task into my phone while walking my dog and have it just... work. Including the permission prompts.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I found takopi and why I rebuilt it
&lt;/h2&gt;

&lt;p&gt;I found &lt;a href="https://github.com/banteg/takopi" rel="noopener noreferrer"&gt;banteg/takopi&lt;/a&gt; in late December 2025. It's a Telegram notifier for AI coding agents, and at first it was an absolute godsend. I could hook up voice-to-text transcription via Telegram, record a voice note, and it would send the task to Claude Code. Brilliant.&lt;/p&gt;

&lt;p&gt;Then I hit the wall.&lt;/p&gt;

&lt;p&gt;Takopi doesn't handle Claude Code's interactive bits. When Claude Code needs permission to run a command, or wants to exit plan mode to implement something, or asks you a question, Takopi just... freezes. The agent sits there waiting for input that never comes. Your Telegram chat goes silent. You don't even know it's stuck unless you check.&lt;/p&gt;

&lt;p&gt;I opened issues. I waited three, maybe four weeks for a response from banteg on the repo. Nothing. The bugs are still there today.&lt;/p&gt;

&lt;p&gt;So I forked it and rebuilt it. &lt;a href="https://github.com/littlebearapps/untether" rel="noopener noreferrer"&gt;Untether&lt;/a&gt; launched in February 2026, and it's been my primary development tool since.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup: what connects to what
&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.amazonaws.com%2Fuploads%2Farticles%2Fpx9dqtyfi9qzy9w5dmxw.webp" 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.amazonaws.com%2Fuploads%2Farticles%2Fpx9dqtyfi9qzy9w5dmxw.webp" alt="Architecture diagram showing the Untether pipeline: Telegram voice note to speech-to-text transcription via API, then to Untether and Claude Code (plus Codex, Gemini, OpenCode, Pi, and Amp) running on a Hetzner VPS, with interactive permissions, live streaming, and two-way file transfer flowing back" width="800" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The chain looks like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;iPhone&lt;/strong&gt; (Telegram app) -&amp;gt; &lt;strong&gt;Telegram Bot API&lt;/strong&gt; -&amp;gt; &lt;strong&gt;Untether&lt;/strong&gt; (Python, running on my VPS) -&amp;gt; &lt;strong&gt;Claude Code&lt;/strong&gt; (also on my VPS)&lt;/p&gt;

&lt;p&gt;The VPS (virtual private server) matters. I run Untether and Claude Code on a Hetzner server in Germany. Not on my MacBook, not on my home network. This means I don't care if the power's on at home, if my MacBook is sleeping, or if my home internet drops. The VPS is always on. Even if my phone dies mid-walk, the coding agent keeps working. I'll see the results when I get back. (The VPS also runs the infrastructure I wrote about in &lt;a href="https://littlebearapps.com/blog/d1-billing-disaster-circuit-breakers/" rel="noopener noreferrer"&gt;how a D1 billing disaster taught me to build circuit breakers&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/littlebearapps/untether" rel="noopener noreferrer"&gt;Untether&lt;/a&gt; is open source, Python 3.12+, and installs with one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install &lt;/span&gt;untether
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key pieces:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Voice transcription.&lt;/strong&gt; When I record a voice note in Telegram, Untether sends it to a Whisper-compatible endpoint via &lt;a href="https://groq.com" rel="noopener noreferrer"&gt;Groq&lt;/a&gt; for transcription, then passes the text to Claude Code as a task. I don't type on my phone. I talk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Progress streaming.&lt;/strong&gt; As Claude Code works, Untether streams updates to my Telegram chat. Tool calls, file changes, elapsed time. I can watch it think in real time or just check back later.&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.amazonaws.com%2Fuploads%2Farticles%2F6nlj6xbvabwxi8hul59s.webp" 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.amazonaws.com%2Fuploads%2Farticles%2F6nlj6xbvabwxi8hul59s.webp" alt="Untether streaming progress in Telegram - tool calls, file changes, and working status visible in real time" width="590" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interactive permissions.&lt;/strong&gt; This is the part that makes it actually usable away from a terminal. When Claude Code needs to run a command, edit a file, or exit plan mode, Untether shows me inline Telegram buttons. Approve, Deny, or reply with instructions. No terminal required.&lt;/p&gt;

&lt;p&gt;I leave plan mode on and I leave permissions on. I prefer to have some control rather than letting Claude Code just go wild. I built a custom button called "Pause and outline plan" that forces Claude Code to write out a detailed plan before it does anything. In the version I'm about to ship (v0.35.0), I've added a second step after that: Approve, Deny, and a new "Stop and let's discuss" button. Sometimes you want to talk it through before committing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple engines.&lt;/strong&gt; Untether isn't locked to Claude Code. It supports Codex, OpenCode, and Pi today, with Gemini CLI and Amp coming. I mostly use Claude Code, but the multi-engine support matters for testing. I have one Telegram chat per engine, and Claude Code can actually switch between them during automated test runs using a &lt;a href="https://github.com/chigwell/telegram-mcp" rel="noopener noreferrer"&gt;Telegram MCP server&lt;/a&gt; I helped fix (we submitted &lt;a href="https://github.com/chigwell/telegram-mcp/pull/77" rel="noopener noreferrer"&gt;a PR&lt;/a&gt; fixing an entity cache bug that broke 87% of operations for session-based users).&lt;/p&gt;

&lt;p&gt;One thing worth knowing if you use multiple engines: each one has its own context file format. Claude Code reads CLAUDE.md, Codex wants agents.md, Gemini has its own thing. If you've only set up context for one engine, the others will still work but they'll take longer to get oriented. Your directory-level context, global context, working directory structure - all of it matters. Get your infrastructure right and Untether works perfectly regardless of which engine you're talking to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does talking beat typing on a phone?
&lt;/h2&gt;

&lt;p&gt;Speaking is roughly 4x faster than typing on a phone screen. 150 words per minute speaking versus about 40 WPM thumb-typing (&lt;a href="https://wisprflow.ai/vibe-coding" rel="noopener noreferrer"&gt;Wispr Flow&lt;/a&gt;). On a walk, with a leash in one hand, that difference matters.&lt;/p&gt;

&lt;p&gt;Speed isn't the real advantage, though. The real advantage is that talking forces you to think out loud, and thinking out loud produces better prompts.&lt;/p&gt;

&lt;p&gt;When I type a task for Claude Code at my desk, I tend to be terse: "refactor the auth middleware." When I'm walking and talking, I naturally add context: "Hey, the auth middleware in Viewpo is getting messy - the session validation is mixed in with the role checking. Can you split those into separate middleware functions? Keep the existing tests passing."&lt;/p&gt;

&lt;p&gt;The voice prompt is longer, more specific, and gives Claude Code more to work with. I'm not trying to be thorough. I'm just talking the way people talk.&lt;/p&gt;

&lt;p&gt;I'm a waffler. I love to talk things out, talk things through, often just to crystallise something for myself as I say it. Claude Code takes that waffle and rearranges it into something structured. It's a surprisingly good loop: I ramble with context, Claude Code extracts the actual task.&lt;/p&gt;

&lt;h3&gt;
  
  
  What voice transcription gets wrong
&lt;/h3&gt;

&lt;p&gt;Honestly? Not much. As long as you speak reasonably loudly and clearly, Groq handles it well. I've only had a couple of times where words genuinely got mangled beyond recognition.&lt;/p&gt;

&lt;p&gt;If I'm mumbling, or doing my neurodiverse ADHD waffle thing where I'm jumping between thoughts mid-sentence, yeah, it can struggle a bit. But Claude Code is pretty good at inferring intent even from imperfect transcription. Most of the time, close enough is close enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you handle AI permissions from your phone?
&lt;/h2&gt;

&lt;p&gt;AI coding agents from your phone have a problem: the agent is going to ask you questions. It's going to want permission to delete files, run tests, push code. If you can't respond to those prompts, the session stalls.&lt;/p&gt;

&lt;p&gt;Takopi didn't handle this. You could send a task, but when Claude Code hit a permission prompt, everything just stopped until you got back to a terminal.&lt;/p&gt;

&lt;p&gt;Untether solves this with inline Telegram buttons:&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.amazonaws.com%2Fuploads%2Farticles%2Fi9jbk63jjybkofryei2u.webp" 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.amazonaws.com%2Fuploads%2Farticles%2Fi9jbk63jjybkofryei2u.webp" alt="Plan mode approval buttons in Telegram - Approve, Deny, and Pause and Outline Plan options appear inline" width="590" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plan mode&lt;/strong&gt; toggles per-chat. I leave it on. When Claude Code wants to implement a plan, I get buttons: Approve, Deny, or my custom "Pause and outline plan"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approve/Deny buttons&lt;/strong&gt; appear inline when Claude Code needs permission for destructive operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Progressive cooldown&lt;/strong&gt; reduces prompt frequency for repeated similar actions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask mode&lt;/strong&gt; lets Claude Code ask me questions through Telegram. I can reply with text or another voice note&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost controls&lt;/strong&gt; with per-run and daily budgets, &lt;code&gt;/usage&lt;/code&gt; breakdowns. Important when you're kicking off tasks and walking away&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "Pause and outline plan" button is one I built for my own workflow. Claude Code in plan mode is a life saver. I'd rather read a plan and approve it than have the agent just start editing files. And in v0.35.0, after Claude Code writes the outline, you get three choices: approve it, deny it, or hit "Stop and let's discuss" if you want to talk it through first.&lt;/p&gt;

&lt;p&gt;This is the part that makes the workflow real rather than theoretical. Without interactive permissions, "code from your phone" means "start a task and hope for the best." With them, I have the same control I'd have at my desk. Just through buttons instead of keystrokes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a real walk looks like
&lt;/h2&gt;

&lt;p&gt;Normi is 13. He's a 16-kilo staffy cross pug cross French bulldog. A little fun-sized potato. Super friendly with everyone, loves people, tolerates cats, and sounds absolutely vicious when he plays. He isn't. He's having the time of his life.&lt;/p&gt;

&lt;p&gt;We go out two or three times a day. Sometimes we do the same tracks and parks we always do, sometimes we explore new ones. I'm outside for probably two to four hours total, depending on the weather and what we're up to. We'll often stop at an oval so Normi can play.&lt;/p&gt;

&lt;p&gt;I call the game "grrrrr" - which is basically the noise Normi makes while playing it. It's tug of war combined with chasey. I got these nearly indestructible dog balls with tug of war ropes on them, and Normi goes absolutely feral for them. He grabs one end, I grab the other, and he growls and shakes his head like he's fighting a crocodile. Then he bolts and wants me to chase him. Then he comes back and wants to do it again. For years I'd try to get him to drop the ball and he'd just stand there growling. "Norman. Come on." Only took me about a decade to realise he didn't want to drop it - he wanted the fight. He's an expert at grrrrr. Arguably he's never lost.&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.amazonaws.com%2Fuploads%2Farticles%2F3nurl1w2oqzrcn2nzwxy.webp" 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.amazonaws.com%2Fuploads%2Farticles%2F3nurl1w2oqzrcn2nzwxy.webp" alt="Normi standing at the oval, looking directly at the camera, ready to go" width="800" height="1066"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Between rounds of grrrrr, while Normi's catching his breath (or more often, while he's pretending he can't hear me calling him back), I pull out my phone and check Telegram. There's usually a response from Claude Code waiting in one of my working directory chats. I read it, record a quick voice note with the next task, put my phone away, and go back to playing.&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.amazonaws.com%2Fuploads%2Farticles%2Fp908lgx2wzs4ma9i72nu.webp" 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.amazonaws.com%2Fuploads%2Farticles%2Fp908lgx2wzs4ma9i72nu.webp" alt="Voice note transcription in Telegram - a 7-second voice note transcribed by Groq and sent to Claude Code as a task" width="590" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Five or ten minutes later I check again. Claude Code has been working the whole time. I might have three or four different working directory chats going - one time I had five or six running in parallel, testing bug fixes in the Untether repo while making website updates and working on various other projects at the same time.&lt;/p&gt;

&lt;p&gt;After thirty or forty minutes, Normi and I are both sitting on the grass, absolutely pooped, having a drink. And Claude Code is still working away on the VPS, finishing up the last task.&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.amazonaws.com%2Fuploads%2Farticles%2Fitab41ldmyto70uf0ijh.webp" 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.amazonaws.com%2Fuploads%2Farticles%2Fitab41ldmyto70uf0ijh.webp" alt="Normi resting on the grass at the oval after playing - tongue out, absolutely pooped" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  This is not work-life balance
&lt;/h3&gt;

&lt;p&gt;Look, I think work-life balance is bullshit. I've never been able to find it. And I don't think most solo devs have either.&lt;/p&gt;

&lt;p&gt;But I can go for two or three walks a day with Normi, in the sun or the rain, and be outside for hours. I don't have to sit in traffic. I don't have to be in some office. I don't have to sit through meetings that could have been emails. I can play grrrrr at the oval and check in on my coding agents between rounds. I can be at Coles, on the bus, in bed at 6am. The VPS doesn't care. Telegram doesn't care. Claude Code keeps running whether I'm watching or not.&lt;/p&gt;

&lt;p&gt;That's living, I guess. To me, anyway.&lt;/p&gt;

&lt;h3&gt;
  
  
  The thinking loop
&lt;/h3&gt;

&lt;p&gt;There's a Stanford study that found walking increases creative output by 60% compared to sitting (&lt;a href="https://news.stanford.edu/stories/2014/04/walking-vs-sitting-042414" rel="noopener noreferrer"&gt;Oppezzo &amp;amp; Schwartz, 2014&lt;/a&gt;, 176 college students across four experiments). I'm not claiming causation for my own work. But I notice it.&lt;/p&gt;

&lt;p&gt;Something about walking with a reasonably clear mind, being outside with Normi, not staring at code - my best task descriptions come out on walks, not at my desk. I think it's because I'm not lost in implementation details. I'm thinking about what I actually want.&lt;/p&gt;

&lt;p&gt;Voice-to-text amplifies this. I'm a talker. I process by talking things through, often just to crystallise something for myself. The walk gives me space to think clearly, I talk it through as a voice note, and Claude Code rearranges my waffle into something structured. The loop works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What doesn't work well?
&lt;/h2&gt;

&lt;p&gt;Honestly, most of it works. But there are a few things worth knowing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Voice notes are great for intent and convenience, but not always good for precision.&lt;/strong&gt; If I say "the auth middleware needs splitting into two separate functions, keep the tests passing" - that works brilliantly. Dictating actual code syntax is painful no matter how good the transcription is. The trick is prompting the same way you would at your laptop - be descriptive, give context, explain what you want and why. As long as you do that, voice works just as well as typing. The issue is never the voice input. It's being vague.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screen size.&lt;/strong&gt; Reading a 200-line diff on a phone screen isn't great, I'll be honest. I'll skim the progress updates on a walk, approve or deny the obvious stuff, and do a proper review when I get home to a real screen. The agent handles the straightforward decisions - formatting, renames, clear-cut logic changes - and I handle the ones that need thought.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mobile signal.&lt;/strong&gt; This is actually one of the best bits about the whole setup. Your AI agents run on the VPS, not your phone. If you lose mobile coverage walking through a dead zone or duck into a building with no signal, the agents keep working. They don't care that your phone went quiet - they're on a server in Germany. When you find coverage again, all the updates are sitting there in Telegram waiting for you. Nothing stalls, nothing breaks. Telegram queues messages beautifully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deep architecture sessions.&lt;/strong&gt; If I need to trace through a complicated chain of files or make big architectural decisions, I'll sometimes save that for home with a proper screen. But even then, I've been surprised how far I can get by just being clear in my voice prompts: "Create a plan and save it. Don't implement yet. Let's discuss first." Going back and forth on plans through voice notes genuinely works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transcription and mumbling.&lt;/strong&gt; If I'm not speaking clearly, or doing my ADHD thing where I jump between thoughts mid-sentence, transcription quality drops. Speak clearly and you'll be fine. Mumble and you'll confuse everyone, including the AI.&lt;/p&gt;

&lt;p&gt;The big thing for me is that using Untether means I actually get to enjoy the walks more, not less. I'm not hunched over a tiny keyboard slowly typing out messages. A voice note takes seven seconds, then I'm back to playing with Normi. The rest of the time I'm genuinely present - outside, moving, not staring at a screen. That's the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;If this workflow sounds useful, here's how to try it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install Untether&lt;/strong&gt;: &lt;code&gt;uv tool install untether&lt;/code&gt; (requires Python 3.12+)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create a Telegram bot&lt;/strong&gt; via &lt;a href="https://t.me/BotFather" rel="noopener noreferrer"&gt;@BotFather&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure&lt;/strong&gt; &lt;code&gt;untether.toml&lt;/code&gt; with your bot token and Claude Code path&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Register your projects&lt;/strong&gt;: &lt;code&gt;untether init &amp;lt;shortname&amp;gt;&lt;/code&gt; in each repo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send your first task&lt;/strong&gt; as a text message or voice note&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You don't need a VPS. You can run Untether on your laptop. But if you want the "my phone can die and work continues" setup, a cheap VPS does the trick. I use Hetzner.&lt;/p&gt;

&lt;p&gt;Untether is free and open source: &lt;a href="https://github.com/littlebearapps/untether" rel="noopener noreferrer"&gt;github.com/littlebearapps/untether&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Normi and I will be at the oval either way. Might as well ship something while we're there.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can I use Untether with AI coding agents other than Claude Code?&lt;/strong&gt;&lt;br&gt;
Yes. Untether supports Claude Code, Codex, OpenCode, and Pi today. Gemini CLI and Amp support are coming.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Untether work on Android?&lt;/strong&gt;&lt;br&gt;
Yes. It works through Telegram, which runs on iOS, Android, desktop, and web. The phone doesn't matter, only the Telegram app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Untether free?&lt;/strong&gt;&lt;br&gt;
Yes. It's open source (MIT licence), free to install and use. You'll need your own API keys for the AI coding agent you connect to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How accurate is voice transcription for coding tasks?&lt;/strong&gt;&lt;br&gt;
Good enough for natural language task descriptions. Groq's Whisper-compatible transcription handles conversational English well. Technical terms occasionally get mangled, but Claude Code usually infers the correct intent from context. Speak clearly and you'll be fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the AI agent keep running if my phone dies?&lt;/strong&gt;&lt;br&gt;
Yes, if you're running on a VPS. The agent runs on the server, not your phone. Telegram just delivers the messages. When your phone comes back online, you'll see everything that happened while you were offline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does Untether compare to Claude Code Channels?&lt;/strong&gt;&lt;br&gt;
Channels launched in March 2026 and adds Telegram and Discord support for Claude Code through MCP. It's Claude-only and text-only. It still pauses at the terminal for permission prompts. Untether supports six engines, accepts voice notes, and handles permissions with inline Telegram buttons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use voice notes to write code with an AI agent?&lt;/strong&gt;&lt;br&gt;
Yes. Untether transcribes Telegram voice notes using &lt;a href="https://groq.com" rel="noopener noreferrer"&gt;Groq&lt;/a&gt;'s Whisper-compatible endpoint, then passes the text to the AI coding agent as a task. Speaking is roughly 4x faster than typing on a phone (150 WPM vs 40 WPM).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What server specs does Untether need?&lt;/strong&gt;&lt;br&gt;
Minimal. Untether itself is lightweight Python. The AI agent does the heavy lifting. A basic VPS like a Hetzner CX22 is more than enough. You can also run it on your laptop if you don't need the always-on setup.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>telegram</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My $5/month Cloudflare bill hit $4,868 because of an infinite loop</title>
      <dc:creator>Nathan Schram</dc:creator>
      <pubDate>Tue, 31 Mar 2026 05:11:24 +0000</pubDate>
      <link>https://dev.to/nathanschram/my-5month-cloudflare-bill-hit-4868-because-of-an-infinite-loop-13g8</link>
      <guid>https://dev.to/nathanschram/my-5month-cloudflare-bill-hit-4868-because-of-an-infinite-loop-13g8</guid>
      <description>&lt;p&gt;The invoice said $4,868.00. My Cloudflare account usually costs $5 a month.&lt;/p&gt;

&lt;p&gt;In January 2026, two bugs in two different workers wrote billions of rows to D1. I'm a &lt;a href="https://littlebearapps.com/about" rel="noopener noreferrer"&gt;solo developer&lt;/a&gt; on the Workers Paid plan. I don't have a billing department. I have a credit card and a vague hope that nothing goes catastrophically wrong. That hope cost me 18 days of stress, a near-suspension of my entire account, and a spam folder I should have been checking more carefully.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Two code bugs wrote 4.83 billion rows to Cloudflare D1 in January 2026, generating a ~$4,868 overage on a $5/month account. After 18 days and four escalation channels, Cloudflare waived the full $4,586.64 invoice. I then built a three-layer circuit breaker system so it can't happen again.&lt;/p&gt;
&lt;/blockquote&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.amazonaws.com%2Fuploads%2Farticles%2Fi22ra5px1zmvkrak32px.webp" 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.amazonaws.com%2Fuploads%2Farticles%2Fi22ra5px1zmvkrak32px.webp" alt="Bar chart showing D1 write operations spiking to 1.42 billion in January 2026, with two colour-coded bug periods highlighted" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong with D1?
&lt;/h2&gt;

&lt;p&gt;Two separate bugs, two separate projects, both writing to D1 without anything to stop them.&lt;/p&gt;

&lt;h3&gt;
  
  
  The embedding worker that couldn't stop writing
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://littlebearapps.com/builds/semantic-librarian/" rel="noopener noreferrer"&gt;Semantic Librarian&lt;/a&gt; is my Australian heritage records project. 1.4 million historical records from the National Library of Australia's Trove archive, searchable via Workers AI embeddings stored in Vectorize, backed by a D1 database. The worker runs on a cron schedule, processing documents in batches: fetch a batch of records, generate embeddings through Workers AI, write the vectors and metadata to D1, move to the next batch.&lt;/p&gt;

&lt;p&gt;The bug was in the "move to the next batch" part. There was no deduplication check. The worker would process a batch of documents, write the embeddings, and on the next cron tick, process the exact same batch again. No offset tracking. No "already processed" flag. Every cycle wrote the same records. And the next cycle wrote them again.&lt;/p&gt;

&lt;p&gt;For four days, from January 11 to 14, the worker ran on autopilot while I was focused on building other things. I wasn't watching the Cloudflare dashboard. Why would I? The worker was deployed, running on a cron, no errors in the logs.&lt;/p&gt;

&lt;p&gt;3.45 billion D1 writes in four days. Here's how that breaks down:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;D1 Writes&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jan 11&lt;/td&gt;
&lt;td&gt;479,873,853&lt;/td&gt;
&lt;td&gt;$479.91&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jan 12&lt;/td&gt;
&lt;td&gt;1,335,107,674&lt;/td&gt;
&lt;td&gt;$1,259.99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jan 13&lt;/td&gt;
&lt;td&gt;1,424,638,592&lt;/td&gt;
&lt;td&gt;$1,411.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jan 14&lt;/td&gt;
&lt;td&gt;282,900,856&lt;/td&gt;
&lt;td&gt;$282.65&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Peak day was January 13: 1.42 billion writes in 24 hours. Storage spiked to 10 GB. After I killed the worker and cleaned up, it dropped to 2.4 GB, confirming most of it was duplicate data.&lt;/p&gt;

&lt;p&gt;I didn't notice for four days because the worker was running silently. No errors. No alerts from Cloudflare. No email saying "hey, your D1 writes are 7,000x above normal." Just a worker doing exactly what I told it to do, over and over and over.&lt;/p&gt;

&lt;h3&gt;
  
  
  The harvester without ON CONFLICT
&lt;/h3&gt;

&lt;p&gt;A second project, a GitHub data harvesting tool I was deploying for the first time, had a different version of the same problem. During the initial data seeding phase in early January (Jan 1-4), each scan cycle re-inserted existing records instead of updating them. The INSERT statements had no &lt;code&gt;ON CONFLICT&lt;/code&gt; clause. So every time the harvester ran, it tried to insert records that already existed, and D1 happily accepted every one. About 910 million redundant writes in four days.&lt;/p&gt;

&lt;p&gt;I found this one faster and fixed it on January 5 with proper &lt;code&gt;ON CONFLICT DO UPDATE&lt;/code&gt; clauses. The Semantic Librarian bug started six days later.&lt;/p&gt;

&lt;p&gt;Between the two bugs: 4.83 billion D1 writes in January. To put that in perspective, my normal usage across all 9 databases is maybe 200 writes per hour. The D1 pricing page says $1 per million rows written beyond the 50 million included. 4.83 billion rows at that rate is $4,779 in write charges alone, plus storage, requests, and AI inference costs that pushed the total to $4,868.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you fight a $4,868 bill on a $5/month account?
&lt;/h2&gt;

&lt;p&gt;Slowly. Across multiple channels. With a detailed audit document and more patience than I thought I had.&lt;/p&gt;

&lt;h3&gt;
  
  
  7 days of silence
&lt;/h3&gt;

&lt;p&gt;On February 1, I submitted support ticket #01953111. I didn't just write "please waive this." I attached a full usage audit as a PDF: daily D1 write counts broken down by project, spike period analysis with exact dates and row counts, root cause analysis for each bug, and a list of every fix and architectural improvement I'd deployed to prevent recurrence.&lt;/p&gt;

&lt;p&gt;I wanted to make it easy for whoever reviewed it. Here's exactly what happened, here's exactly why, and here's what I built to make sure it doesn't happen again. If you're going to ask a company to waive $4,868, you should come prepared.&lt;/p&gt;

&lt;p&gt;No response by February 7. Six days. I sent a follow-up asking if it had been assigned to the billing team. Nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Finding the right human
&lt;/h3&gt;

&lt;p&gt;On February 7, I posted to the &lt;a href="https://community.cloudflare.com/t/billing-ticket-01953111-d1-write-overage-from-code-bug-seeking-courtesy-credit/889957" rel="noopener noreferrer"&gt;Cloudflare Community Forum&lt;/a&gt;. A CF Community MVP called neiljay responded quickly and pointed me to a post by cherryjimbo (CF MVP '23-'26) who had shared a direct email for the Head of Billing.&lt;/p&gt;

&lt;p&gt;On February 8, I emailed Dmitry Alexeenko (Head of Billing) directly, referencing my ticket number and cherryjimbo's referral.&lt;/p&gt;

&lt;p&gt;Then I waited.&lt;/p&gt;

&lt;p&gt;Marta from support had actually replied on February 11. The case had been raised with Engineering and was on temporary hold. That should have been reassuring.&lt;/p&gt;

&lt;h3&gt;
  
  
  The spam folder that almost killed my account
&lt;/h3&gt;

&lt;p&gt;On February 18, I found an automated email in my junk folder. It was dated February 17. Cloudflare's billing system had sent a suspension warning: paid services would be disabled for the unpaid invoice. I ran a full account audit. R2 object storage and Analytics Engine were already disabled. My 34 workers were still running, the 8 D1 databases were still accessible, KV and Queues were fine. Partial suspension, not full. Not yet.&lt;/p&gt;

&lt;p&gt;The human support team had my case on hold with Engineering, actively working on it. The automated billing system operated on its own timeline and didn't check whether a human being was already handling the dispute. Two parallel systems, zero coordination between them.&lt;/p&gt;

&lt;p&gt;I sent urgent follow-ups to both Marta and Dmitry. Dmitry's autoresponder came back: the Portugal office was closed for Carnival, and he'd included his mobile number for urgent matters. I texted him. That same day, I &lt;a href="https://www.reddit.com/r/CloudFlare/comments/1r7skeq/support_said_my_48k_billing_dispute_was_on_hold/" rel="noopener noreferrer"&gt;posted to Reddit r/CloudFlare&lt;/a&gt;: "Support said my $4.8k billing dispute was on hold, but the automated system just suspended me anyway."&lt;/p&gt;

&lt;p&gt;By that evening, four escalation channels were active: the original support ticket, the community forum post, the direct email to Dmitry, and the Reddit post. Within hours, things moved. Dmitry responded despite the holiday. Akash Das, Director of Customer Support, personally took the case. He'd read my audit document and accurately identified both root causes: the infinite write loop in the heritage records worker and the missing conflict handling in the data harvesting tool. The case was upgraded to urgent priority.&lt;/p&gt;

&lt;h2&gt;
  
  
  Did Cloudflare do the right thing?
&lt;/h2&gt;

&lt;p&gt;Yes. Daniel Anselmo (Technical Support Shift Engineer) confirmed the full waiver on February 19: $4,586.64, invoice IN 56608827. Account unlocked. All services restored. I re-subscribed to the Workers Paid plan and verified everything: 34 workers running, 8 D1 databases accessible, KV, Queues, R2, Analytics Engine all back online.&lt;/p&gt;

&lt;p&gt;The $4,586.64 was the actual invoice total, slightly different from my $4,868 estimate because of how Cloudflare calculates final billing. Either way, the full amount was waived as a one-time courtesy.&lt;/p&gt;

&lt;p&gt;18 days from first ticket to resolution. That feels long when you're living it, and fair when you look back at it. I want to credit the specific people who made the resolution happen: Akash Das (Director of Customer Support) for personally reviewing the case and identifying both technical root causes accurately from my audit. Dmitry Alexeenko (Head of Billing) for responding and escalating despite a public holiday in Portugal. neiljay and cherryjimbo on the Cloudflare Community Forum for pointing me to the right contact when the ticket queue was silent. And Daniel Anselmo for closing it out cleanly.&lt;/p&gt;

&lt;p&gt;My critique isn't of the people. The people were good. It's of the gap between the human support process (which was thorough once it engaged) and the automated billing system (which nearly suspended my entire account while that support was actively investigating my case). Those two systems don't talk to each other fast enough. A billing dispute that's actively being reviewed by the Director of Support should probably not trigger an automated suspension at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why doesn't D1 have write rate limits?
&lt;/h2&gt;

&lt;p&gt;Mine isn't an isolated case.&lt;/p&gt;

&lt;p&gt;In 2025, &lt;a href="https://www.ofsecman.io/post/postmortem-5-000-incident-in-10-seconds-due-to-cloudflare-d1" rel="noopener noreferrer"&gt;ofsecman.io documented a $5,000+ D1 overage&lt;/a&gt; caused by a missing &lt;code&gt;WHERE&lt;/code&gt; clause in an update statement. A single-row update became a full-table update on every incoming request. Over $5,000 in under 10 seconds. Their conclusion was blunt: "Don't ever use Cloudflare D1 as a Database."&lt;/p&gt;

&lt;p&gt;On the Cloudflare Community Forum, &lt;a href="https://community.cloudflare.com/t/d1-database-index-problem-cost-3200/780753" rel="noopener noreferrer"&gt;a first-time database user reported a $3,200 bill&lt;/a&gt; because they didn't set up an index. Their credit card was overdrawn before they noticed anything. "Cloudflare did not give me any notice or reminder."&lt;/p&gt;

&lt;p&gt;Three different bugs, three different accounts, same outcome. D1 charges per row written with no caps, no write rate limits, and no billing alerts granular enough for D1 write operations specifically. Cloudflare's billing notifications exist, but they're not designed to catch a worker writing a billion rows in 24 hours.&lt;/p&gt;

&lt;p&gt;Scale-to-zero billing is D1's selling point. You pay nothing when your database is idle. That's genuinely great for solo developers and small projects, and it's why I chose Cloudflare's stack in the first place. Scale-to-zero also means scale-to-infinity when a bug amplifies, because the same billing model that charges you nothing at rest charges you per operation at scale with no ceiling.&lt;/p&gt;

&lt;p&gt;D1 hit general availability in &lt;a href="https://blog.cloudflare.com/d1-ga-update/" rel="noopener noreferrer"&gt;September 2024&lt;/a&gt;. The billing model shipped before the billing safeguards did. This is a platform maturity gap, not malice, and I expect Cloudflare will address it. It's a gap that has cost at least three people documented real money though, and probably more who paid the bill without writing about it.&lt;/p&gt;

&lt;p&gt;I'm not saying don't use D1. I still use it across &lt;a href="https://littlebearapps.com/projects" rel="noopener noreferrer"&gt;9 databases for multiple projects&lt;/a&gt;. I'm saying don't use it without your own circuit breakers, because the platform doesn't have them yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did I build to prevent this from happening again?
&lt;/h2&gt;

&lt;p&gt;After the invoice was waived, I spent a week doing nothing except cost safety. I had been building features. Now I was building guardrails. Nine improvements across three tiers of priority, all shipped and deployed. Tier 1 was critical one-line fixes I could push immediately. Tier 2 was the anomaly detection that would have caught the January incident within an hour. Tier 3 was the longer-term monitoring improvements.&lt;/p&gt;

&lt;p&gt;Everything described here lives in an infrastructure SDK I built after the incident. Two TypeScript packages: a consumer SDK that goes into each worker, and an admin backend with a monitoring dashboard and the telemetry pipeline that feeds it. The admin side runs on Cloudflare Pages, so I can check budget state from my phone - a meaningful upgrade from my previous approach of noticing the invoice a month later.&lt;/p&gt;

&lt;p&gt;I open-sourced it because three people hitting the same $4,000+ wall suggests this isn't just my problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update (March 2026):&lt;/strong&gt; The original circuit breaker infrastructure described above (Platform SDKs) worked but was too complex - 10+ workers, 61 D1 migrations, cross-account HMAC forwarding. I've since replaced it with &lt;a href="https://littlebearapps.com/builds/cf-monitor/" rel="noopener noreferrer"&gt;CF Monitor&lt;/a&gt;, a much simpler rewrite: one worker per account, Analytics Engine + KV only, zero D1. It's open source and available as an npm package (&lt;code&gt;@littlebearapps/cf-monitor&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Each worker imports the consumer SDK, wraps its environment bindings on startup, and the tracking happens automatically. No per-project instrumentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three layers of circuit breakers
&lt;/h3&gt;

&lt;p&gt;The circuit breakers work at three levels of granularity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feature-level&lt;/strong&gt; is the most precise. Each distinct function in each project gets its own budget. A GitHub scanner, a document embedder, an API endpoint - each has a defined daily limit for D1 writes, KV operations, Workers AI neurons, whatever resources it consumes. If the document embedder goes haywire, it gets disabled. The GitHub scanner keeps running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project-level&lt;/strong&gt; aggregates all features for a project. Individual features might stay within their budgets while the project total is too high.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Global emergency stop&lt;/strong&gt; is the nuclear option. It kills everything across all projects immediately. I haven't had to use it. I hope I never do.&lt;/p&gt;

&lt;p&gt;Each level enforces progressively: 70% triggers a Slack warning, 90% triggers a critical alert, 100% auto-disables the feature. The breakers auto-reset after 1 hour via KV TTL, so a tripped feature doesn't sit dead until I manually re-enable it at 3am.&lt;/p&gt;

&lt;h3&gt;
  
  
  Counting writes before they become a bill
&lt;/h3&gt;

&lt;p&gt;The tracking can't use D1 writes to count D1 writes. That would be self-defeating. If my monitoring system writes usage data to D1, it's consuming the exact resource it's trying to protect. The January incident itself proved this: my original monitoring infrastructure was writing ~200 rows per hour to D1 just to track usage across all projects. That's not a lot in isolation, but the principle is wrong.&lt;/p&gt;

&lt;p&gt;The consumer SDK wraps your Cloudflare environment bindings with proxies that automatically count every operation. D1 reads and writes, KV gets and puts, R2 uploads, Workers AI inference calls, Vectorize queries - all tracked transparently. When your worker calls &lt;code&gt;env.DB.prepare(...).run()&lt;/code&gt;, the proxy intercepts it, increments a counter, and forwards the call. Your code doesn't change. You call &lt;code&gt;createTrackedEnv(env)&lt;/code&gt; at startup and the counting happens behind the scenes.&lt;/p&gt;

&lt;p&gt;The counters get flushed to &lt;a href="https://developers.cloudflare.com/analytics/analytics-engine/" rel="noopener noreferrer"&gt;Analytics Engine&lt;/a&gt; via a Cloudflare Queue. Analytics Engine is free for the first 25 million data points per month and it's designed for exactly this kind of high-volume telemetry. Zero D1 write overhead for the tracking itself.&lt;/p&gt;

&lt;p&gt;The budget checker queries Analytics Engine roughly every 30 seconds, sums up recent writes per feature, and compares them against the budget defined in a YAML config file. If a feature crosses 70%, Slack warning. 90%, critical alert. 100%, the feature's circuit breaker trips and the SDK starts rejecting operations for that feature until the breaker resets.&lt;/p&gt;

&lt;p&gt;Detection latency: about 30 seconds from a write happening to the circuit breaker evaluating it. In January, my runaway worker ran for four days. Now it would run for about 30 seconds before getting shut down automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  The monitoring that ties it together
&lt;/h3&gt;

&lt;p&gt;On top of the circuit breakers, the nine hardening improvements across three tiers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Auto-reset circuit breakers via KV TTL (1-hour expiry, no manual intervention needed)&lt;/li&gt;
&lt;li&gt;Workers AI cost monitoring added to the sentinel (previously untracked)&lt;/li&gt;
&lt;li&gt;Investigation SQL column fix (the monitoring was querying the wrong column name)&lt;/li&gt;
&lt;li&gt;Hourly D1 write anomaly detection using a 168-hour rolling window with 3-sigma threshold&lt;/li&gt;
&lt;li&gt;Per-project anomaly detection, not just account-wide (previously only checked totals)&lt;/li&gt;
&lt;li&gt;Budget warning thresholds at 70% and 90% with Slack alerts and 1-hour deduplication&lt;/li&gt;
&lt;li&gt;Monthly budget tracking with progressive alerts at 70%, 90%, and exceeded&lt;/li&gt;
&lt;li&gt;Batch resource snapshot inserts, reduced from ~200 individual D1 writes per hour to ~8 batch transactions&lt;/li&gt;
&lt;li&gt;Six missing budget overrides for features that were falling back to overly generous defaults&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All nine shipped and deployed within a week. The batch insert change alone (item 8) cut monitoring D1 write overhead by 96%, from ~200 individual writes per hour down to ~8 batch transactions. The hourly anomaly detection (item 4) would have caught the January spike within its first hour: 480 million writes in a single day is roughly 15,000 standard deviations above my normal baseline of ~200 writes per hour. The 3-sigma threshold would have tripped before the first hour was up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What pattern do serverless billing failures share?
&lt;/h2&gt;

&lt;p&gt;Three cases. Same billing model. Same outcome.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;My infinite loop&lt;/th&gt;
&lt;th&gt;ofsecman.io&lt;/th&gt;
&lt;th&gt;CF Community&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Root cause&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Missing deduplication&lt;/td&gt;
&lt;td&gt;Missing WHERE clause&lt;/td&gt;
&lt;td&gt;Missing index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Time to cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4 days&lt;/td&gt;
&lt;td&gt;10 seconds&lt;/td&gt;
&lt;td&gt;Days (unclear)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bill&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$4,868&lt;/td&gt;
&lt;td&gt;$5,000+&lt;/td&gt;
&lt;td&gt;$3,200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Warning from CF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The common denominator isn't the code. Bugs happen. I wrote about this in &lt;a href="https://littlebearapps.com/blog/dogfooding-bugs-tests-cant-find" rel="noopener noreferrer"&gt;my last post on dogfooding&lt;/a&gt;: the bugs that matter most are the ones that live in the gaps between states, not in the states themselves. A worker that runs correctly once will also run correctly a billion times. The bug isn't in the execution; it's in the assumption that anything would stop it.&lt;/p&gt;

&lt;p&gt;The common denominator is that D1's billing model has no safety net between "working correctly" and "catastrophic overage." No write rate limit. No anomaly detection. No automatic pause when usage spikes 10,000x above normal. The billing system faithfully counts every row, generates an invoice, and sends it to your credit card.&lt;/p&gt;

&lt;p&gt;Every serverless database with per-operation billing has this exposure. D1 isn't unique in charging per write. Most managed databases give you some combination of connection pools, query timeouts, billing caps, or at minimum a usage alert that fires before you hit four figures. D1 currently offers none of those for write operations.&lt;/p&gt;

&lt;p&gt;If you're building on D1 in production, build your own circuit breakers. I did. The infrastructure described in this post took about a week to build and deploy. The January invoice would have taken me considerably longer to pay off. That's a pretty clear cost-benefit calculation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common questions about D1 billing and cost protection
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can you set a billing cap on Cloudflare D1?
&lt;/h3&gt;

&lt;p&gt;No. As of March 2026, Cloudflare doesn't offer a hard billing cap for D1 write operations. You can set up billing notifications, but they're not granular enough to catch a worker writing a billion rows overnight. Application-level circuit breakers are currently the only option.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you detect a runaway worker before the bill arrives?
&lt;/h3&gt;

&lt;p&gt;Monitor D1 write counts at sub-hourly intervals. I use Analytics Engine (free tier, 25 million events per month) to track every D1 write via proxied environment bindings, with a budget checker that evaluates every 30 seconds. Anomaly detection with a 168-hour rolling window catches spikes that exceed 3 standard deviations from normal. The whole system adds zero D1 write overhead because telemetry goes through Analytics Engine, not D1.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this just a D1 problem?
&lt;/h3&gt;

&lt;p&gt;The billing exposure exists on any serverless platform with per-operation pricing and no rate limits. D1 is the most visible example right now because it's relatively new (GA 2024) and write-heavy workloads can accumulate cost fast. DynamoDB, Firestore, and PlanetScale all have their own versions of this risk, though most offer billing alerts or auto-scaling limits that D1 currently lacks.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The timeline, dollar amounts, and technical details in this post are reconstructed from &lt;a href="https://community.cloudflare.com/t/billing-ticket-01953111-d1-write-overage-from-code-bug-seeking-courtesy-credit/889957" rel="noopener noreferrer"&gt;support ticket #01953111&lt;/a&gt;, &lt;a href="https://www.reddit.com/r/CloudFlare/comments/1r7skeq/" rel="noopener noreferrer"&gt;Reddit r/CloudFlare&lt;/a&gt;, Cloudflare dashboard data, and internal audit documents.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>serverless</category>
      <category>buildinpublic</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Dogfooding found 22 bugs my 1,548 tests missed</title>
      <dc:creator>Nathan Schram</dc:creator>
      <pubDate>Thu, 19 Mar 2026 04:28:20 +0000</pubDate>
      <link>https://dev.to/nathanschram/dogfooding-found-22-bugs-my-1548-tests-missed-31m4</link>
      <guid>https://dev.to/nathanschram/dogfooding-found-22-bugs-my-1548-tests-missed-31m4</guid>
      <description>&lt;p&gt;Last week I found 86 orphaned processes eating 10.3 GB of RAM on my VPS. The week before that, my stall monitor fired because I went for a walk. And my own documentation tool told me my docs were stale.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real use of three open-source tools found 22 bugs that 1,548 automated tests missed.&lt;/li&gt;
&lt;li&gt;Bugs cluster in two categories: resource accumulation over time, and gaps between "works" and "works for me".&lt;/li&gt;
&lt;li&gt;Test suites check states. Dogfooding finds the transitions between them.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;None of these would show up in a test suite. I found them because I actually use my own tools - not as a testing practice, just because they solve problems I have. Test suites tell you if something works. Using your own product tells you if it's any good. Those are different questions with different answers. Joel Spolsky &lt;a href="https://www.joelonsoftware.com/2001/05/05/what-is-the-work-of-dogs-in-this-country/" rel="noopener noreferrer"&gt;described this gap&lt;/a&gt; twenty-five years ago - he found 45 bugs in one Sunday afternoon of actually using CityDesk to run his blog. "All the testing we did, meticulously pulling down every menu and seeing if it worked right, didn't uncover the showstoppers."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://en.wikipedia.org/wiki/Eating_your_own_dog_food" rel="noopener noreferrer"&gt;Dogfooding&lt;/a&gt;&lt;/strong&gt; is the practice of using your own products as your primary tools - not as a scheduled testing exercise, but as part of how you work. &lt;strong&gt;&lt;a href="https://github.com/littlebearapps/untether" rel="noopener noreferrer"&gt;Untether&lt;/a&gt;&lt;/strong&gt; is a Telegram bridge for AI coding agents. &lt;strong&gt;&lt;a href="https://github.com/littlebearapps/pitchdocs" rel="noopener noreferrer"&gt;PitchDocs&lt;/a&gt;&lt;/strong&gt; is a documentation generator for code repositories. &lt;strong&gt;&lt;a href="https://github.com/littlebearapps/outlook-assistant" rel="noopener noreferrer"&gt;Outlook Assistant&lt;/a&gt;&lt;/strong&gt; is an &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt; that gives AI assistants access to Outlook email, calendar, and contacts.&lt;/p&gt;

&lt;p&gt;I run three open-source tools that I built for myself. Untether and PitchDocs I use every day. Outlook Assistant I pull out when the job calls for it - digging through inbox, sent, archived, and deleted folders to find invoices and receipts for tax time, or trawling through calendar events across linked calendars. Not daily, but when I do use it, I use it hard. And honestly, the bugs I find through real use are the ones that matter most - the ones your users would hit first.&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.amazonaws.com%2Fuploads%2Farticles%2Fskpdpeihwdev2d0bfk6d.webp" 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.amazonaws.com%2Fuploads%2Farticles%2Fskpdpeihwdev2d0bfk6d.webp" alt="Terminal listing MCP server processes across Claude Code sessions - each session spawns about 14 servers, consuming over 100 MB each" width="800" height="643"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What does daily Untether use actually find?
&lt;/h2&gt;

&lt;p&gt;In 3 days of daily use, I shipped 8 releases and found bugs that 1,548 automated tests missed. The bugs that matter live in the transitions between states - sleeping and waking, busy and stuck, present and away.&lt;/p&gt;

&lt;p&gt;I use &lt;a href="https://github.com/littlebearapps/untether" rel="noopener noreferrer"&gt;Untether&lt;/a&gt; for basically everything. Voice notes from the couch, approving file changes while making coffee, kicking off test runs from my phone. It's not a tool I built and then test occasionally - it's how I do my job.&lt;/p&gt;

&lt;p&gt;Last week I noticed a 41-minute stall in one of my chats had gone completely undetected. A wrangler tail command got stuck, no events were flowing, and Untether just sat there silently. No warning, nothing. I only caught it because I was actually waiting for a result and it never came.&lt;/p&gt;

&lt;p&gt;So I &lt;a href="https://github.com/littlebearapps/untether/issues/92" rel="noopener noreferrer"&gt;built a stall monitor&lt;/a&gt;. Seemed simple enough - if no events arrive for 5 minutes, send me a Telegram warning. v0.34.0, shipped, done.&lt;/p&gt;

&lt;p&gt;Then the real education started.&lt;/p&gt;

&lt;h3&gt;
  
  
  The stall monitor that couldn't tell stuck from busy
&lt;/h3&gt;

&lt;p&gt;I ran pytest through Untether and the stall monitor fired 3 times in 10 minutes. The process was alive and working fine, but it just wasn't emitting progress events during tool execution. From the monitor's perspective, silence meant "stuck". In reality, silence meant "busy running your tests".&lt;/p&gt;

&lt;p&gt;I had to add /proc diagnostics - CPU usage, memory, TCP connections, file descriptors, child processes - so the monitor could tell the difference between "stuck" and "busy doing something useful." That became v0.34.1. Along with a liveness watchdog, progressive warnings, and a JsonlStreamState tracker that remembers recent events in a ring buffer. The first version couldn't tell silence from activity.&lt;/p&gt;

&lt;p&gt;Then I closed my laptop overnight. Came back the next morning to find the &lt;a href="https://github.com/littlebearapps/untether/issues/99" rel="noopener noreferrer"&gt;stall monitor stuck in an infinite loop&lt;/a&gt;. The subprocess had died when the laptop went to sleep, but the monitor kept firing "No progress" warnings every 3 minutes - 7 of them stacked up by the time I opened the lid. Each one showing &lt;code&gt;pid=None, process_alive=None&lt;/code&gt; because it couldn't even find the process. It just kept warning about a ghost.&lt;/p&gt;

&lt;p&gt;So I built dead process detection, a zombie warning cap (3 warnings before auto-cancel, absolute cap at 10), and early PID threading so the monitor knows about the subprocess from spawn, not from the first event. I also made /cancel work as a standalone command without having to reply to the progress message - because on mobile, finding a specific message to reply to when your screen is full of stall warnings is not fun. v0.34.2.&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.amazonaws.com%2Fuploads%2Farticles%2Fnke4seos74b55utjggh2.webp" 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.amazonaws.com%2Fuploads%2Farticles%2Fnke4seos74b55utjggh2.webp" alt="Telegram chat showing 6 stacked stall monitor warnings escalating from 5 to 20 minutes, with a failed /cancel attempt" width="590" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  When "idle" means walking Normi
&lt;/h3&gt;

&lt;p&gt;Then I went for a walk.&lt;/p&gt;

&lt;p&gt;Claude was waiting for approval on a file change. I had the inline keyboard showing on my phone - Approve, Deny, Skip - but I was out walking Normi and didn't reply for about 6 minutes. Stall monitor fired. "No progress for 6 min."&lt;/p&gt;

&lt;p&gt;That's not a stall. I was just away. The difference between "the process is stuck" and "the human hasn't replied yet" is obvious to a person but invisible to a monitor that only watches event timestamps. Added approval-aware thresholds - 30 minutes when there's an inline keyboard showing, 5 minutes normally.&lt;/p&gt;

&lt;p&gt;A long pytest run triggered it again. A 10-minute test suite is not a stall. Built a &lt;a href="https://github.com/littlebearapps/untether/issues/105" rel="noopener noreferrer"&gt;three-tier threshold system&lt;/a&gt;: 5 minutes for normal operation, 10 minutes during active tool execution, 30 minutes during approval waits. v0.34.3.&lt;/p&gt;

&lt;p&gt;Four releases. One "simple" feature. Each release driven by a real moment where I was actually using the tool and it got something wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  86 orphaned processes and 10.3 GB of RAM
&lt;/h3&gt;

&lt;p&gt;While chasing all of this down, I noticed my VPS was getting sluggish. Telegram messages were slow, progress updates felt laggy. I found &lt;a href="https://github.com/littlebearapps/untether/issues/88" rel="noopener noreferrer"&gt;86 orphaned MCP server processes&lt;/a&gt; eating 10.3 GB of RAM.&lt;/p&gt;

&lt;p&gt;Here's what happened: each Claude Code session spawns about 14 MCP server processes - brave-search, context7, apify, jina, github, trello, pal, and so on. My systemd unit file was using &lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.kill.html" rel="noopener noreferrer"&gt;&lt;code&gt;KillMode=process&lt;/code&gt;&lt;/a&gt;, which means when Untether restarts, systemd kills the main Python process but leaves all the children alive. They get reparented to systemd and just sit there, holding memory, doing nothing. I'd been iterating fast - 64 service restarts in one day during the v0.30-v0.33 development cycle. Each restart leaked another 14 processes. They accumulated silently.&lt;/p&gt;

&lt;p&gt;One config change to &lt;code&gt;KillMode=control-group&lt;/code&gt; and all 10.3 GB came back.&lt;/p&gt;

&lt;p&gt;Then I built a &lt;a href="https://github.com/littlebearapps/untether/issues/91" rel="noopener noreferrer"&gt;subprocess watchdog&lt;/a&gt; to catch a related problem: when a runner subprocess exits but its MCP server children keep stdout pipes open, &lt;code&gt;proc.wait()&lt;/code&gt; blocks forever because anyio waits for both process exit and pipe drain. The session just hangs with no completion event. The watchdog polls process liveness with &lt;code&gt;os.kill(pid, 0)&lt;/code&gt; instead, gives a 5-second grace period, then kills the orphan process group.&lt;/p&gt;

&lt;p&gt;None of this shows up in a test suite. The laptop sleep bug requires an actual laptop going to actual sleep. The "went for a walk" edge case requires a human being away from their phone. The orphaned process leak requires 64 restarts in one day of real development. The subprocess pipe deadlock requires actual MCP servers holding actual file descriptors. You can't mock this stuff. You can only find it by living with the tool.&lt;/p&gt;

&lt;p&gt;Eight releases in 3 days. 545 new tests (1003 to 1548 total). And a stall monitor that actually works now, because it got tested by my life, not just my test suite. Michael Bolton &lt;a href="https://developsense.com/blog/2009/08/testing-vs-checking" rel="noopener noreferrer"&gt;calls this the difference&lt;/a&gt; between &lt;em&gt;testing&lt;/em&gt; and &lt;em&gt;checking&lt;/em&gt; - automated tests check what you already know to look for, but they can't discover the things you never thought to test.&lt;/p&gt;

&lt;p&gt;Every one of these bugs lived in a gap between states. Sleeping and waking. Busy and stuck. Present and away. Tests verify that individual states work. Dogfooding finds the transitions between them - the seams where things actually break.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when your docs tool says your docs are bad?
&lt;/h2&gt;

&lt;p&gt;Running my own documentation tool on its own repo exposed context drift, content filter blocks, and stale docs that test fixtures would never catch. The most embarrassing moment was when PitchDocs told me my own docs were stale.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/littlebearapps/pitchdocs" rel="noopener noreferrer"&gt;PitchDocs&lt;/a&gt; generates documentation for repos. READMEs, changelogs, roadmaps, security policies, user guides, AI context files. I use it on all my repos. Including PitchDocs itself.&lt;/p&gt;

&lt;p&gt;That's where it gets interesting.&lt;/p&gt;

&lt;p&gt;I ran /docs-audit on PitchDocs one morning and got a score of... not great. My own documentation tool was telling me my docs were stale. The irony was not lost on me. But that's the point - I wouldn't have noticed without actually running the tool on my own work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context drift across 7 files
&lt;/h3&gt;

&lt;p&gt;The bigger discovery came from context file drift. PitchDocs generates AI context files for 7 different tools: CLAUDE.md, AGENTS.md, .cursorrules, copilot-instructions.md, .windsurfrules, .clinerules, and GEMINI.md. When I added the platform-profiles skill and the /pitchdocs:platform command, I had to manually update counts in all 6 of those files plus llms.txt. "15 skills" became "16 skills", "12 commands" became "13 commands", across 7 files.&lt;/p&gt;

&lt;p&gt;I did it. Then I added another feature and had to do it again. And again.&lt;/p&gt;

&lt;p&gt;That friction became Context Guard. First version was a post-commit hook that warns you when AI context files have drifted from the codebase. Then I upgraded it to a two-tier system - a gentle nudge after commits, plus a pre-commit guard that blocks the commit entirely if context files are stale. The whole thing exists because I kept getting bitten by my own documentation going out of sync while I was actively building the tool that's supposed to prevent exactly that problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  When the content filter blocks your own docs
&lt;/h3&gt;

&lt;p&gt;Then Claude Code's &lt;a href="https://github.com/littlebearapps/pitchdocs/pull/13" rel="noopener noreferrer"&gt;content filter blocked me&lt;/a&gt; from generating a CODE_OF_CONDUCT file. PitchDocs was trying to write standard open-source community documents, and the API returned HTTP 400 errors because the content triggered safety filters. The same thing happened with SECURITY.md. I had to build chunked writing workarounds and add a content-filter.md rule with risk levels and mitigations. This only surfaced because I was actually generating these files for real repos, not test fixtures.&lt;/p&gt;

&lt;p&gt;The cross-tool compatibility matrix came from real testing. PitchDocs claims to work with 9 AI tools. That claim exists because I actually installed it in Cursor, Windsurf, Codex CLI, and Gemini CLI and watched what happened. Each tool had its own quirks. The compatibility docs aren't theoretical - they're field notes from running the same skill files across different environments and documenting where they broke.&lt;/p&gt;

&lt;p&gt;The README went through 11 revisions in 11 days. I kept applying PitchDocs to its own README, reading the output, and thinking "no, that's not right". The 4-question test, the lobby principle, the feature benefits extraction with persona inference - all of it came from repeatedly failing to describe my own product well and building features to fix the specific ways it failed.&lt;/p&gt;

&lt;p&gt;Look, a documentation tool that doesn't use itself to generate its own docs is just a theory. Running /docs-audit on PitchDocs and getting a mediocre score was embarrassing, but it showed me exactly what to fix. I'd rather be embarrassed than wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when you let AI manage your email?
&lt;/h2&gt;

&lt;p&gt;Using AI for intensive email tasks uncovered 12 bugs in one release cycle, plus two real security vulnerabilities. The safety controls - dry-run mode, rate limiting, recipient allowlist - all came from production failures that slipped through every other layer, not threat modelling.&lt;/p&gt;

&lt;p&gt;I don't manage my email through Claude Code every day. But when I need to find something specific - tax receipts scattered across inbox and sent and archived, invoices from three months ago, calendar events linked from shared calendars - that's when I pull out &lt;a href="https://github.com/littlebearapps/outlook-assistant" rel="noopener noreferrer"&gt;Outlook Assistant&lt;/a&gt;. Claude Code can programmatically search across every folder, collate the results, and export what I need. It turns a full afternoon of manual searching into a 10-minute conversation.&lt;/p&gt;

&lt;p&gt;The first version had 55 tools. That's what happens when you map every Microsoft Graph API endpoint to its own MCP tool. Read email, search email, list email, get attachment, list attachments, send email, update email, move email, flag email. Then repeat for calendar, contacts, folders, rules, and categories. It worked. The API coverage was thorough.&lt;/p&gt;

&lt;p&gt;Then I actually used it in conversation.&lt;/p&gt;

&lt;h3&gt;
  
  
  55 tools and the context window
&lt;/h3&gt;

&lt;p&gt;55 tools consume a lot of tokens. Each tool has a name, description, and parameter schema that gets loaded into context. I hit token limits in real conversations - not contrived long conversations, just normal "search for that email from last week, read it, draft a reply" workflows. The context window was getting eaten by tool definitions before I could do real work.&lt;/p&gt;

&lt;p&gt;I consolidated 55 tools down to 20 using what I called the STRAP pattern - action-parameter consolidation where one tool handles multiple operations through an action parameter. manage-emails with actions like flag, move, categorise, export. manage-calendar with create, update, delete. 64% reduction. About 11,000 tokens saved per turn. I only knew the limit was a problem because I was the one hitting it.&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.amazonaws.com%2Fuploads%2Farticles%2Fmz3zidlew73decjre0ya.webp" 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.amazonaws.com%2Fuploads%2Farticles%2Fmz3zidlew73decjre0ya.webp" alt="Diagram comparing 55 individual MCP tools consolidated to 20 using the STRAP action-parameter pattern" width="792" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Silent API failures and progressive search
&lt;/h3&gt;

&lt;p&gt;Microsoft's $search API &lt;a href="https://github.com/littlebearapps/outlook-assistant/issues/35" rel="noopener noreferrer"&gt;silently fails on personal Outlook.com accounts&lt;/a&gt;. Not an error - it just returns no results. I found out because I searched for an email I knew existed and got nothing back. Built progressive search: try $search first, fall back to $filter with subject/from matching, then try broader date-range filtering, then full scan. Four strategies, automatic fallback, with a warning message so you know which strategy actually found your email.&lt;/p&gt;

&lt;p&gt;Twelve bugs came from real use in the v3.1.0 cycle. A folder that doesn't exist would &lt;a href="https://github.com/littlebearapps/outlook-assistant/issues/46" rel="noopener noreferrer"&gt;silently fall back to the inbox&lt;/a&gt; instead of telling you it couldn't find it. Asking for count=0 emails would return everything instead of nothing. The "minimal" view mode said "No content" instead of showing a body preview. HTML email bodies weren't detected correctly for certain Content-Types. Conversation export crashed on personal accounts. Calendar events showed &lt;a href="https://github.com/littlebearapps/outlook-assistant/issues/51" rel="noopener noreferrer"&gt;UTC timestamps instead of local time&lt;/a&gt;. Inbox rule sequences showed internal IDs instead of readable order.&lt;/p&gt;

&lt;p&gt;Each of these is a small thing. None of them would fail a test that checks "does the API return a 200?" But they only matter when you're a person trying to get through your email.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safety controls born from production failures, not documentation
&lt;/h3&gt;

&lt;p&gt;The safety controls tell their own story. Dry-run mode for sending emails. Session rate limiting. Recipient allowlist. None of these came from a threat model document. They came from failures that got past the test suite, past AI agent verification, and past CI checks - and only surfaced when I was actually using the tool on real email.&lt;/p&gt;

&lt;p&gt;The first time Claude drafted a reply and I realised it was one approval button away from sending it to the wrong person, I built dry-run mode that afternoon. Not the next sprint. That afternoon. Rate limiting came after a loop scenario almost happened in production - the kind of thing where the AI gets confused and tries to send 50 replies. The test suite didn't catch it because the tests mock the send endpoint. The AI agent review didn't catch it because it looked correct in isolation. CI passed. It only became obvious when I was sitting there watching it happen in real time. The allowlist lets me restrict which addresses Claude can actually send to during testing, because "oops, sent a test email to a client" is not a recoverable mistake.&lt;/p&gt;

&lt;p&gt;That's what dogfooding adds as a layer. You run the test suites. You get the AI agent to stress-test it. You run CI. And then you spend days or weeks actually using it in production, pushing it to its limits, and you find the edge cases that none of those layers caught. The guardrails in Outlook Assistant didn't come from security best practices or compliance requirements. They came from real production use where things went wrong after every automated check had passed.&lt;/p&gt;

&lt;p&gt;I also found two real security vulnerabilities through production use - an &lt;a href="https://github.com/littlebearapps/outlook-assistant/issues/60" rel="noopener noreferrer"&gt;XSS issue&lt;/a&gt; and an &lt;a href="https://github.com/littlebearapps/outlook-assistant/issues/63" rel="noopener noreferrer"&gt;information exposure bug&lt;/a&gt; - that I fixed and then added &lt;a href="https://github.com/littlebearapps/outlook-assistant/issues/65" rel="noopener noreferrer"&gt;CodeQL SAST scanning&lt;/a&gt; to catch that class of problem earlier. Those bugs shouldn't have made it as far as they did, and they wouldn't have been found as quickly without me actually using the tool on real email.&lt;/p&gt;

&lt;h2&gt;
  
  
  What pattern do these bugs share?
&lt;/h2&gt;

&lt;p&gt;Across all three products, the bugs I find through real use cluster into two categories that tests can't reach.&lt;/p&gt;

&lt;p&gt;Resource accumulation over time. 86 orphaned processes eating 10.3 GB of RAM. Documentation counts going stale across 7 files. Token budgets getting consumed by tool definitions before the real work starts. These problems are invisible in short test runs. They only appear after hours or days of real use.&lt;/p&gt;

&lt;p&gt;The gap between "works" and "works for me." A stall monitor that can't tell the difference between a stuck process and a person walking their dog. An email search that returns nothing because Microsoft's API silently fails on personal accounts. A documentation tool that can't generate a CODE_OF_CONDUCT because the content filter blocks it. These aren't bugs in the traditional sense. They're mismatches between what the code does and what the person using it needs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bug type&lt;/th&gt;
&lt;th&gt;What tests check&lt;/th&gt;
&lt;th&gt;What dogfooding found&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Resource leaks&lt;/td&gt;
&lt;td&gt;Memory per isolated test run&lt;/td&gt;
&lt;td&gt;86 processes accumulating over 64 restarts.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State transitions&lt;/td&gt;
&lt;td&gt;Each state in isolation&lt;/td&gt;
&lt;td&gt;Gaps between sleeping/waking, busy/stuck, present/away.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API quirks&lt;/td&gt;
&lt;td&gt;Mocked API returns 200 OK&lt;/td&gt;
&lt;td&gt;Microsoft search silently returns nothing on personal accounts.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UX friction&lt;/td&gt;
&lt;td&gt;Feature exists and works&lt;/td&gt;
&lt;td&gt;Finding a reply button while stall warnings fill your screen.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Safety gaps&lt;/td&gt;
&lt;td&gt;Permissions check passes&lt;/td&gt;
&lt;td&gt;Nearly sending email to the wrong person in production.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The common thread across all of these bugs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They only show up after sustained real use, not isolated test runs.&lt;/li&gt;
&lt;li&gt;They live in the transitions between states, not in the states themselves. (Nancy Leveson &lt;a href="http://sunnyday.mit.edu/papers/jsr.pdf" rel="noopener noreferrer"&gt;found the same pattern&lt;/a&gt; in spacecraft accidents - the software worked per spec, but failed at state transitions under real conditions.)&lt;/li&gt;
&lt;li&gt;They're invisible to automated tests because you can't mock a human walking their dog.&lt;/li&gt;
&lt;li&gt;They matter more to users than the logic errors your test suite catches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I think test suites give you a kind of false confidence. You get to 90% coverage and you feel good about it. Martin Fowler &lt;a href="https://martinfowler.com/bliki/TestCoverage.html" rel="noopener noreferrer"&gt;made this point years ago&lt;/a&gt; - high coverage is useful for finding untested code, but it's "of little use as a numeric statement of how good your tests are." The bugs that actually matter - the ones your users would hit on day one - don't live in test cases. They live in the space between your code and someone's actual life.&lt;/p&gt;

&lt;p&gt;That said, I don't dogfood as a deliberate practice. I don't schedule "dogfooding sessions" or maintain a testing protocol. I use these tools because they solve problems I have. The bugs get found as a side effect of genuine use, not deliberate testing. The stall monitor saga happened because I actually rely on Untether to work. The context drift problem surfaced because I actually use PitchDocs on my repos. The dry-run mode exists because I actually use Claude to handle real email tasks.&lt;/p&gt;

&lt;p&gt;If you're building something and you don't use it yourself, you're shipping based on hope rather than experience. Jason Fried &lt;a href="https://37signals.com/podcast/eat-your-own-dogfood/" rel="noopener noreferrer"&gt;put it simply&lt;/a&gt;: "A good chef is tasting their food as they go." And experience, it turns out, is a much better debugger than pytest.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common questions about dogfooding
&lt;/h2&gt;

&lt;p&gt;Some things I get asked when I talk about this approach.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between dogfooding and automated testing?
&lt;/h3&gt;

&lt;p&gt;Testing checks that code produces expected outputs from known inputs. Dogfooding exposes code to conditions you can't mock - laptop sleep, distracted humans, resources accumulating over days. 8 of the bugs I found in Untether exist in the gaps between states that tests treat as isolated. Both matter, but they catch different classes of problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many bugs does real use catch that tests miss?
&lt;/h3&gt;

&lt;p&gt;Across three products in one month, real use surfaced 22 bugs that automated tests missed entirely. They split into resource accumulation (86 orphaned processes, 10.3 GB of leaked RAM) and works-vs-works-for-me gaps. Tests catch logic errors. Dogfooding catches experience errors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do you schedule dedicated dogfooding sessions?
&lt;/h3&gt;

&lt;p&gt;No. I use these tools because they solve real problems - Untether for mobile coding, PitchDocs for repo docs, Outlook Assistant for email. The bugs surface as a side effect of genuine use. Scheduled testing would never reproduce "walked Normi for 6 minutes" as an edge case.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can dogfooding replace automated testing?
&lt;/h3&gt;

&lt;p&gt;No. Untether has 1,548 automated tests and I run them constantly. Automated tests catch regressions and logic errors reliably. Dogfooding catches a different category - state transitions, resource leaks, UX friction that only appears in real workflows. You need both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does dogfooding work differently for solo developers?
&lt;/h3&gt;

&lt;p&gt;Solo developers are their own most demanding user. I put Untether through 64 service restarts in a single day, which revealed 86 orphaned processes. Real development patterns create edge cases that no QA team testing "normal usage" would ever reproduce.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All numbers in this post are verified from GitHub issues, PRs, and commit history. &lt;a href="https://github.com/littlebearapps/untether" rel="noopener noreferrer"&gt;Untether&lt;/a&gt;, &lt;a href="https://github.com/littlebearapps/pitchdocs" rel="noopener noreferrer"&gt;PitchDocs&lt;/a&gt;, and &lt;a href="https://github.com/littlebearapps/outlook-assistant" rel="noopener noreferrer"&gt;Outlook Assistant&lt;/a&gt; are all open source.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>testing</category>
      <category>opensource</category>
      <category>devlog</category>
    </item>
  </channel>
</rss>
