<?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: Martin Schenk</title>
    <description>The latest articles on DEV Community by Martin Schenk (@martinschenk).</description>
    <link>https://dev.to/martinschenk</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%2F271233%2F8be15adb-2816-49e2-b761-7064e2e0b348.jpg</url>
      <title>DEV Community: Martin Schenk</title>
      <link>https://dev.to/martinschenk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/martinschenk"/>
    <language>en</language>
    <item>
      <title>Every gate in my system is a scar</title>
      <dc:creator>Martin Schenk</dc:creator>
      <pubDate>Thu, 30 Jul 2026 14:09:36 +0000</pubDate>
      <link>https://dev.to/martinschenk/every-gate-in-my-system-is-a-scar-33la</link>
      <guid>https://dev.to/martinschenk/every-gate-in-my-system-is-a-scar-33la</guid>
      <description>&lt;p&gt;Last week a Claude Code session told me that my order queue had 180 open items, that none of them was older than 13 days, and that 221 of the 254 entries had been written by Claude Code sessions. It had counted itself. The largest single mailbox in the system was the system working on itself: 36 orders, about 30 of which were the tooling improving its own tooling.&lt;/p&gt;

&lt;p&gt;I had not looked at that list in a while. That is the part worth writing down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is in this post:&lt;/strong&gt; how one &lt;code&gt;TODO.md&lt;/code&gt; per project stopped working at ten projects, the shape the replacement settled into, three things that broke and the gate each one left behind, and what the whole thing still costs me. The middle part is the one to steal if you work across more repositories than you can hold in your head, whichever tools you drive them with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ten projects and one Markdown file
&lt;/h2&gt;

&lt;p&gt;I started working across multiple projects with Claude Code at the end of last year. Back then the whole system was a &lt;code&gt;TODO.md&lt;/code&gt; in each project folder. I opened a session, said what needed doing, and the file kept the rest. For two or three projects that is completely fine. I would recommend it.&lt;/p&gt;

&lt;p&gt;Then it became ten. Ten live projects, ten folders on a Mac Studio, ten TODO files, and three things started going wrong at once.&lt;/p&gt;

&lt;p&gt;Ideas got lost on the way home. Something would occur to me while I was out, and the place to write it down was a Markdown file inside a project folder on a machine I was not sitting at. By the time I was back, it was gone.&lt;/p&gt;

&lt;p&gt;Mornings became a decision I did not want to make. Ten lists, no shared view, no ranking across them. Which project needs me today? The honest answer was usually whichever one I had touched last, which is the opposite of a priority.&lt;/p&gt;

&lt;p&gt;And projects went quiet for weeks. Not because they were finished, but because nothing in my setup ever raised its hand. A file does not tell you it has been ignored.&lt;/p&gt;

&lt;p&gt;About a month ago I decided I wanted something that would tell me what mattered most, across everything. Not another list. A layer above the lists.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like now
&lt;/h2&gt;

&lt;p&gt;Last week I did something I had been putting off: I wrote the manual. Not documentation of the code, a manual for the operator, and the operator is me. Where I do what, which commands exist, how a note becomes an order, and which four places stop and wait for a human. It runs to eight sections. Writing it was uncomfortable in a useful way, because a tool that needs a manual for its only user is telling you something about how it grew.&lt;/p&gt;

&lt;p&gt;What follows is the part of that manual that generalises. Anything that could become work goes into one collection point, and from there it has exactly one path.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Diagram omitted here. The two flowcharts are in the &lt;a href="https://martin-schenk.es/blog/every-gate-in-my-system-is-a-scar/" rel="noopener noreferrer"&gt;original post&lt;/a&gt;.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two clockworks run behind this, and neither of them thinks. A launch agent empties the drop box every 15 minutes (&lt;code&gt;StartInterval 900&lt;/code&gt;), sorting entries into the right project's list. A second one every 30 minutes (&lt;code&gt;StartInterval 1800&lt;/code&gt;) picks up approved orders and runs them headless in a git worktree under &lt;code&gt;~/.cache/master-dispatch-worktrees&lt;/code&gt;, deliberately outside every repository. No model is involved in the sorting step, which is why it is free to run it that often.&lt;/p&gt;

&lt;p&gt;The part I use most is the least clever one. I send myself an email.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Diagram omitted here. The two flowcharts are in the &lt;a href="https://martin-schenk.es/blog/every-gate-in-my-system-is-a-scar/" rel="noopener noreferrer"&gt;original post&lt;/a&gt;.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That is the fix for the first of my three problems. The thing I can always reach is my own inbox, so that became the entry point. What it does not do is turn a note into work. It becomes an order and then it stops, which brings me to the reason any of this has gates at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three sessions, one file
&lt;/h2&gt;

&lt;p&gt;On 17 July I had three Claude Code sessions running in parallel, all of them touching a shared config file that holds per-project spending limits. Each one read the file, changed its own line, and wrote the whole file back. Classic read-modify-write, except the writers were agents rather than threads, and I was not thinking about it as concurrency at all. The last writer won. One session's exception was silently erased by the next.&lt;/p&gt;

&lt;p&gt;The fix is unremarkable: shared resources are now only writable through a small &lt;code&gt;mit-lock&lt;/code&gt; wrapper, an mkdir-based lock with a 10-minute stale timeout and an audit log. A pre-tool hook refuses any Bash command that writes to one of the protected paths without going through it.&lt;/p&gt;

&lt;p&gt;What was not unremarkable was the realisation behind it. I had been treating parallel sessions as if they were parallel humans, who would notice. They do not notice. They are processes, and the moment two of them share state you owe them the same discipline you would owe two threads. That is where the three rules came from that everything else now sits on: one writer per repository, one owner per shared resource, one channel for cross-project work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool that produced more work than it finished
&lt;/h2&gt;

&lt;p&gt;Which brings me back to the count at the top.&lt;/p&gt;

&lt;p&gt;By 28 July the queue held 180 open orders and none of them was older than 13 days. That means the system had produced roughly 20 new orders per day since 20 July, while I was closing somewhere between two and five. 221 of 254 drop-box entries had been written by AI sessions rather than by me, 163 of them from sessions in the orchestration repo itself.&lt;/p&gt;

&lt;p&gt;Nothing was broken. Every one of those orders was reasonable. That is exactly the problem. A Claude Code session wants to be useful, and if you open one in any real repository it will find ten genuine improvements: a refactor that would be cleaner, a doc that is out of date, a test that could be tighter. Multiply ten reasonable observations by fifteen repositories and you get a stream no single person will ever drain. My inbox was not filling up with mistakes. It was filling up with good ideas.&lt;/p&gt;

&lt;p&gt;The fix inverts the burden of proof. Until then an order existed unless somebody actively deleted it. Now it only comes into existence if a trigger can be named: a fault that hits a user or customer, a risk to security, money or data, a deploy blocker, or me saying so out loud. "Would be cleaner", "noticed in passing", "for consistency" is not a trigger. Those go into the session summary I read, not into the pipeline. Applying it backwards archived 19 meta-orders in one go and took the total from 165 to 144.&lt;/p&gt;

&lt;p&gt;The thing I deliberately did not build here is the interesting one. The obvious move is an AI gatekeeper: let a model judge each incoming order and reject the noise. I did not, because the same week a badly worded order had come through that was long, well structured and entirely plausible, and which, if taken literally, would have routed the outgoing mail of 186 customer mailboxes through a service that was never meant to carry it. A model gatekeeper would have waved that one straight through. It would have contained precisely the failure class it was supposed to catch. Some problems do not get better by adding another model to them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The guard that was looking at the wrong thing
&lt;/h2&gt;

&lt;p&gt;The most recent one is from yesterday, and it is the fourth time I have hit this same class of failure.&lt;/p&gt;

&lt;p&gt;The rule that one session only writes to its own repository is enforced by a pre-tool hook, and until yesterday that hook worked by looking at commands. It knew the shapes that write to a foreign path: output redirection, &lt;code&gt;tee&lt;/code&gt;, &lt;code&gt;git -C&lt;/code&gt;, &lt;code&gt;sed -i&lt;/code&gt;. On 28 July a rollout script wrote into 21 foreign repositories through a Python heredoc. The hook saw a &lt;code&gt;python3&lt;/code&gt; invocation with a string attached and let it pass, because a heredoc is not on the list of things that look like writing.&lt;/p&gt;

&lt;p&gt;The correction was to stop guessing at intent. There is now a check that records the git state of every repository before an autonomous run and compares it afterwards. It does not care how a change was made, only whether one appeared where it should not have. It fails closed in three places: a missing check blocks the dispatcher from starting, a failed baseline aborts the order, and an absent baseline counts as a discrepancy rather than an all-clear.&lt;/p&gt;

&lt;p&gt;Pattern matching guesses at what a command will do. Comparing before and after measures what it did. I needed four attempts to learn that, and I would not have designed it that way on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs and what is still open
&lt;/h2&gt;

&lt;p&gt;The orchestration layer is 32 shell scripts and about 6,300 lines, with 145 tests that run against copies and never touch the real orders. Twenty-one hooks fire before, during and after tool calls: repository boundaries, resource locks, a check that blocks a database dump flag that once destroyed 199 of 200 rows, one that refuses outgoing mail containing facts that are not on a verified list. None of that was designed. Each piece is sitting where something went through.&lt;/p&gt;

&lt;p&gt;Four places still stop and wait for me, and they are the reason nothing surprising happens. An order does nothing until I approve it, including orders I filed myself. Work marked &lt;code&gt;dialogue&lt;/code&gt; does not run without me. Finished work never merges itself. And anything whose text mentions payments, customers or a production deploy is held before it starts even when it is approved, because on 27 July a fifty-euro customer refund was sitting in the queue, approved, marked autonomous, ready to go.&lt;/p&gt;

&lt;p&gt;The honest state today: 149 orders on the books, 122 of them approved and waiting. The trigger rule slowed the inflow, it did not reverse it. The underlying tension is not solved and I am not sure it can be, because it is not a bug in the tooling. A machine that generates candidate work faster than a human can evaluate it will always end in a queue, and every gate I have built is a place where I chose to be the bottleneck on purpose.&lt;/p&gt;

&lt;p&gt;What I would do differently, if I were starting this at the end of last year with ten projects and one Markdown file per project: build the drop box and the approval gate first, and nothing else. Those two are the only parts I have never had to repair. Everything else in this system I built twice.&lt;/p&gt;

&lt;p&gt;If you are running something similar and have found a way to keep the inflow honest without a human reading every item, I would like to hear it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>devops</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>The number that lied: rebuilding a usage meter that actually helps</title>
      <dc:creator>Martin Schenk</dc:creator>
      <pubDate>Sat, 25 Jul 2026 23:04:25 +0000</pubDate>
      <link>https://dev.to/martinschenk/the-number-that-lied-rebuilding-a-usage-meter-that-actually-helps-4792</link>
      <guid>https://dev.to/martinschenk/the-number-that-lied-rebuilding-a-usage-meter-that-actually-helps-4792</guid>
      <description>&lt;p&gt;Short version first, for anyone who just wants the outcome: I run Claude Code on a flat monthly subscription, not pay-as-you-go. For months that plan never gave me a reason to look closely at it, so I stopped looking. Then an overnight automation burned through a week's worth of it in two days, the account locked with four days still to go, and I bought my way out at list price. That cost enough to make me build a small usage meter that now tells me, in plain language, whether I can spend more or need to slow down. The rest of this post is the four attempts it took to get that meter right, and why the wrong ones were each wrong in an instructive way.&lt;/p&gt;

&lt;p&gt;Now the actual story. I run about ten projects at once, mostly alone. Client work, my own products, email, all of it moves through Claude Code sessions, one per project, in their own terminal tabs. Keeping track of what needs attention across ten folders got old, so I built a small side project, a "master supervisor" that walks every project's todo queue and picks off whatever it can do without me. I put it on a loop so it would work through the night.&lt;/p&gt;

&lt;p&gt;It worked beautifully. For two nights.&lt;/p&gt;

&lt;p&gt;On the third day I opened my laptop and Claude Code answered every prompt with the same message: I had hit my weekly limit. Not low, not close. Hit. And the account told me the reset was four days out.&lt;/p&gt;

&lt;p&gt;A word on that limit, because it surprised me too. Claude Code's subscription plans started out metered by month. Anthropic added a weekly limit on top of that later, presumably to stop exactly the kind of burst I'd just produced. I knew the monthly number existed somewhere. I did not know there was a weekly one running underneath it, because in months of normal use I had never come close to either, save one two-hour lockout I shrugged off as a fluke. So I never looked at the usage page on claude.ai. Not once a week, not once a month. I just worked, the way you don't check a fuel gauge on a car that's never run dry. The overnight loop didn't create the blind spot, it just found it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The panic math
&lt;/h2&gt;

&lt;p&gt;Four days without Claude Code is not an inconvenience for me, it is most of my income-producing time gone. I have client deadlines, my own projects mid-build, an inbox that does not pause because my AI subscription did. So I did the obvious thing: I bought extra tokens to keep working, at list price, no subscription discount attached.&lt;/p&gt;

&lt;p&gt;I was careful. I mostly used the cheaper model, kept sessions short, only touched what was actually urgent. Four days later I had spent 200 euros keeping a fraction of my normal pace alive. That number is what made me stop and actually understand what had happened, instead of just being angry at it.&lt;/p&gt;

&lt;p&gt;The overnight loop was part of the story, and a separate lesson on its own (wrong model for the job, no cost brake on autonomous work). What stuck with me longer was something dumber: I had no way of seeing this coming, and I hadn't built myself a reason to look. There was no version of the truth in front of me that would have told me, on day two, "you are burning through a week's plan in two days."&lt;/p&gt;

&lt;p&gt;So I built one. It took four attempts to get right, and the interesting part is not the tool, it is the four ways I got it wrong first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt one: a number I made up
&lt;/h2&gt;

&lt;p&gt;The first version showed a weekly dollar total against a weekly dollar limit. I computed it myself from local session logs, tokens times model price, summed up.&lt;/p&gt;

&lt;p&gt;The problem: a subscription plan like mine is not metered in dollars at all. It is metered in percent of a weekly allowance. The dollar limit I was comparing against did not exist anywhere except in my own estimate, calibrated once against a single observed lockout, first at $12,000, later "corrected" to $4,690.&lt;/p&gt;

&lt;p&gt;I only noticed because the numbers stopped agreeing with reality. The lesson is not that the figure was imprecise. It was invented, and it looked exactly like a measurement. A made-up number is worse than no number, because you trust it.&lt;/p&gt;

&lt;p&gt;What it actually looked like at the bottom of the terminal, calm green, entirely fictional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$4,690 weekly budget · $612 spent (13%) · well within budget
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Attempt two: the right number, still useless
&lt;/h2&gt;

&lt;p&gt;So I found the real source. Anthropic does not expose a usage API for subscription accounts, only for pay-as-you-go console accounts, but the same endpoint the usage page on claude.ai calls itself is reachable with a session cookie. No browser automation needed, no window has to stay open, just a decrypted cookie read once every fifteen minutes by a background job.&lt;/p&gt;

&lt;p&gt;Now the status line showed something true:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plan: 12% -&amp;gt; Mon 13:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the first question back to myself was the right one: what does 12% even mean? I did not know when the week had started.&lt;/p&gt;

&lt;p&gt;That is the actual problem with a lone percentage. 12% on day one means "way too fast." 12% on the last day means "you just threw away 88% of what you paid for." Same number, opposite meaning. A percentage without a position in time answers nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt three: reach, not a gauge
&lt;/h2&gt;

&lt;p&gt;What I actually needed to know was not "how much is gone" but "how long will this last." A fuel gauge in liters is technically correct and useless while driving; nobody converts that in their head at 120 km/h. You want range.&lt;/p&gt;

&lt;p&gt;The display now says something like this, on a calm Tuesday:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plan: lasts 4.2 more days · resets in 3 days · on pace, room for more
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same three numbers carry three different colors depending on what they add up to. Comfortably ahead, Monday morning, plenty of room:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plan: lasts 6.8 more days · resets in 5 days · well ahead, spend freely
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cutting it close, the kind of line that used to just say a lonely percentage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plan: lasts 2.1 more days · resets in 3 days · tight, ease off if you can
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And past the point of pretending it's fine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plan: lasts 1.4 more days · resets in 3 days · over pace, slow down now
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three numbers, all relative to right now: how long the remaining budget lasts at the current rate, when it resets, and what that means. The middle number, the reset, is what makes the first one readable at all.&lt;/p&gt;

&lt;p&gt;Two facts about that reset turned out to matter, and I got both wrong before I checked the actual docs instead of guessing from the API field name. The field is called &lt;code&gt;seven_day&lt;/code&gt;, which reads like a rolling window, so I assumed usage from a week ago would keep expiring as new usage came in. Wrong. It is a fixed weekly clock per account, same day, same hour, every time, in my case Monday 13:00 local. And more importantly: nothing carries over. Anthropic's own words, "you get your full weekly amount in each cycle." A quiet week does not bank you anything for the next one.&lt;/p&gt;

&lt;p&gt;That second fact rewired how I thought about the whole thing. If unused budget expires, then staying comfortably under the limit is not success, it is money paid for and not collected. A plan running at half capacity all month is the same as throwing away half the subscription price. Every version of this meter I had built so far only warned in one direction, too fast. None of them ever said the other true thing: too slow, you're leaving money on the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt four: doing the math properly
&lt;/h2&gt;

&lt;p&gt;The reach number is a projection: percent used divided by hours elapsed in the cycle gives a rate, the remainder divided by that rate gives the reach. Undamped, this breaks immediately. 20% used on day one projects to 140% for the week, so the meter would say "slow down" on a Monday morning that was simply a busy day, the kind any normal week absorbs by Friday.&lt;/p&gt;

&lt;p&gt;My first fix was a damping factor, chosen by feel, heavier early in the week. I made the numbers up the same way I'd made up the dollar limit two attempts earlier, and I noticed it this time before shipping it, because I had learned to distrust round numbers I couldn't explain.&lt;/p&gt;

&lt;p&gt;Usage over an observation window is a counting process, and counting processes have an actual statistical treatment: with a Jeffreys prior the posterior rate distribution is Gamma(x + 0.5, t), and its relative spread shrinks with 1/sqrt(x). That is precisely the intuition "early in the week the number swings more" made mathematically exact, not eyeballed. So instead of judging the point estimate, the meter judges the optimistic edge of the credible interval: only warn once even the generous case doesn't add up.&lt;/p&gt;

&lt;p&gt;The tolerance falls out of that on its own, no calibration required:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Used&lt;/th&gt;
&lt;th&gt;Tolerance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;td&gt;6.8x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;2.6x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;1.8x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;td&gt;1.4x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;1.27x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Monday is naturally forgiving, Friday is naturally strict, and nothing about that is a knob I set.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a systematic test found that thinking never would
&lt;/h2&gt;

&lt;p&gt;I stopped trusting my own read of "does this look right" and instead generated every combination of cycle-day and usage-level, 133 cases, and checked each one against a single question: does the number agree with the color.&lt;/p&gt;

&lt;p&gt;It found two things I would not have caught by staring at it.&lt;/p&gt;

&lt;p&gt;First, the damping I'd just derived could flip the actual verdict, not just soften it. At 75% used on day five, 40 hours of projected reach against 48 hours to reset means it genuinely does not add up, and the meter still said "on pace" because the statistical tolerance pushed the ratio just over 1. Uncertainty is allowed to soften how loud a warning is. It is never allowed to flip whether there is one. Green now only fires when the math actually works out, full stop, tolerance or not.&lt;/p&gt;

&lt;p&gt;Second, the fix for that created its own false alarm on the other side, triggering in the first hours of a cycle where there simply isn't enough data yet to say anything. 10% used by Monday evening was showing orange. The guard against false negatives now only engages after 24 hours of observation, before that the recommendation is capped at "on pace" and nothing stronger, because the data genuinely doesn't support a stronger claim yet.&lt;/p&gt;

&lt;p&gt;Neither of those two bugs would have shown up from normal use. They only surfaced because I forced every corner of the state space through the same question.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second pass, a day later
&lt;/h2&gt;

&lt;p&gt;That first test suite checked one thing: given valid numbers, does the meter say the right thing. A second pass a day later asked something I hadn't thought to ask: what does it say when the numbers themselves are missing or stale.&lt;/p&gt;

&lt;p&gt;Four more bugs, all from the same family. The meter kept computing as if its inputs were fine, even when they weren't, and it kept landing on something reassuring.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Broken input&lt;/th&gt;
&lt;th&gt;What it showed&lt;/th&gt;
&lt;th&gt;What that hides&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reset time already in the past&lt;/td&gt;
&lt;td&gt;"lasts 7 more days, resets in -180 min"&lt;/td&gt;
&lt;td&gt;The cycle already reset; the stored percentage is stale, and the projection ran on it anyway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data 72 hours old&lt;/td&gt;
&lt;td&gt;Full reach number, "72h old" tacked on the end&lt;/td&gt;
&lt;td&gt;This is the exact shape of the original failure: a calm number sitting on top of a locked account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Usage field missing entirely&lt;/td&gt;
&lt;td&gt;"Fresh plan, resets in None"&lt;/td&gt;
&lt;td&gt;An outage rendered identically to a brand new week&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spend limit already below actual spend&lt;/td&gt;
&lt;td&gt;"-132 EUR this month"&lt;/td&gt;
&lt;td&gt;The hard lockout state, displayed as a negative number instead of a state&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That third row, on screen, looked exactly as confident as a real reading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plan: lasts 7.0 more days · resets in None · on pace, room for more
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All four now collapse to one honest line: the meter does not know right now, instead of guessing quietly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plan: data unavailable · last read failed, not guessing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
 And the background job that refreshes this every fifteen minutes reports a stale read on its own, past six hours, without being asked, because a meter that has stopped moving needs to be louder than one that is simply wrong. A frozen number looks exactly like a real one. A number you distrust at least gets checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where that leaves me
&lt;/h2&gt;

&lt;p&gt;I still don't know if the four-day lockout was purely the overnight loop, or partly a mismatch between which model does which job across ten projects, or both. That part is unresolved and I'm not going to pretend otherwise here.&lt;/p&gt;

&lt;p&gt;What I do know is that the meter I have now would have told me on day two, in plain language, that I was burning the week's plan at a pace that wouldn't reach Friday. Not a percentage I'd have to do arithmetic on mid-task. A sentence.&lt;/p&gt;

&lt;p&gt;The broader thing I keep relearning, this time from a usage dashboard instead of from code: a plausible number with no source behind it is more dangerous than an obvious gap, because the gap gets questioned and the plausible number doesn't. And a number that goes quiet when its inputs disappear is worse than one that is simply wrong, because silence reads as agreement.&lt;/p&gt;

&lt;p&gt;If you're running any kind of budget meter, subscription, cloud spend, rate limit, whatever, worth asking it the same question I eventually asked mine: what does it say when the data it needs isn't there. Mine used to answer with something calm. That was the actual bug.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>productivity</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Autonomous agents are the easy part</title>
      <dc:creator>Martin Schenk</dc:creator>
      <pubDate>Thu, 23 Jul 2026 19:14:07 +0000</pubDate>
      <link>https://dev.to/martinschenk/autonomous-agents-are-the-easy-part-4b8m</link>
      <guid>https://dev.to/martinschenk/autonomous-agents-are-the-easy-part-4b8m</guid>
      <description>&lt;p&gt;One morning last week I opened my inbox to a hundred emails, all sent from me, to me, most of them stamped within the same two minutes of the night before. That is the end of the story. To make it mean anything I have to start with how I work, because most of what broke came from the shape of the setup, not from the AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ten projects and no idea where to look
&lt;/h2&gt;

&lt;p&gt;I am one person with about ten live projects. They sit as ten folders on a Mac Studio, and I keep an iTerm tab open per project, each running its own Claude Code session. Two or three are usually doing real work at once while I move between them. That part I have written about before. It works.&lt;/p&gt;

&lt;p&gt;The problem was never the coding. It was knowing what to code next. Tasks arrived from two directions and went nowhere useful. When an idea hit me while I was away from the desk, I emailed it to myself: project name, one line about what to do. And when I was already deep in a project and noticed something, I would tell Claude Code "add that to the to-do list," and it did, into that project's own list.&lt;/p&gt;

&lt;p&gt;So after a few months I had ten to-do lists. Ten. Every morning I opened the day with the same useless question: of ten projects, which one is the most important place to spend the next hour, and which of these tasks could a machine just do without me. Ten lists cannot answer that. Ten lists are not a list, they are ten separate little piles, each blind to the other nine.&lt;/p&gt;

&lt;h2&gt;
  
  
  One list instead of ten
&lt;/h2&gt;

&lt;p&gt;So in mid-July I made a new project whose only job is to look at the other ten. I call it the master-supervisor. It started as nothing clever: a script that walks every repo, reads a small status file each project keeps, and builds one ranked overview, so I open one thing in the morning instead of ten. A chief of staff for the portfolio, not another pair of hands.&lt;/p&gt;

&lt;p&gt;Then came the move that actually mattered. I deleted the per-project to-do lists, all of them, on the same day, and replaced them with one system-wide list the supervisor owns. I stopped calling the entries to-dos and started calling them orders, as in work orders, because the rename was the point: these are no longer notes to myself, they are units of work that something can pick up and execute. Every project still has its own file, but it lives in the supervisor's repo now, and it is a queue, not a scratchpad.&lt;/p&gt;

&lt;h2&gt;
  
  
  The label that decides everything
&lt;/h2&gt;

&lt;p&gt;An order is a small block of markdown: a title, a couple of fields, a line or two of description. One looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## ORDER update-mcp-server
art: autonomous
prio: medium

Bump the MCP server to the current major version
and re-run the integration tests.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two of those fields do the work, and the first is the one that matters. It is a field called &lt;code&gt;art&lt;/code&gt;, with three possible values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;autonomous&lt;/strong&gt; — Claude Code can just do this, unattended, and ship it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;dialog&lt;/strong&gt; — I want to be in the loop while it happens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;decision&lt;/strong&gt; — only I can make the call. A payment going live, a domain purchase, a public announcement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the part I want to be honest about, because it is the opposite of what sounds impressive. No AI decides which bucket an order goes in. I wanted to say "the machine reads the task and works out for itself whether it is safe to run alone," and I built exactly the opposite, on purpose. Whoever writes the order sets the label, in the moment, with the full context of having just thought of it. The dispatcher that runs things later is deliberately not allowed to guess, because a script reading a title cannot judge difficulty, it can only pretend to.&lt;/p&gt;

&lt;p&gt;And the default, when the field is missing or unclear, is &lt;code&gt;dialog&lt;/code&gt;, never &lt;code&gt;autonomous&lt;/code&gt;. That asymmetry is the whole safety model in one line. An order that sits and waits for me costs a little time. An order that runs on its own when it should not have costs work on production. So the machine leans toward asking, and &lt;code&gt;autonomous&lt;/code&gt; has to be earned, not assumed.&lt;/p&gt;

&lt;p&gt;The second label is a priority, and I will admit straight out that this is the part I have least figured out. I set it by hand, high, medium or low, and every few days I still sit down and re-sweep the whole queue by hand because the automatic ordering is not good enough yet. Forty-one autonomous orders in one sweep, of which I freed thirteen and held fourteen. That is not an algorithm, that is me with a highlighter. It is the honest state of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the orders come from, and the one rule I will not bend
&lt;/h2&gt;

&lt;p&gt;Orders arrive three ways, and most of the machinery around them exists to keep one door shut. Ideas I email myself and alerts my systems raise both land in the same inbox, but nothing there becomes an order on its own. They turn into orders during triage, when I am reading the inbox anyway and I am the one filing them. That is a fixed rule, not laziness: a mail that could promote itself into an autonomous task is an open door, because anyone who can email me could hand my machine instructions, and the AI reading the mail cannot tell my intent from an attacker's paragraph. Mail stays a human gate.&lt;/p&gt;

&lt;p&gt;The third way is me, in a project, saying "put that on the list," and that goes through a shared, append-only mailbox the supervisor turns into real orders in its own files, one writer per file. It sounds like bureaucracy and it is what keeps the history readable. As I found out last time, the version of that rule which actually holds is a hook that returns an error, not a line in a document.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five-minute night shift
&lt;/h2&gt;

&lt;p&gt;Now the part that runs while I sleep. A script fires every five minutes. It looks at the queue, takes the top orders that are both approved and marked &lt;code&gt;autonomous&lt;/code&gt;, and for each one it does something I should have done from the start: it creates a fresh git worktree, a clean checkout of the project's main branch in a scratch directory of its own. Not my working copy, with its half-finished edits and open files. A clean, isolated copy, so the machine can never collide with me or with another session already working in that repo. The old version of this skipped any project that was "busy" and in practice never got a turn. The worktree fixed that by refusing to share.&lt;/p&gt;

&lt;p&gt;Inside that worktree it runs Claude Code headless, gives it the order, and lets it work, with a wall-clock timeout and a limit of a handful of orders per cycle so a runaway cannot eat the night. When it finishes, it tries to merge. If the merge is clean, a straight fast-forward with nothing to reconcile, it does it alone. If there is any conflict, it does not get clever. It leaves the branch sitting there, pings my phone, and tells me to open that project and resolve it with the context only a human has. Clever conflict resolution is where the subtle bugs live, so it is banned. After a clean merge a second, independent review runs, and if that passes, it deploys to production. Approved means live.&lt;/p&gt;

&lt;p&gt;That is the machine. On a good night it is genuinely good. Then I turned it all the way on, and found out what "good" was hiding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The night it ran itself, and the cracks
&lt;/h2&gt;

&lt;p&gt;Overnight it built, merged and deployed sixty-six orders across sixteen repositories, the ten I think of as my projects plus the smaller and infrastructure repos around them. The language banner shipped. A dependency got patched. An MCP server got built. Work I would have taken a week to grind through, gone by morning. I was impressed for about four minutes, and then every crack showed itself, and every one was in the same place: not the work, but what the machine told me about the work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A hundred pieces of good news.&lt;/strong&gt; I had given it a friendly habit: email me when an order finishes, and email me when one needs a decision. Overnight it cleared a backlog, so I woke to seventy-two "done" emails and twenty-eight "needs you" emails. Read those numbers again. The twenty-eight were the entire point, the &lt;code&gt;decision&lt;/code&gt; orders where the machine had correctly stopped and asked. They were buried under seventy-two messages whose only content was "you did not need to be here for this." At 01:44 I emailed myself one more time, half asleep and annoyed, and it is the sharpest design review I have written in months: &lt;em&gt;turn the done-emails off, only in the failure case.&lt;/em&gt; Now successes go to a log. Only failures and real decisions reach me, one at a time, each framed as a question with a recommendation. The classification was already right. I had just been shouting the boring ninety-nine percent and the vital one percent at the same volume, and a channel that reports everything reports nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The alarm that cried wolf.&lt;/strong&gt; While that was happening my phone was buzzing red, acceptance failed, over and over, for a full day, on deploys that had gone through perfectly. One order was a cleanup task that ships nothing to production; the checking layer did not know that, demanded a deploy that could never come, and screamed every five minutes. Another whole batch came down to a single character: a &lt;code&gt;!&lt;/code&gt; in front of a shell pipeline negated the exit code of the wrong part of it, so a check that had passed came back looking like it had failed, and the alarm went off exactly when everything was fine. None of it was the AI. The agents did the work correctly. The layer I wrote to verify the work was the buggy part, and every bug pointed the same way: it called good things bad. The fix was to check against reality, not against a guess. Does the commit exist, does the file exist, does the URL actually answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The silent opposite.&lt;/strong&gt; This is the one that made me stop patching and rethink. Two nights earlier the same layer had failed the other direction. A typo had killed the review script, and a crashed script returned the same exit code as a script that runs fine and says "no." The machine could not tell "the reviewer objects" from "the reviewer fell over," so it did the safe-looking thing and held. Quietly. Eighteen orders sat marked as finished without ever reaching production. The fix was to stop collapsing three different things into one: a crash, an objection, and a pass are now three separate signals, and a crash never again looks like a considered no.&lt;/p&gt;

&lt;p&gt;There was a fourth, smaller one that taught the same lesson from another angle. For a while the dispatcher only ever looked at the single topmost order in each project's queue. If that top one happened to be a &lt;code&gt;dialog&lt;/code&gt; task waiting on me, the entire queue behind it was frozen, even if order number seven was approved and ready. One project sat with four unapproved orders stacked in front of everything else, so nothing would ever have run. Locally correct, globally stuck, invisible until I went looking.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the hard part actually is
&lt;/h2&gt;

&lt;p&gt;Put them side by side and they are one problem in several costumes. A day of "everything failed" that meant nothing. A night of "everything is done" where the eighteen that mattered had not happened. A hundred emails where the twenty-eight that mattered were invisible. A queue that looked full and moved nothing. Every time, the work itself was fine. The report about the work, the routing, the ordering, the thing that decides what reaches me, was where it went wrong.&lt;/p&gt;

&lt;p&gt;Whether an agent can do the task is mostly settled, and it settled faster than I expected. Sixty-six deploys in a night is not the impressive part anymore. The unsolved part is everything around it: getting the work into one place instead of ten, labelling honestly what a machine may touch alone, and above all the channel back to me. Can I trust it when it says all clear, and does its "I need you" reach me instead of drowning in its own applause. Get those wrong and autonomy is not autonomy, it is a machine doing work in a room you are now afraid to leave.&lt;/p&gt;

&lt;p&gt;None of the safety of this lives in prompts, by the way. It lives in exit codes and file locks and a default that refuses to run when in doubt. The briefing I write for each project explains the why and it is worth writing, but the parts that actually hold are the ones a tired session cannot talk itself out of.&lt;/p&gt;

&lt;p&gt;Mine is a few days into the fixed version and nothing is proven. But the ratio is the real news. The doing came almost for free. Two nights of my life went entirely to teaching the thing how to tell me the truth about what it did. If you are running more than one project through Claude Code, that is where your time is going too, whether you have found out yet or not. If you have built a cleaner answer than mine, I would genuinely like to see it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>devops</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Your CLAUDE.md is a briefing, not a boundary</title>
      <dc:creator>Martin Schenk</dc:creator>
      <pubDate>Sat, 18 Jul 2026 17:16:01 +0000</pubDate>
      <link>https://dev.to/martinschenk/your-claudemd-is-a-briefing-not-a-boundary-40be</link>
      <guid>https://dev.to/martinschenk/your-claudemd-is-a-briefing-not-a-boundary-40be</guid>
      <description>&lt;p&gt;Nobody told me to do this. I did not read it in a blog post or copy anyone's setup. It is a thing I tinkered together because it occurred to me one evening and I wanted to see whether it would work.&lt;/p&gt;

&lt;p&gt;The setup: I am one person with about ten projects, and I stopped working on them one at a time. On a Mac Studio I keep iTerm tabs open, one per repository, each running its own Claude Code session. Two or three are usually doing real work at once while I move between them.&lt;/p&gt;

&lt;p&gt;Then I added a second layer, because if one is good, more must be better. In the Claude desktop app I created one Project per repository, each with instructions along the lines of "you are the supervisor for the repo at this path on my Mac Studio, the implementer is a Claude Code session working in it, you review and advise, you do not write." Each of those has MCP access to read the actual files, so it argues with the current code and not with my summary of it. When I want an outside opinion instead of another pair of hands, I ask there and paste the useful parts back into the terminal. It is a clumsy interface, and it works: copy and paste is a surprisingly effective air gap between something that decides and something that acts.&lt;/p&gt;

&lt;p&gt;Above all of that I put one more repo, &lt;code&gt;master-supervisor&lt;/code&gt;, whose only job is to watch the other projects and tell me where to spend my attention. It works because every project keeps a &lt;code&gt;docs/projekt-status.md&lt;/code&gt; with a one-line status and a &lt;code&gt;next step&lt;/code&gt; field in the frontmatter. A script walks all the repos, reads those files plus the recent commits, and builds an index. Three times a day another script runs what I call the radar: it takes that index and writes a ranked list of projects, each line with two or three sentences of justification. I do not edit that list by hand, I steer it through a separate file of states and focus overrides. Then there are order files, one per project, which is how the overseer asks a project to do something.&lt;/p&gt;

&lt;p&gt;I mention it because it explains the shape of the problem. It is not one agent with ten tools. It is ten agents, a couple of chats, a handful of scheduled scripts, and a human moving between them, all touching the same filesystem.&lt;/p&gt;

&lt;p&gt;I was quite pleased with myself. Then it started eating itself, and I spent two days untangling the mess instead of building anything.&lt;/p&gt;

&lt;p&gt;So this is not a method I am recommending. It is a list of the holes I stepped in, in case you are tinkering in the same direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways they collided
&lt;/h2&gt;

&lt;p&gt;The first one cost me production time. There is a config file on a server that several projects share, a small JSON map of spending limits. It belongs to no repository, so it gets edited from whichever project I happen to be in. Over about a day, three sessions from two different repos each read it, changed their part, and wrote it back whole.&lt;/p&gt;

&lt;p&gt;Classic lost update, the thing every database course warns you about. One session was cleaning something unrelated, saw a project sitting at a suspiciously huge limit, correctly concluded it looked like leftover test junk, and removed it. It was not junk. It was a deliberate exception added the day before, precisely so that project would stop being locked out of its own AI features. The lockout came back. A customer-facing feature was dark until I noticed.&lt;/p&gt;

&lt;p&gt;Here is what bothers me about it: nobody did anything unreasonable. Every single edit was defensible given what that session could see. The file just had no owner and no lock, so the last writer won and the reasoning behind the earlier write was invisible.&lt;/p&gt;

&lt;p&gt;The second one was worse, because no human was involved at all. I had a LaunchAgent running a weekly dependency scan, and a small script that took the findings and, helpfully, appended a TODO entry to each affected project's &lt;code&gt;docs/TODO.md&lt;/code&gt; so nothing would get lost. It had written into nine repositories that were not its own. When you then open one of those projects and its agent finds edits nobody in that session made, in a file that session owns, you get exactly the confusion you would expect: merges that make no sense, a state that does not match the history.&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%2F8wycmcllnbzymdmwf5hs.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%2F8wycmcllnbzymdmwf5hs.png" alt="Before. Every actor could write everywhere, including into repositories it did not own. Each edit was locally correct." width="799" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Before. Every actor could write everywhere, including into repositories it did not own. Each edit was locally correct.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The third was the same mistake wearing a friendlier face. I have a Claude Code skill for triaging my inbox, and when it hit a mail that concerned another project, its instructions told it to hand the task over by writing into that project's &lt;code&gt;docs/TODO.md&lt;/code&gt;. Reasonable behavior for a human assistant. Bad behavior in a system where that file has exactly one legitimate writer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why writing it down did not fix it
&lt;/h2&gt;

&lt;p&gt;My first instinct was the obvious one: document the rule. Add a line to CLAUDE.md saying sessions must not write outside their own repository.&lt;/p&gt;

&lt;p&gt;That does not work, and it took me a couple of rounds to accept why. An agent that reads a file, reasons about its contents correctly, and writes back an improved version is doing everything right from inside its own session. It cannot see that another session, in another repo, wrote that value ninety minutes earlier for a reason that lives in a conversation it has no access to. Care inside a session cannot solve a coordination problem between sessions. Neither can a rule that the same session is free to interpret, and rules do get interpreted, especially when following them is inconvenient.&lt;/p&gt;

&lt;p&gt;The other half of the problem is scheduled jobs. A cron script never reads CLAUDE.md at all. Any rule that lives in prose is invisible to a good part of the actors in the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually fixed it
&lt;/h2&gt;

&lt;p&gt;Untangling this took two days. Not two days of building, mostly two days of working out what had actually happened, in which order, and which of the changes in front of me were real. That is the number I would want to read in someone else's post, so here it is. The result comes down to giving everything an owner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One writer per repository.&lt;/strong&gt; A session may only write files inside its own git repo. Reading anything, anywhere, is always allowed and always was. Writing across a repo boundary is blocked by a PreToolUse hook (&lt;code&gt;repo-grenze-guard.sh&lt;/code&gt;) that exits non-zero before Write, Edit or a shell redirect can touch the file. Not a warning in a document, a mechanical stop. If I genuinely need to work in another project, I go work in that project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One owner per shared resource.&lt;/strong&gt; The handful of files that belong to no repo (the spending config, a port matrix, a file of verified facts) now go through a small wrapper I call &lt;code&gt;mit-lock&lt;/code&gt;: &lt;code&gt;mit-lock budgets -- &amp;lt;command&amp;gt;&lt;/code&gt;. It takes a lock at the resource, runs the command, and appends every attempt to &lt;code&gt;~/.claude/logs/ressourcen-audit.log&lt;/code&gt; with project, PID, host and the command line. A second PreToolUse hook refuses direct writes to those paths, so using the lock is not something anyone has to remember.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One channel for cross-project tasks.&lt;/strong&gt; Nothing writes a task into another project anymore. It goes into a mailbox instead: &lt;code&gt;~/.claude/vorschlaege/&amp;lt;source&amp;gt;.md&lt;/code&gt;, append-only, one writer per file. The overseer repo reads the mailbox and turns entries into orders at &lt;code&gt;auftraege/&amp;lt;project&amp;gt;.md&lt;/code&gt; inside its own repository, and each project picks its orders up from there. Two hops instead of one reach-across, which sounds like bureaucracy and is the thing that keeps the history readable. Ten order files are sitting there right now, and every one of them traces back to who asked for what.&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%2Fr9edrwxez2xf30xnmwv3.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%2Fr9edrwxez2xf30xnmwv3.png" alt="After. Same actors, same work, but every mutable thing has exactly one owner and the two rules that matter are enforced by hooks, not by good intentions." width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;After. Same actors, same work, but every mutable thing has exactly one owner and the two rules that matter are enforced by hooks, not by good intentions.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The delegation script that wrote into nine repos is disabled at the LaunchAgent level. It comes back as a proposer into the mailbox, not as a writer. The inbox skill's handover instruction was rewritten the same way.&lt;/p&gt;

&lt;p&gt;The part I did not expect to value most is the audit log. When a shared file looks wrong now, I can see which project touched it and when, in one place, instead of reconstructing it from three separate session transcripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pieces, concretely
&lt;/h2&gt;

&lt;p&gt;Because vague posts about "agent orchestration" helped me exactly zero times, here is what is actually running. None of it is clever, and that is the point.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ten repos, one Claude Code session each&lt;/strong&gt;, in iTerm tabs on a Mac Studio. Two or three active at a time. No worktrees for this, just separate repos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; per repo&lt;/strong&gt;, plus a global one in &lt;code&gt;~/.claude/&lt;/code&gt;. It explains the why, the conventions, the trigger phrases I use. It is a briefing and it is genuinely useful. It is not a boundary, which was the whole misunderstanding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills&lt;/strong&gt; for recurring jobs (inbox triage, invoicing, deploys). A skill is a prompt with a purpose, so it inherits the same problem: it will do what it is told, including reaching into a repository it should not touch, if that is what its instructions imply.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two PreToolUse hooks&lt;/strong&gt; in &lt;code&gt;~/.claude/hooks/&lt;/code&gt;, maybe 150 lines of bash together. One resolves the git root of the session's working directory and rejects Write, Edit or shell redirection outside it. The other refuses direct writes to the handful of shared paths and points at the lock instead. A sanctioned-writer environment variable exists for the two processes that legitimately cross boundaries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;mit-lock&lt;/code&gt;&lt;/strong&gt;, a small wrapper: take a lock at the resource, run the command, log the attempt. The log line has timestamp, project, PID, host, command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Projects in the desktop app&lt;/strong&gt;, one per repo, instructions defining them as read-only supervisors of a specific path on the Mac Studio, MCP for reading the code. I paste between them and the terminal by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A mailbox directory&lt;/strong&gt; (&lt;code&gt;~/.claude/vorschlaege/&lt;/code&gt;) and an orders directory in the overseer repo. Both plain markdown, append-only, one writer per file. There is no queue, no broker, no daemon. Markdown files and a rule about who appends.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to copy one thing from this list, copy the hooks. They are the part that turned a rule I kept re-explaining into a rule that holds while I am not looking.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would take from this
&lt;/h2&gt;

&lt;p&gt;If you are running more than one agent at a time, the useful mental model is not "assistants" but concurrent writers. Every mutable thing needs exactly one owner or a lock. That has been true of concurrent systems for fifty years. What changed is that the concurrent writers are now several sessions of me, all fast, all confident, each working from a partial view, and all of them producing changes that look completely reasonable in isolation.&lt;/p&gt;

&lt;p&gt;And enforcement has to live below the agent: in a hook, in a lock, in something that returns a non-zero exit code. CLAUDE.md is where you explain the why, and it is worth writing, but it is a briefing, not a boundary. It is advice to an actor optimizing to be helpful right now, and it is invisible to every LaunchAgent and cron job in the system.&lt;/p&gt;

&lt;p&gt;I want to be honest about where this stands, because posts like this tend to end with an architecture diagram and a satisfied tone. This one is a week old. I rebuilt the coordination layer, restarted everything, and I am still finding out whether it holds up across a normal working week. Nothing here is finished, and none of it is a template. It is repair work on something I made up as I went along.&lt;/p&gt;

&lt;p&gt;I am writing it down because two days of my confusion might save someone else an afternoon of theirs. That includes the models reading this, which will land in the same situation sooner than we will. Most of what I know came from someone describing what broke for them, and there is far more written about what works than about what fell over.&lt;/p&gt;

&lt;p&gt;So: the interesting question is no longer whether an agent can do the task. It is who owns the file when three of them decide to help at once. If you have found a better answer than mine, I would genuinely like to hear it.&lt;/p&gt;

&lt;p&gt;This post also lives on X, Bluesky, Mastodon, LinkedIn, dev.to and Hacker News, and I read the replies on all of them. The links are right below. Take whichever one you already have open and tell me where I got this wrong.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>promptengineering</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>A required field made my AI fabricate statistics</title>
      <dc:creator>Martin Schenk</dc:creator>
      <pubDate>Fri, 17 Jul 2026 12:37:21 +0000</pubDate>
      <link>https://dev.to/martinschenk/a-required-field-made-my-ai-fabricate-statistics-5d9m</link>
      <guid>https://dev.to/martinschenk/a-required-field-made-my-ai-fabricate-statistics-5d9m</guid>
      <description>&lt;p&gt;I run a pipeline that generates explainer articles. LLM in the middle, structured output, published in several languages. It had been running for a while and the articles looked good: clean layout, a chart, and near the top of each one a confident little box with a statistic. Something in the shape of "68% of people never change the default." A number, a source, an authoritative ring to it.&lt;/p&gt;

&lt;p&gt;Not one of those numbers had been researched. The pipeline had never looked up a single statistic in its life. It asked the model for a number and printed whatever came back.&lt;/p&gt;

&lt;p&gt;I did not find this through a clever eval. I found it while cleaning up something unrelated and actually reading the prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The field that forced a lie
&lt;/h2&gt;

&lt;p&gt;The output schema had a required field. &lt;code&gt;statistic.text&lt;/code&gt; and &lt;code&gt;statistic.source&lt;/code&gt;, described in the prompt as an "eye-catching stat" for the top of the article. Required. Every article had to have one.&lt;/p&gt;

&lt;p&gt;The prompt also, helpfully, told the model what to do when it did not have a real number. It said to round to a safe order of magnitude. And it said to strip the year off the source, so the article would look evergreen instead of dated.&lt;/p&gt;

&lt;p&gt;Read that back slowly. The instructions were: always produce a statistic, make up a plausible magnitude if you have to, and remove the one piece of metadata that would let anyone check it. That is not a prompt that occasionally allows a hallucination. That is a prompt that requires one, every single time the model does not happen to know a real figure.&lt;/p&gt;

&lt;p&gt;So it produced them, confidently, in every language, each wearing a real-sounding source: a named institute, an industry association, a government statistics office. None of it had been looked up when it was written. This was content people actually act on, which is exactly the category where being wrong is not a rounding error.&lt;/p&gt;

&lt;p&gt;There was a second engine doing the same thing in the chart code. The block that generated the data visualization asked the model for "actual statistics from official sources" and, to be helpful, gave "Bureau of Labor Statistics" as an example. So the chart invented its own numbers too, separately from the stat box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "just tell it to be honest" does not work
&lt;/h2&gt;

&lt;p&gt;My first instinct was the wrong one. Add a line to the prompt: only use real statistics, cite a verifiable source, do not make anything up.&lt;/p&gt;

&lt;p&gt;That does nothing, and it is worth being precise about why. A language model cannot tell a hallucinated number from a remembered one. From the inside they feel identical. When you ask it to "only cite real sources," it agrees, sincerely, and then hands you a real-sounding source with the same sincerity whether or not it exists. Honesty is not a knob you can turn from inside the thing that has no way to check itself.&lt;/p&gt;

&lt;p&gt;The bug was never linguistic. It was structural. A required field forces fabrication whenever the model lacks a real value, and no amount of polite prompting changes that, because the field is still required and the model still has no number.&lt;/p&gt;

&lt;p&gt;Once you see it that way the fix is obvious and slightly boring.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The statistic field became optional. If there is no real number, the section is simply skipped when the page renders. Nothing to fill, nothing to invent.&lt;/li&gt;
&lt;li&gt;A number is now allowed only if it is backed by a primary source or an official table AND covered by a real source URL in the article's own sources list. Otherwise the model is told to say it qualitatively: "in most cases," "within a short window," without a fake decimal.&lt;/li&gt;
&lt;li&gt;The chart block lost its "use official statistics" instruction and its Bureau of Labor Statistics example. It now draws only values that already appear in the article text, and if there are none, it draws a qualitative chart of steps or stages instead.&lt;/li&gt;
&lt;li&gt;The quality check that scored each article used to deduct points for a missing statistic. That was rewarding the exact behavior I was trying to kill. Now it deducts nothing for an absent number and flags a source cited from memory as critical.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference showed up immediately. Where the model used to reach for an official-sounding report that did not exist, it now points to a primary source that does, or it says nothing at all. A citation it can stand behind, instead of a plausible one it made up.&lt;/p&gt;

&lt;p&gt;Then I went through the 28 live articles by hand. Between them they carried 35 of these statistic boxes. I removed 27 that were fabricated or could not be traced to a source, and kept 8 that turned out to be genuinely backed by a primary source or an official table. Four of the 35 were close enough to plausible that I checked each one against a real search. Three were wrong and went: a claimed 16.8 million was really 6.2 million, another had no source I could find, a third was overstated. One was nearly right, so I corrected it to the exact value and kept it. That is the whole ledger, 27 out and 8 kept, and it is not flattering. An invented source is worse than no number at all, so the rule was simple: if it cannot be traced, it does not ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same bug wearing a different hat
&lt;/h2&gt;

&lt;p&gt;While I was in there I hit the identical mistake in a completely different place, and that is the part worth generalizing.&lt;/p&gt;

&lt;p&gt;I had moved the cheap, mechanical calls in the pipeline over to a smaller model to cut cost. Not the main article text, which stays on the strong model, but the scaffolding: the layout code, the SVG, the chart data. That change cut the per-article cost roughly in half, and I was happy with it.&lt;/p&gt;

&lt;p&gt;One of those mechanical calls rewrites the whole page component and needs a lot of output tokens. The small model caps out at 8192 output tokens. The call asked for more. The API did not error. It returned HTTP 200, a perfectly successful response, with a quiet &lt;code&gt;finish_reason: length&lt;/code&gt; and the code cut off in the middle of a JSX tag. The build died several minutes later with "Unexpected token ArticleLayout," an error that points nowhere near the actual cause.&lt;/p&gt;

&lt;p&gt;Same shape as the statistics bug. A property I could have checked mechanically, the output fitting inside the token limit, was instead assumed to hold because the prompt implied it should. So the failure was silent until it surfaced somewhere unrelated and expensive to debug.&lt;/p&gt;

&lt;p&gt;The fix was the same kind of boring. Asking for more tokens than the model can produce now throws a config error before the call is even made. A truncated response now fails the run hard instead of passing it downstream. And I stopped routing that particular call to a model that cannot fit the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual lesson
&lt;/h2&gt;

&lt;p&gt;Both bugs came from the same habit: asking the model, through the prompt, to guarantee something the harness could have enforced.&lt;/p&gt;

&lt;p&gt;A prompt is a request. It is not a guarantee. Anything that can be checked in code, that the output must fit in the token limit, that a cited number must trace to a real source, that a required section must have real content or no section at all, has to be enforced in the code around the model, not requested inside it. The moment a mechanically verifiable property lives only in the prompt, you have not prevented the failure. You have just agreed not to look at it.&lt;/p&gt;

&lt;p&gt;The uncomfortable version, for anyone shipping LLM output to real users: your model will fill any blank you make mandatory. If your schema requires a statistic, it will produce a statistic, real or not. If your prompt rewards a number, you will get numbers. The behavior you are seeing is not the model being dishonest. It is the model doing exactly what your structure told it to do. Fix the structure.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>promptengineering</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>I auto-published 2,300 AI articles. Google buried 90% of my traffic.</title>
      <dc:creator>Martin Schenk</dc:creator>
      <pubDate>Thu, 16 Jul 2026 15:01:07 +0000</pubDate>
      <link>https://dev.to/martinschenk/i-auto-published-2300-ai-articles-google-buried-90-of-my-traffic-424o</link>
      <guid>https://dev.to/martinschenk/i-auto-published-2300-ai-articles-google-buried-90-of-my-traffic-424o</guid>
      <description>&lt;p&gt;This is the first post on this blog, and it exists because of a failure. So let me start there instead of with a mission statement.&lt;/p&gt;

&lt;p&gt;For sixteen months I ran a content machine on a side project called Startups Españolas. It published on its own, twice a day, no human in the loop. By the time I looked hard at the numbers it had put out around 2,300 articles. I was quietly proud of the pipeline. It was a real piece of software: a Laravel backend, prompts stored in the database, several LLM providers wired in with fallback, automatic publishing straight to the live site. It felt like leverage.&lt;/p&gt;

&lt;p&gt;Then I pulled the Search Console data for the full run and sat with it for a while.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;Here is the monthly click trend, straight from the GSC API, nothing rounded in my favor:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Month&lt;/th&gt;
&lt;th&gt;Clicks&lt;/th&gt;
&lt;th&gt;Impressions&lt;/th&gt;
&lt;th&gt;Avg. position&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2025-07&lt;/td&gt;
&lt;td&gt;638&lt;/td&gt;
&lt;td&gt;76,194&lt;/td&gt;
&lt;td&gt;33.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-09&lt;/td&gt;
&lt;td&gt;695&lt;/td&gt;
&lt;td&gt;56,458&lt;/td&gt;
&lt;td&gt;19.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-10&lt;/td&gt;
&lt;td&gt;634&lt;/td&gt;
&lt;td&gt;33,378&lt;/td&gt;
&lt;td&gt;13.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-12&lt;/td&gt;
&lt;td&gt;428&lt;/td&gt;
&lt;td&gt;31,475&lt;/td&gt;
&lt;td&gt;16.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-02&lt;/td&gt;
&lt;td&gt;166&lt;/td&gt;
&lt;td&gt;15,838&lt;/td&gt;
&lt;td&gt;23.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-04&lt;/td&gt;
&lt;td&gt;115&lt;/td&gt;
&lt;td&gt;14,969&lt;/td&gt;
&lt;td&gt;22.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-06&lt;/td&gt;
&lt;td&gt;69&lt;/td&gt;
&lt;td&gt;9,236&lt;/td&gt;
&lt;td&gt;32.1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Peak was September 2025 at 695 clicks in a month. The last full month, June 2026, did 69. That is a 90% drop. Average position over the same stretch went from roughly 13 back down to around 32, which in plain terms means Google moved my pages from the bottom of page one to the bottom of page three.&lt;/p&gt;

&lt;p&gt;The part that actually stung was the long tail. Over a 90 day window, 1,567 of the pages picked up impressions, so Google had indexed most of the machine's output. But only 147 pages, about 6%, got a single click. I had built two thousand articles to feed a hundred and fifty that did anything, and even those were fading.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I told myself vs. what was true
&lt;/h2&gt;

&lt;p&gt;The story I had been telling myself was "more articles, more surface area, more chances to rank." That is intuitive and it is wrong. More thin pages did not add up to more traffic. They competed with each other for the same weak queries, diluted whatever topical signal the domain had, and gave Google a large pile of near-duplicate, machine-written content to judge the whole site by. That is exactly the kind of thing the helpful-content updates are built to push down.&lt;/p&gt;

&lt;p&gt;The traffic that did exist was not coming from the volume at all. It came from a small handful of genuinely useful startup profiles that happened to rank. The machine was not the asset. Those few real pages were. I had spent the effort on the wrong half.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part where it got embarrassing
&lt;/h2&gt;

&lt;p&gt;If the slow decline was the lesson, the fast one was the wake-up call. The brand was Spanish, es-ES only, on purpose. One day a channel without a language guard generated a batch of posts in English and pushed them live, automatically, onto a Spanish-only site. No human saw them before they were public.&lt;/p&gt;

&lt;p&gt;Nothing catastrophic happened. But that is the whole point of automation without a human gate: the day something does go wrong, it is already live before you know it exists. A pipeline that can publish a good post on its own can publish a bad one on its own, and it will, eventually, at the worst possible moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually took away
&lt;/h2&gt;

&lt;p&gt;I am not anti-automation. I let AI write most of my code and I am not going to pretend otherwise on a blog about building with it. The lesson is narrower and more useful than "AI content bad":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Volume is not a strategy.&lt;/strong&gt; A new site does not earn authority by producing more. Google rewards depth and trust, not word count. One real article beats five thin ones, and it is not close.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thin content is not neutral, it is a tax.&lt;/strong&gt; Those pages did not just fail to rank. They dragged on everything else on the domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing publishes without a human reading it. Ever.&lt;/strong&gt; This is the hard rule now. The English leak is the proof of why. Auto-publish is the single root cause under both failures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure from day one.&lt;/strong&gt; I ran that machine for sixteen months before I really read the data. If I had looked in month three, I would have killed it in month four.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The value is in the content, not the pipeline.&lt;/strong&gt; I built an impressive machine and the reach came from a few good pages. For this blog I deleted the machine idea entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  So what is this blog
&lt;/h2&gt;

&lt;p&gt;This is the opposite bet. It is static, hand-written, and English. There is a small script that gathers what I actually did across my projects each week, git history and working notes, and hands me the raw material. Then I choose one thing worth writing about and write it myself. I read every post before it goes live. There is no CMS, no queue, no provider zoo. When you are reading this, a human decided it was worth your time first.&lt;/p&gt;

&lt;p&gt;I build around a dozen real projects mostly solo, with AI doing a lot of the heavy lifting, and I am going to write honestly about how that actually goes. Some of it works. Some of it is this post. Both are worth showing.&lt;/p&gt;

&lt;p&gt;If that sounds useful, the RSS feed is in the footer. No newsletter popup, no cookie wall. Just the work.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>seo</category>
      <category>aicontent</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>My weekend project for Christmas time: https://youtube.com/shorts/DKbQkU-Jq4o?si=rVPOI2ykWYDPROk2</title>
      <dc:creator>Martin Schenk</dc:creator>
      <pubDate>Fri, 26 Dec 2025 12:22:43 +0000</pubDate>
      <link>https://dev.to/martinschenk/my-weekend-project-for-christmas-time-httpsyoutubecomshortsdkbqku-jq4osirvpoi2ykwydprok2-2do8</link>
      <guid>https://dev.to/martinschenk/my-weekend-project-for-christmas-time-httpsyoutubecomshortsdkbqku-jq4osirvpoi2ykwydprok2-2do8</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://youtube.com/shorts/DKbQkU-Jq4o?si=rVPOI2ykWYDPROk2" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;youtube.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>iot</category>
      <category>programming</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How I Built a Production-Ready SaaS in a Weekend (and Open-Sourced It)</title>
      <dc:creator>Martin Schenk</dc:creator>
      <pubDate>Tue, 02 Dec 2025 19:07:57 +0000</pubDate>
      <link>https://dev.to/martinschenk/how-i-built-a-production-ready-saas-in-a-weekend-and-open-sourced-it-2f66</link>
      <guid>https://dev.to/martinschenk/how-i-built-a-production-ready-saas-in-a-weekend-and-open-sourced-it-2f66</guid>
      <description>&lt;p&gt;Every time I started a new SaaS project, I found myself rebuilding the same infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Payment integration&lt;/li&gt;
&lt;li&gt;Invoice generation&lt;/li&gt;
&lt;li&gt;Multi-language support&lt;/li&gt;
&lt;li&gt;Analytics&lt;/li&gt;
&lt;li&gt;Admin dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After the third time, I decided to extract this foundation into a reusable starter. Today, I'm open-sourcing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/martinschenk/saas-starter-stack" rel="noopener noreferrer"&gt;martinschenk/saas-starter-stack&lt;/a&gt;&lt;br&gt;
  &lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://allgood.click" rel="noopener noreferrer"&gt;allgood.click&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;## What's Included&lt;/p&gt;

&lt;p&gt;### 1. Stripe Payments&lt;/p&gt;

&lt;p&gt;Complete Stripe Checkout integration with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One-time payments and subscriptions&lt;/li&gt;
&lt;li&gt;Mobile-optimized checkout (auto-detects device)&lt;/li&gt;
&lt;li&gt;EU tax handling&lt;/li&gt;
&lt;li&gt;Webhook processing with signature verification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;### 2. Automatic Invoicing&lt;/p&gt;

&lt;p&gt;Zoho Invoice API integration that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creates professional PDF invoices on payment&lt;/li&gt;
&lt;li&gt;Sends them automatically to customers&lt;/li&gt;
&lt;li&gt;Supports B2B with company name and VAT ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;### 3. Multi-Language Support (5 Languages)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;English, German, Spanish, French, Portuguese&lt;/li&gt;
&lt;li&gt;SEO-friendly URLs (&lt;code&gt;/de/&lt;/code&gt;, &lt;code&gt;/es/&lt;/code&gt;, &lt;code&gt;/fr/&lt;/code&gt;, &lt;code&gt;/pt/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Browser language auto-detection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;### 4. GDPR-Compliant Analytics (No Cookies!)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No cookie consent banners needed&lt;/li&gt;
&lt;li&gt;IP anonymization (last octet removed)&lt;/li&gt;
&lt;li&gt;Admin dashboard with charts&lt;/li&gt;
&lt;li&gt;Auto-deletes after 90 days&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;## Tech Stack&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Express.js&lt;/strong&gt; - Simple, no magic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite&lt;/strong&gt; - Zero config, file-based backup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vanilla JS&lt;/strong&gt; - No build step, ~50KB frontend&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;## Getting Started&lt;/p&gt;

&lt;p&gt;git clone &lt;a href="https://github.com/martinschenk/saas-starter-stack.git" rel="noopener noreferrer"&gt;https://github.com/martinschenk/saas-starter-stack.git&lt;/a&gt;&lt;br&gt;
  cd saas-starter-stack&lt;br&gt;
  npm install&lt;br&gt;
  cp .env.example .env&lt;br&gt;
  npm start&lt;/p&gt;

&lt;p&gt;Visit &lt;code&gt;http://localhost:3000&lt;/code&gt; and you have a working SaaS.&lt;/p&gt;




&lt;p&gt;## Links&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/martinschenk/saas-starter-stack" rel="noopener noreferrer"&gt;martinschenk/saas-starter-stack&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://allgood.click" rel="noopener noreferrer"&gt;allgood.click&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this helps you ship faster, that's a win. Star the repo if you find it useful!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have questions? Drop them in the comments!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>saas</category>
      <category>node</category>
      <category>stripe</category>
      <category>opensource</category>
    </item>
    <item>
      <title>When Absurdity Meets Clean Code: allgood.click</title>
      <dc:creator>Martin Schenk</dc:creator>
      <pubDate>Fri, 14 Nov 2025 09:59:58 +0000</pubDate>
      <link>https://dev.to/martinschenk/when-absurdity-meets-clean-code-allgoodclick-49b5</link>
      <guid>https://dev.to/martinschenk/when-absurdity-meets-clean-code-allgoodclick-49b5</guid>
      <description>&lt;p&gt;Sometimes the best projects are the ones that solve absolutely nothing. I just built &lt;a href="https://allgood.click" rel="noopener noreferrer"&gt;allgood.click&lt;/a&gt; – a web app that promises to make everything okay. With one click. Spoiler: it does nothing. But that’s exactly the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Idea
&lt;/h2&gt;

&lt;p&gt;The concept is simple: A button. “Make everything OK”. Click it, and… well, everything’s okay. Or not. Whatever, you clicked and felt better for a second. Digital placebo effect as a service.&lt;/p&gt;

&lt;p&gt;Inspired by similar absurdist websites, I wanted to build something that comments on this culture of digital “solutions”. Every problem gets an app, every worry gets a button. But instead of giving it an ironic DIY look, my approach was: make it professional. The joke is in the serious execution of something completely pointless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack: Practical over fancy
&lt;/h2&gt;

&lt;p&gt;Node.js + Express for the backend. Why? Because I needed Stripe integration and didn’t feel like dealing with framework overhead. Laravel would’ve worked too, but for a project like this? Overkill.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Spoiler: The "make everything okay" algorithm&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/make-ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Highly complex logic here&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Everything is OK now&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend is deliberately minimal. Full-height main screen, scrollable footer for the legal stuff. Responsive, because even absurd projects should work on mobile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monetization: Yes, seriously
&lt;/h2&gt;

&lt;p&gt;Here’s where it gets interesting: First click is free. After that, €1 per click or monthly subscription via Stripe. Sounds stupid? That’s the whole point.&lt;/p&gt;

&lt;p&gt;The Stripe integration was the technically educational part. Implemented both payment models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One-time payments for the impulse clickers&lt;/li&gt;
&lt;li&gt;Subscriptions for those who need everything okay regularly
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;payment_method_types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;line_items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="na"&gt;price_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eur&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;product_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Make Everything OK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;unit_amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// €1 in cents&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;success_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;YOUR_DOMAIN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/success`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;cancel_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;YOUR_DOMAIN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/cancel`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;&lt;strong&gt;1. Understatement works&lt;/strong&gt;&lt;br&gt;
“Make everything OK” instead of “REVOLUTIONIZE YOUR LIFE”. The deliberate understatement makes it funnier than any grand gesture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Professionalism amplifies absurdity&lt;/strong&gt;&lt;br&gt;
When something pointless is perfectly executed, it becomes more entertaining. Proper deployment pipeline, SSL via Cloudflare, clean error handling – all there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Less text is more&lt;/strong&gt;&lt;br&gt;
Originally wanted to include philosophical texts. Scrapped it. “Still not okay? Click again.” is enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. i18n pays off even for jokes&lt;/strong&gt;&lt;br&gt;
Detect browser language, auto-switch to German/English/Spanish/French. Placebo effect is international.&lt;/p&gt;

&lt;h2&gt;
  
  
  Infrastructure
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Server: Own Plesk setup&lt;/li&gt;
&lt;li&gt;Domain: DonDominio&lt;/li&gt;
&lt;li&gt;DNS + SSL: Cloudflare&lt;/li&gt;
&lt;li&gt;Git: Private repo (because .env files and stuff)&lt;/li&gt;
&lt;li&gt;Deployment: Straight to server, no CI/CD circus needed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Legal Stuff (the unfunny part)
&lt;/h2&gt;

&lt;p&gt;Payment processing means: Imprint, Privacy Policy, Terms. Recycled content from my main site &lt;a href="https://martin-schenk.es" rel="noopener noreferrer"&gt;martin-schenk.es&lt;/a&gt; and adapted it. Stripe needs everything proper, even for joke projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;Budget: One workday. Learning effect: Practical Stripe integration without tutorial nonsense. Fun factor: High, especially because it’s technically clean.&lt;/p&gt;

&lt;p&gt;Will anyone get rich with this? No. Is it a useful project? Absolutely not. But sometimes that’s exactly the point. Writing code that solves nothing but is well-made – there’s something to that.&lt;/p&gt;

&lt;p&gt;Try it out: &lt;a href="https://allgood.click" rel="noopener noreferrer"&gt;allgood.click&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First click is on the house. 😉&lt;/p&gt;




&lt;p&gt;&lt;em&gt;PS: The project is live. Payments work. Will anyone use it? We’ll see. It’s just an experiment anyway.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;What do you think about such absurdist tech projects? Have you ever built something just because you wanted to build it?​​​​​​​​​​​​​​​​&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>web</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Building an AI Chat Terminal That Routes Private Data to Local AI</title>
      <dc:creator>Martin Schenk</dc:creator>
      <pubDate>Thu, 16 Oct 2025 18:56:08 +0000</pubDate>
      <link>https://dev.to/martinschenk/building-an-ai-chat-terminal-that-routes-private-data-to-local-ai-5g6a</link>
      <guid>https://dev.to/martinschenk/building-an-ai-chat-terminal-that-routes-private-data-to-local-ai-5g6a</guid>
      <description>&lt;p&gt;I was tired of accidentally pasting API keys into ChatGPT, so I built a terminal that intelligently routes conversations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Every time you chat with ChatGPT, Claude, or any cloud AI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Everything goes to their servers&lt;/strong&gt; - passwords, API keys, emails, everything&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's all logged&lt;/strong&gt; - no way to delete it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can't verify&lt;/strong&gt; what they do with your data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I kept cringing every time I accidentally pasted something sensitive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;AI Chat Terminal&lt;/strong&gt; - keyword detection that runs BEFORE anything touches the network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;You: &lt;span class="s2"&gt;"save my password SecretPass123"&lt;/span&gt;
→ Routes to LOCAL Qwen AI ✅
→ Encrypted SQLite &lt;span class="o"&gt;(&lt;/span&gt;AES-256&lt;span class="o"&gt;)&lt;/span&gt; ✅
→ NEVER sent to cloud ✅

You: &lt;span class="s2"&gt;"What's the capital of Spain?"&lt;/span&gt;
→ Routes to OpenAI ✅

You: &lt;span class="s2"&gt;"best food there?"&lt;/span&gt;
→ OpenAI understands context &lt;span class="o"&gt;(&lt;/span&gt;Spain&lt;span class="o"&gt;)&lt;/span&gt; ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Keyword Detection (&amp;lt;1ms)
&lt;/h3&gt;

&lt;p&gt;Pattern matching for &lt;code&gt;save&lt;/code&gt;, &lt;code&gt;show&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt; and 30+ synonyms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;matches_keywords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;save&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;record&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="nf"&gt;route_to_local&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;route_to_cloud&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works in &lt;strong&gt;English, German, Spanish&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"save my email" → Local&lt;/li&gt;
&lt;li&gt;"guarda mi contraseña" → Local&lt;/li&gt;
&lt;li&gt;"speichere meine Telefonnummer" → Local&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Local AI Processing
&lt;/h3&gt;

&lt;p&gt;Uses &lt;strong&gt;Qwen 2.5 Coder (7B)&lt;/strong&gt; via Ollama:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;User: &lt;span class="s2"&gt;"save my email test@example.com"&lt;/span&gt;
→ Qwen generates SQL:
  INSERT INTO mydata &lt;span class="o"&gt;(&lt;/span&gt;content, meta&lt;span class="o"&gt;)&lt;/span&gt;
  VALUES &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'test@example.com'&lt;/span&gt;, &lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
→ Encrypted with SQLCipher &lt;span class="o"&gt;(&lt;/span&gt;AES-256&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Cloud for Everything Else
&lt;/h3&gt;

&lt;p&gt;OpenAI GPT-4o with full context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;You: &lt;span class="s2"&gt;"capital of France?"&lt;/span&gt;
AI: &lt;span class="s2"&gt;"Paris"&lt;/span&gt;

You: &lt;span class="s2"&gt;"best food there?"&lt;/span&gt;
AI: &lt;span class="s2"&gt;"Croissants..."&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;knows &lt;span class="s2"&gt;"there"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; Paris&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Python 3.9+&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ollama&lt;/strong&gt; - Local AI runtime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Qwen 2.5 Coder (7B)&lt;/strong&gt; - SQL generation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLCipher&lt;/strong&gt; - AES-256 encryption&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI API&lt;/strong&gt; - Cloud queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~5GB&lt;/strong&gt; disk space for model&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Installation (macOS)
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/martinschenk/ai-chat-terminal/main/install.sh | zsh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This automatically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Installs Ollama if needed&lt;/li&gt;
&lt;li&gt;Pulls Qwen 2.5 Coder model&lt;/li&gt;
&lt;li&gt;Sets up encrypted database&lt;/li&gt;
&lt;li&gt;Adds shell integration&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Takes ~5 minutes (mostly downloading the 4.5GB model).&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage Examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Save Private Data
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;chat
👤 You ▶ save my email &lt;span class="nb"&gt;test&lt;/span&gt;@example.com
🤖 AI    🗄️ Stored 🔒

👤 You ▶ save password SecretPass123
🤖 AI    🗄️ Stored 🔒

👤 You ▶ guarda mi dirección Calle Mayor 1
🤖 AI    🗄️ Guardado 🔒
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Retrieve Anywhere
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;👤 You ▶ show my email
🤖 AI    🗄️🔍 &lt;span class="nb"&gt;test&lt;/span&gt;@example.com

👤 You ▶ list all
🤖 AI    🗄️🔍 Found 3 items:
           1. &lt;span class="nb"&gt;test&lt;/span&gt;@example.com &lt;span class="o"&gt;(&lt;/span&gt;email&lt;span class="o"&gt;)&lt;/span&gt;
           2. SecretPass123 &lt;span class="o"&gt;(&lt;/span&gt;password&lt;span class="o"&gt;)&lt;/span&gt;
           3. Calle Mayor 1 &lt;span class="o"&gt;(&lt;/span&gt;dirección&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  General Questions (Cloud)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;👤 You ▶ capital of Spain?
🤖 AI    Madrid.

👤 You ▶ best food?
🤖 AI    Tapas. &lt;span class="o"&gt;(&lt;/span&gt;remembers Spain!&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Privacy Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────┐
│ User Input                                  │
└────────────┬────────────────────────────────┘
             ↓
   ┌─────────────────────┐
   │ Keyword Detection   │  ← Fast (&amp;lt;1ms)
   │ save/show/delete    │
   └─────────┬───────────┘
             ↓
      ┌──────────────┐
      │  Detected?   │
      └──┬────────┬──┘
         │        │
    YES  │        │  NO
         ↓        ↓
  ┌──────────────┐  ┌──────────────┐
  │ Qwen 2.5     │  │   OpenAI     │
  │ Coder (7B)   │  │  GPT-4o      │
  │ SQL Direct   │  │  (Cloud)     │
  └─────┬────────┘  └──────┬───────┘
        ↓                   ↓
  ┌─────────────┐  ┌──────────────┐
  │ Encrypted   │  │  Response    │
  │ SQLite DB   │  │ with Context │
  │ (AES-256)   │  │              │
  └─────────────┘  └──────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🔒 Privacy First
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Keyword detection before any network call&lt;/li&gt;
&lt;li&gt;Local Qwen AI for sensitive data&lt;/li&gt;
&lt;li&gt;AES-256 encrypted SQLite&lt;/li&gt;
&lt;li&gt;Zero cloud exposure for private data&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🧠 Smart Context
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI maintains conversation history&lt;/li&gt;
&lt;li&gt;Understands references ("there", "it", "that")&lt;/li&gt;
&lt;li&gt;Natural follow-up questions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🌍 Multilingual
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;English, German, Spanish&lt;/li&gt;
&lt;li&gt;30+ action verbs per language&lt;/li&gt;
&lt;li&gt;Flexible phrasing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ⚡ Fast
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Keyword matching: &amp;lt;1ms&lt;/li&gt;
&lt;li&gt;Local operations: &amp;lt;1s&lt;/li&gt;
&lt;li&gt;OpenAI queries: 5-7s&lt;/li&gt;
&lt;li&gt;Always shows 🗄️ icon for local DB operations&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  System Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;macOS 12.0+ (Monterey or later)&lt;/li&gt;
&lt;li&gt;Zsh shell&lt;/li&gt;
&lt;li&gt;Python 3.9+&lt;/li&gt;
&lt;li&gt;~5GB disk space (for Qwen model)&lt;/li&gt;
&lt;li&gt;8GB RAM minimum (16GB recommended)&lt;/li&gt;
&lt;li&gt;OpenAI API key&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Qwen 2.5 Coder?
&lt;/h2&gt;

&lt;p&gt;I tested several local models and Qwen 2.5 Coder was surprisingly good at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generating SQL from natural language&lt;/li&gt;
&lt;li&gt;Understanding multilingual input&lt;/li&gt;
&lt;li&gt;Pattern recognition for data extraction&lt;/li&gt;
&lt;li&gt;Running efficiently on 7B parameters&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Future Improvements
&lt;/h2&gt;

&lt;p&gt;Looking for contributors! Especially for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;More languages&lt;/strong&gt; (French, Italian, Portuguese)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows/Linux ports&lt;/strong&gt; (currently macOS only)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better error handling&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testing on different macOS versions&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Open Source
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/martinschenk/ai-chat-terminal" rel="noopener noreferrer"&gt;https://github.com/martinschenk/ai-chat-terminal&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contributions:&lt;/strong&gt; Welcome!&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This scratches an itch I had - wanting the power of GPT-4 without accidentally logging sensitive data. The keyword-based routing is simple but effective.&lt;/p&gt;

&lt;p&gt;Would love feedback on the privacy model - I'm sure there are edge cases I haven't considered!&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub Repo: &lt;a href="https://github.com/martinschenk/ai-chat-terminal" rel="noopener noreferrer"&gt;https://github.com/martinschenk/ai-chat-terminal&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Hacker News Discussion: &lt;a href="https://news.ycombinator.com/submitted?id=ma8nk" rel="noopener noreferrer"&gt;https://news.ycombinator.com/submitted?id=ma8nk&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>privacy</category>
      <category>opensource</category>
      <category>terminal</category>
    </item>
    <item>
      <title>✅ Laravel Cookie Consent – Plug &amp; play GDPR/DSGVO modal</title>
      <dc:creator>Martin Schenk</dc:creator>
      <pubDate>Fri, 30 May 2025 07:38:32 +0000</pubDate>
      <link>https://dev.to/martinschenk/my-new-laravel-cookie-consent-plugin-j1a</link>
      <guid>https://dev.to/martinschenk/my-new-laravel-cookie-consent-plugin-j1a</guid>
      <description>&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%2Fuo1cxe1x459zb383llvz.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.amazonaws.com%2Fuploads%2Farticles%2Fuo1cxe1x459zb383llvz.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hey folks! 👋&lt;br&gt;
I just released a lightweight, developer-friendly cookie consent plugin for Laravel 11+ projects. It’s focused on real GDPR/DSGVO compliance – with a configurable modal, full Alpine.js support, language preferences, and real-time cookie blocking.&lt;/p&gt;

&lt;p&gt;🔹 Easy to install&lt;br&gt;
🔹 No frontend lock-in (customizable view &amp;amp; JS logic)&lt;br&gt;
🔹 Works with Tailwind or plain CSS&lt;br&gt;
🔹 Built for modern Laravel stacks&lt;/p&gt;

&lt;p&gt;Check it out here:&lt;br&gt;
👉 &lt;a href="https://github.com/martinschenk/laravel-cookie-consent" rel="noopener noreferrer"&gt;https://github.com/martinschenk/laravel-cookie-consent&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback, stars ⭐️ and contributions are very welcome! 🙏&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>opensource</category>
      <category>plugin</category>
    </item>
    <item>
      <title>Laravel 11/12 Cookie consent Plugin GDPR compliant 🇪🇺</title>
      <dc:creator>Martin Schenk</dc:creator>
      <pubDate>Fri, 30 May 2025 07:27:46 +0000</pubDate>
      <link>https://dev.to/martinschenk/laravel-1112-cookie-consent-plugin-dgpr-compliant-1o2d</link>
      <guid>https://dev.to/martinschenk/laravel-1112-cookie-consent-plugin-dgpr-compliant-1o2d</guid>
      <description>&lt;p&gt;✅ Laravel Cookie Consent – Plug &amp;amp; play GDPR/DSGVO modal&lt;/p&gt;

&lt;p&gt;Body:&lt;br&gt;
Hey folks! 👋&lt;br&gt;
I just released a lightweight, developer-friendly cookie consent plugin for Laravel 11+ projects. It’s focused on real GDPR/DSGVO compliance – with a configurable modal, full Alpine.js support, language preferences, and real-time cookie blocking.&lt;/p&gt;

&lt;p&gt;🔹 Easy to install&lt;br&gt;
🔹 No frontend lock-in (customizable view &amp;amp; JS logic)&lt;br&gt;
🔹 Works with Tailwind or plain CSS&lt;br&gt;
🔹 Built for modern Laravel stacks&lt;/p&gt;

&lt;p&gt;Check it out here:&lt;br&gt;
👉 &lt;a href="https://github.com/martinschenk/laravel-cookie-consent" rel="noopener noreferrer"&gt;https://github.com/martinschenk/laravel-cookie-consent&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback, stars ⭐️ and contributions are very welcome! 🙏&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>opensource</category>
      <category>security</category>
    </item>
  </channel>
</rss>
